X.com Timeline Auto-Load with Uninterrupted Reading

Automatically loads new posts on X.com while keeping the reading position intact. Sets a virtual marker at the last visible handler (e.g., @username) before loading new posts and restores the view to this marker.

当前为 2024-11-20 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name               X.com Timeline Auto-Load with Uninterrupted Reading
// @name:de            X.com Timeline Auto-Load mit unterbrechungsfreiem Lesen
// @name:fr            X.com Timeline Auto-Load avec lecture ininterrompue
// @name:es            Carga automática de la línea de tiempo de X.com con lectura sin interrupciones
// @name:it            Caricamento automatico della timeline di X.com con lettura ininterrotta
// @name:zh            X.com 时间线自动加载,无缝阅读
// @name:ja            X.com タイムライン自動読み込みと中断のない読書
// @description        Automatically loads new posts on X.com while keeping the reading position intact. Sets a virtual marker at the last visible handler (e.g., @username) before loading new posts and restores the view to this marker.
// @description:de     Lädt automatisch neue Beiträge auf X.com, ohne die Leseposition zu verlieren. Setzt eine virtuelle Markierung am letzten sichtbaren Handler (z. B. @Benutzername) vor dem Laden neuer Beiträge und stellt die Ansicht zu dieser Markierung wieder her.
// @description:fr     Charge automatiquement les nouveaux messages sur X.com tout en conservant la position de lecture. Place un marqueur virtuel au dernier handle visible (par exemple, @nomutilisateur) avant de charger les nouveaux messages et restaure la vue à ce marqueur.
// @description:es     Carga automáticamente nuevos posts en X.com mientras mantiene la posición de lectura intacta. Coloca un marcador virtual en el último manejador visible (por ejemplo, @nombredeusuario) antes de cargar nuevos posts y restaura la vista a ese marcador.
// @description:it     Carica automaticamente nuovi post su X.com mantenendo intatta la posizione di lettura. Imposta un segnalibro virtuale sull'ultimo handle visibile (es. @nomeutente) prima di caricare nuovi post e ripristina la vista su quel segnalibro.
// @description:zh     在X.com上自动加载新帖子,同时保持阅读位置不变。在加载新帖子之前,在最后一个可见的处理器(例如@用户名)处设置一个虚拟标记,并将视图恢复到该标记。
// @description:ja     X.comで新しい投稿を自動的に読み込み、読書位置をそのまま保持します。新しい投稿を読み込む前に、最後に見えるハンドル(例:@ユーザー名)に仮想マーカーを設定し、このマーカーにビューを復元します。
// @namespace          http://tampermonkey.net/
// @version            2024.12.20
// @icon               https://cdn-icons-png.flaticon.com/128/14417/14417460.png
// @author             Copiis
// @match              https://x.com/*
// @grant              none
// @license            MIT
// ==/UserScript==

(function () {
    let isAutomationActive = false;
    let savedTopPostInfo = null; // Speichert Name und Handler des obersten Posts vor dem Laden neuer Beiträge

    const observer = new MutationObserver(() => {
        if (isAtTopOfPage() && !isAutomationActive) {
            console.log("[DEBUG] Scrollen zum oberen Ende erkannt. Automatik wird aktiviert.");
            extractPostInfoFromTopPost();
            isAutomationActive = true;
        }

        if (isAutomationActive) {
            const newPostsButton = getNewPostsButton();
            if (newPostsButton) {
                console.log("[DEBUG] 'Neue Posts anzeigen'-Button gefunden, wird geklickt.");
                newPostsButton.click();
                waitForNewPostsToLoad();
            }
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });

    window.addEventListener('scroll', () => {
        if (window.scrollY === 0 && !isAutomationActive) {
            console.log("[DEBUG] Scrollen zum oberen Ende erkannt. Automatik wird aktiviert.");
            extractPostInfoFromTopPost();
            isAutomationActive = true;
        } else if (window.scrollY > 0 && isAutomationActive) {
            console.log("[DEBUG] Automatik deaktiviert, da nicht oben.");
            isAutomationActive = false;
        }
    });

    function isAtTopOfPage() {
        return window.scrollY === 0;
    }

    function getNewPostsButton() {
        return Array.from(document.querySelectorAll('button, span')).find((button) =>
            /neue Posts anzeigen|Post anzeigen/i.test(button.textContent.trim())
        );
    }

    function extractPostInfoFromTopPost() {
        const topPost = getTopVisiblePost();
        if (topPost) {
            savedTopPostInfo = getPostInfo(topPost);
            console.log("[DEBUG] Oberster Post vor Laden neuer Beiträge gespeichert:", savedTopPostInfo);
        } else {
            savedTopPostInfo = null;
            console.log("[DEBUG] Kein oberster Post gefunden.");
        }
    }

    function getPostInfo(post) {
        const nameElement = post.querySelector('a[role="link"] div[dir="ltr"] span');
        const name = nameElement?.textContent?.trim() || "Unbekannt";

        const handlerElement = post.querySelector('a[role="link"]');
        const handler = handlerElement?.getAttribute("href") || "Unbekannt";

        return { name, handler };
    }

    function getTopVisiblePost() {
        const posts = Array.from(document.querySelectorAll('article'));
        console.log(`[DEBUG] Anzahl gefundener Posts: ${posts.length}`);
        return posts.length > 0 ? posts[0] : null;
    }

    function waitForNewPostsToLoad() {
        console.log("[DEBUG] Warte auf das Laden neuer Beiträge...");
        const checkInterval = setInterval(() => {
            const matchedPost = findPostByInfo(savedTopPostInfo);
            if (matchedPost) {
                clearInterval(checkInterval);
                console.log("[DEBUG] Gespeicherter Post nach Laden gefunden. Scrollen...");
                scrollToPost(matchedPost, 'end'); // Scrollt so, dass der gespeicherte Post am unteren Rand sichtbar ist
            }
        }, 100);
    }

    function findPostByInfo(postInfo) {
        if (!postInfo) return null;
        const posts = Array.from(document.querySelectorAll('article'));
        return posts.find((post) => {
            const nameElement = post.querySelector('a[role="link"] div[dir="ltr"] span');
            const name = nameElement?.textContent?.trim();

            const handlerElement = post.querySelector('a[role="link"]');
            const handler = handlerElement?.getAttribute("href");

            return name === postInfo.name && handler === postInfo.handler;
        });
    }

    function scrollToPost(post, position = 'end') {
        console.log(`[DEBUG] Scrollen zum gespeicherten Post (Position: ${position})...`);
        post.scrollIntoView({ behavior: 'smooth', block: position });
        isAutomationActive = false;
        console.log("[DEBUG] Automatik deaktiviert nach Scrollen.");
    }
})();