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-21 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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.21
// @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 isAutoScrolling = false; // Status für automatisches Scrollen
    let savedTopPostInfo = null; // Speichert Name, Handler und erstes Wort des Beitrags

    // Funktion zur Überprüfung der Sichtbarkeit und Aktivität
    function checkVisibility() {
        if (document.visibilityState === 'visible' && document.hasFocus()) {
            console.log("[DEBUG] X.com ist jetzt aktiv und sichtbar.");
        } else {
            console.log("[DEBUG] X.com ist entweder im Hintergrund oder nicht aktiv.");
            disableAutomationByScrolling(); // Automatik ausschalten
        }
    }

    // Automatik durch Scrollen deaktivieren
    function disableAutomationByScrolling() {
        console.log("[DEBUG] Automatik wird durch Scrollen deaktiviert.");
        window.scrollBy(0, 10); // Scrollt 10 Pixel nach unten
        isAutomationActive = false; // Automatik deaktivieren
    }

    // Initialisiere die Sichtbarkeitsprüfung
    function initVisibilityListeners() {
        document.addEventListener('visibilitychange', checkVisibility);
        window.addEventListener('focus', checkVisibility);
        window.addEventListener('blur', checkVisibility);

        // Initial prüfen
        checkVisibility();
    }

    // Starte die kontinuierliche Überprüfung, sobald die Seite geladen wird
    document.addEventListener('DOMContentLoaded', () => {
        console.log("[DEBUG] Seite geladen. Starte Überprüfung für sichtbaren Handler...");
        checkForVisibleHandler();
        initVisibilityListeners();
    });

    // Funktion, um kontinuierlich nach einem sichtbaren Handler zu suchen
    function checkForVisibleHandler() {
        const checkInterval = setInterval(() => {
            const topPost = getTopVisiblePost();
            if (topPost) {
                savedTopPostInfo = getPostInfo(topPost);
                console.log("[DEBUG] Sichtbarer Beitrag gefunden und gespeichert:", savedTopPostInfo);
                clearInterval(checkInterval); // Stoppe die Überprüfung, sobald ein Beitrag gefunden wurde
            }
        }, 100); // Überprüfe alle 100ms
    }

    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 (isAutoScrolling) {
            console.log("[DEBUG] Automatisches Scrollen erkannt. Scroll-Event ignoriert.");
            return;
        }

        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 Beitrag gespeichert:", savedTopPostInfo);
        } else {
            console.log("[DEBUG] Kein oberster Beitrag 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";

        const postContentElement = post.querySelector('div[data-testid="tweetText"]'); // Text des Beitrags
        const postContent = postContentElement?.textContent?.trim() || "";
        const firstWord = postContent.split(/\s+/)[0]; // Erstes Wort des tatsächlichen Beitrags

        return { name, handler, firstWord };
    }

    function getTopVisiblePost() {
        const posts = Array.from(document.querySelectorAll('article'));
        console.log(`[DEBUG] Anzahl gefundener Beiträge: ${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 Beitrag nach Laden gefunden. Scrollen...");
                scrollToPost(matchedPost, 'end'); // Scrollt so, dass der gespeicherte Beitrag 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");

            const postContentElement = post.querySelector('div[data-testid="tweetText"]'); // Text des Beitrags
            const postContent = postContentElement?.textContent?.trim() || "";
            const firstWord = postContent.split(/\s+/)[0]; // Erstes Wort des tatsächlichen Beitrags

            return (
                name === postInfo.name &&
                handler === postInfo.handler &&
                firstWord === postInfo.firstWord // Verhindert Duplikate
            );
        });
    }

    function scrollToPost(post, position = 'end') {
        console.log(`[DEBUG] Scrollen zum gespeicherten Beitrag (Position: ${position})...`);
        isAutoScrolling = true; // Setzt den Status, dass ein automatisches Scrollen läuft
        post.scrollIntoView({ behavior: 'smooth', block: position });
        setTimeout(() => {
            isAutoScrolling = false; // Scrollen abgeschlossen
            isAutomationActive = false;
            console.log("[DEBUG] Automatisches Scrollen beendet. Automatik deaktiviert.");
        }, 1000); // Warte, bis das Scrollen abgeschlossen ist
    }
})();