X (Twitter) Feed to Markdown with Auto-Scroll

Extracts content from the X (Twitter) feed and converts it to Markdown format, with an added direct auto-scroll feature.

// ==UserScript==
// @name         X (Twitter) Feed to Markdown with Auto-Scroll
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  Extracts content from the X (Twitter) feed and converts it to Markdown format, with an added direct auto-scroll feature.
// @match        https://x.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- Markdown转换功能的状态变量 ---
    let isMonitoring = false;
    let collectedTweets = new Map();
    let observer;

    // --- 自动滚动功能的状态变量 ---
    let isAutoScrolling = false;
    let scrollIntervalId = null;

    // --- 创建Markdown转换按钮 ---
    const markdownButton = document.createElement('button');
    markdownButton.textContent = '开始转换Markdown';
    Object.assign(markdownButton.style, {
        position: 'fixed',
        top: '10px',
        right: '10px',
        zIndex: '9999',
        padding: '8px 16px',
        backgroundColor: '#1DA1F2',
        color: 'white',
        border: 'none',
        borderRadius: '5px',
        cursor: 'pointer',
        fontSize: '14px'
    });
    document.body.appendChild(markdownButton);
    markdownButton.addEventListener('click', toggleMonitoring);

    // --- 创建自动滚动按钮 ---
    const scrollButton = document.createElement('button');
    scrollButton.textContent = '开始自动滚动';
    Object.assign(scrollButton.style, {
        position: 'fixed',
        top: '55px', // 放在第一个按钮的下方
        right: '10px',
        zIndex: '9999',
        padding: '8px 16px',
        backgroundColor: '#28a745', // 绿色
        color: 'white',
        border: 'none',
        borderRadius: '5px',
        cursor: 'pointer',
        fontSize: '14px'
    });
    document.body.appendChild(scrollButton);
    scrollButton.addEventListener('click', toggleAutoScroll);


    // --- 自动滚动功能 ---

    /**
     * 【已修改】直接执行浏览器滚动,而不是模拟按键
     */
    function performScroll() {
        // window.scrollBy(x, y) 让窗口从当前位置滚动指定的像素值
        // x为0表示水平不滚动,y为400表示向下滚动400像素
        // 你可以调整 400 这个数值来改变滚动的速度/距离
        window.scrollBy(0, 400);
        console.log('Auto-scroll: Scrolled down by 400px.');
    }

    /**
     * 切换自动滚动状态
     */
    function toggleAutoScroll() {
        if (isAutoScrolling) {
            // 停止滚动
            clearInterval(scrollIntervalId);
            scrollIntervalId = null;
            isAutoScrolling = false;
            scrollButton.textContent = '开始自动滚动';
            scrollButton.style.backgroundColor = '#28a745'; // 恢复绿色
            console.log('自动滚动已停止。');
        } else {
            // 开始滚动
            isAutoScrolling = true;
            // 【已修改】调用新的滚动函数
            scrollIntervalId = setInterval(performScroll, 500); // 每500ms滚动一次
            scrollButton.textContent = '停止自动滚动';
            scrollButton.style.backgroundColor = '#dc3545'; // 变为红色
            console.log('自动滚动已开始...');
        }
    }


    // --- Markdown转换功能 (原脚本逻辑) ---
    // (以下代码保持不变)

    function toggleMonitoring() {
        if (isMonitoring) {
            stopMonitoring();
            displayCollectedTweets();
        } else {
            startMonitoring();
        }
    }

    function startMonitoring() {
        isMonitoring = true;
        markdownButton.textContent = '停止并导出Markdown';
        markdownButton.style.backgroundColor = '#FF4136';
        collectedTweets.clear();
        console.log("开始监控推文...");

        document.querySelectorAll('article[data-testid="tweet"]').forEach(processTweet);

        const config = { childList: true, subtree: true };
        observer = new MutationObserver(mutations => {
            for (const mutation of mutations) {
                if (mutation.addedNodes.length) {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            if (node.matches('article[data-testid="tweet"]')) {
                                processTweet(node);
                            }
                            node.querySelectorAll('article[data-testid="tweet"]').forEach(processTweet);
                        }
                    });
                }
            }
        });

        observer.observe(document.body, config);
    }

    function stopMonitoring() {
        isMonitoring = false;
        markdownButton.textContent = '开始转换Markdown';
        markdownButton.style.backgroundColor = '#1DA1F2';
        if (observer) {
            observer.disconnect();
        }
        console.log("停止监控。");
    }

    function processTweet(tweet) {
        if (tweet.querySelector('[data-testid="promotedTweet"]')) return;
        const timeElement = tweet.querySelector('time[datetime]');
        if (timeElement && timeElement.closest('div[data-testid="User-Name"]')?.nextElementSibling?.textContent?.includes('Ad')) {
             return;
        }

        const tweetData = formatTweet(tweet);
        if (tweetData && tweetData.url && !collectedTweets.has(tweetData.url)) {
            collectedTweets.set(tweetData.url, tweetData.markdown);
        }
    }

    function displayCollectedTweets() {
        if (collectedTweets.size === 0) {
            alert('没有收集到任何推文。');
            return;
        }

        const sortedTweets = Array.from(collectedTweets.values()).sort((a, b) => {
             const timeMatchA = a.match(/\*\*发布时间\*\*: (.*)/);
             const timeMatchB = b.match(/\*\*发布时间\*\*: (.*)/);
             if (!timeMatchA || !timeMatchB) return 0;
             const timeA = new Date(timeMatchA[1]);
             const timeB = new Date(timeMatchB[1]);
             return timeB - timeA;
        });

        const markdownOutput = sortedTweets.join('\n\n---\n\n');
        const newWindow = window.open('', '_blank');
        newWindow.document.write('<pre style="white-space: pre-wrap; word-wrap: break-word; padding: 10px;">' + markdownOutput.replace(/</g, "&lt;").replace(/>/g, "&gt;") + '</pre>');
        newWindow.document.title = 'Twitter Feed as Markdown';
    }

    function extractTextContent(element) {
        if (!element) return '';
        let text = '';
        element.childNodes.forEach(node => {
            if (node.nodeType === Node.ELEMENT_NODE) {
                if (node.tagName === 'IMG') {
                    text += node.alt;
                } else if (node.tagName === 'A') {
                    const url = node.href;
                    if (!url.includes('/photo/') && !url.includes('/video/')) {
                        text += `[${node.textContent}](${url})`;
                    }
                } else {
                    text += node.textContent;
                }
            } else {
                text += node.textContent;
            }
        });
        return text.trim();
    }

    function formatTweet(tweet) {
        const timeElement = tweet.querySelector('time');
        if (!timeElement) return null;

        const linkElement = timeElement.closest('a');
        if (!linkElement) return null;

        const tweetUrl = 'https://x.com' + linkElement.getAttribute('href');
        const authorHandle = `@${tweetUrl.split('/')[3]}`;
        const postTime = timeElement.getAttribute('datetime');

        const mainContentElement = tweet.querySelector('div[data-testid="tweetText"]');
        const mainContent = extractTextContent(mainContentElement);

        let quoteContent = '';
        const quoteHeader = Array.from(tweet.querySelectorAll('span')).find(s => s.textContent === 'Quote');
        if (quoteHeader) {
            const quoteContainer = quoteHeader.parentElement.nextElementSibling;
            if (quoteContainer && quoteContainer.getAttribute('role') === 'link') {
                const quoteAuthorEl = quoteContainer.querySelector('[data-testid="User-Name"]');
                const quoteAuthor = quoteAuthorEl ? quoteAuthorEl.textContent.replace(/\n/g, ' ').replace(/\s+/g, ' ').trim() : '未知作者';
                const quoteTextEl = quoteContainer.querySelector('div[lang]');
                const quoteText = extractTextContent(quoteTextEl);
                const quoteLines = `**${quoteAuthor}**: ${quoteText}`.split('\n');
                quoteContent = `\n\n${quoteLines.map(line => `> ${line}`).join('\n> ')}`;
            }
        }

        let sharedLink = '';
        const cardWrapper = tweet.querySelector('[data-testid="card.wrapper"]');
        if (cardWrapper) {
            const cardLinkEl = cardWrapper.querySelector('a');
            if(cardLinkEl) {
                const cardUrl = cardLinkEl.href;
                const detailContainer = cardWrapper.querySelector('[data-testid$="detail"]');
                let cardTitle = '';
                if (detailContainer) {
                    const spans = detailContainer.querySelectorAll('span');
                    cardTitle = spans.length > 1 ? spans[1].textContent : '链接';
                } else {
                    const largeMediaTitleEl = cardWrapper.querySelector('div[class*="r-fdjqy7"] span');
                    cardTitle = largeMediaTitleEl ? largeMediaTitleEl.textContent : '链接';
                }
                sharedLink = `\n- **分享链接**: [${cardTitle.trim()}](${cardUrl})`;
            }
        }

        const socialContext = tweet.querySelector('[data-testid="socialContext"]');
        let repostedBy = '';
        if (socialContext && socialContext.textContent.toLowerCase().includes('reposted')) {
            repostedBy = `> *由 ${socialContext.textContent.replace(/reposted/i, '').trim()} 转推*\n\n`;
        }

        let threadIndicator = '';
        const hasThreadLink = Array.from(tweet.querySelectorAll('a[role="link"] span')).some(span => span.textContent === 'Show this thread');
        if (hasThreadLink) {
            threadIndicator = `- **串推**: 是\n`;
        }

        let markdown = `${repostedBy}- **原文链接**: ${tweetUrl}\n`;
        markdown += `- **作者**: ${authorHandle}\n`;
        markdown += `- **发布时间**: ${postTime}\n`;
        markdown += threadIndicator;
        markdown += `- **推文内容**:\n${mainContent}${quoteContent}`;
        markdown += sharedLink;

        return {
            url: tweetUrl,
            markdown: markdown
        };
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址