X Timeline Manager

自动跟踪并保存您在 Twitter/X 上的最后阅读位置,允许在刷新或导航后无缝恢复。

目前为 2024-11-28 提交的版本。查看 最新版本

// ==UserScript==
// @name               X Timeline Manager
// @description        Automatically tracks and saves your last reading position on Twitter/X, allowing seamless resumption after refreshing or navigating away.
// @description:de     Verfolgt und speichert automatisch Ihren letzten Lesefortschritt auf Twitter/X, sodass Sie nach einem Refresh oder Verlassen der Seite nahtlos fortfahren können.
// @description:fr     Suit et enregistre automatiquement votre dernière position de lecture sur Twitter/X, permettant une reprise facile après un rafraîchissement ou un changement de page.
// @description:es     Realiza un seguimiento y guarda automáticamente tu última posición de lectura en Twitter/X, permitiendo continuar sin problemas después de actualizar o cambiar de página.
// @description:it     Tiene traccia e salva automaticamente la tua ultima posizione di lettura su Twitter/X, consentendo una ripresa fluida dopo il refresh o la navigazione altrove.
// @description:pt     Acompanha e salva automaticamente sua última posição de leitura no Twitter/X, permitindo retomar sem interrupções após atualizar ou navegar para outro lugar.
// @description:ru     Автоматически отслеживает и сохраняет вашу последнюю позицию чтения в Twitter/X, позволяя беспрепятственно продолжить чтение после обновления или перехода на другую страницу.
// @description:zh-CN  自动跟踪并保存您在 Twitter/X 上的最后阅读位置,允许在刷新或导航后无缝恢复。
// @description:ja     Twitter/X での最後の読書位置を自動的に追跡して保存し、更新やページ遷移後にシームレスに再開できるようにします。
// @description:ko     Twitter/X에서 마지막 읽기 위치를 자동으로 추적하고 저장하여 새로 고침하거나 다른 페이지로 이동한 후에도 원활하게 이어갈 수 있습니다.
// @description:hi     Twitter/X पर आपके अंतिम पढ़ने की स्थिति को स्वचालित रूप से ट्रैक और सहेजता है, जिससे ताज़ा करने या दूसरी जगह नेविगेट करने के बाद भी आसानी से फिर से शुरू किया जा सके।
// @description:ar     يتتبع ويحفظ تلقائيًا آخر موضع قراءة لك على Twitter/X، مما يسمح بالاستئناف بسلاسة بعد التحديث أو التنقل بعيدًا。
// @description:ar     يقوم بتحميل المنشورات الجديدة تلقائيًا على X.com/Twitter ويعيدك إلى موضع القراءة.
// @icon               https://cdn-icons-png.flaticon.com/128/14417/14417460.png
// @supportURL         https://www.paypal.com/paypalme/Coopiis?country.x=DE&locale.x=de_DE
// @namespace          http://tampermonkey.net/
// @version            2024.11.28-2
// @author             Copiis
// @license            MIT
// @match              https://x.com/home
// @grant              GM_setValue
// @grant              GM_getValue
// ==/UserScript==

(function () {
    let lastReadPost = null; // Letzte Leseposition
    let isAutoScrolling = false; // Script-gesteuertes Scrollen verhindern
    let isSearching = false; // Verhindert Markierungen während der Suche

    window.onload = () => {
        console.log("🚀 Seite vollständig geladen. Initialisiere Skript...");
        initializeScript();
    };

    function initializeScript() {
        loadLastReadPost(); // Lese letzte Leseposition

        if (lastReadPost) {
            console.log(`📍 Geladene Leseposition: ${lastReadPost.timestamp}, @${lastReadPost.authorHandler}`);
            waitForPageToLoad(() => {
                scrollToLastReadPost();
            });
        } else {
            console.log("❌ Keine gespeicherte Leseposition gefunden.");
        }

        // Manuelles Scrollen überwachen
        window.addEventListener("scroll", () => {
            if (!isAutoScrolling && !isSearching) {
                markLatestPost();
            }
        });

        // DOM-Änderungen für neue Beiträge beobachten
        const observer = new MutationObserver(() => {
            if (window.scrollY <= 5) { // Überprüft, ob die Scroll-Position 5 Pixel oder weniger von oben entfernt ist
                const newPostsButton = getNewPostsButton();
                if (newPostsButton) {
                    console.log("🆕 Neue Beiträge gefunden. Klicke auf den Button.");
                    clickNewPostsButton(newPostsButton);
                }
            }
        });

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

    function markLatestPost() {
        const posts = Array.from(document.querySelectorAll("article"));

        if (posts.length === 0) {
            console.log("❌ Keine Beiträge gefunden.");
            return;
        }

        const latestPost = posts.reduce((latest, current) => {
            const currentTimestamp = getPostTimestamp(current);
            const latestTimestamp = getPostTimestamp(latest);

            return new Date(currentTimestamp) > new Date(latestTimestamp) ? current : latest;
        });

        if (!latestPost) {
            console.log("❌ Kein gültiger Beitrag gefunden.");
            return;
        }

        const postTimestamp = getPostTimestamp(latestPost);
        const authorHandler = getPostAuthorHandler(latestPost);

        if (!postTimestamp || !authorHandler) {
            console.log("❌ Jüngster Beitrag hat keine gültigen Daten.");
            return;
        }

        // Speichern nur, wenn der Beitrag sich ändert
        if (
            !lastReadPost ||
            lastReadPost.timestamp !== postTimestamp ||
            lastReadPost.authorHandler !== authorHandler
        ) {
            lastReadPost = { timestamp: postTimestamp, authorHandler };
            saveLastReadPost(); // Speichert ohne Konsolenausgabe
            console.log(`💾 Jüngster Beitrag gespeichert: ${postTimestamp}, @${authorHandler}`);
        }
    }

    function saveLastReadPost() {
        if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler) {
            console.log("❌ Ungültige Leseposition, Speichern übersprungen.");
            return;
        }

        GM_setValue("lastReadPost", JSON.stringify(lastReadPost));
    }

    function getNewPostsButton() {
        const primaryButton = Array.from(document.querySelectorAll("div.css-146c3p1 span.css-1jxf684"))
            .find(span => span.textContent.includes("Post anzeigen") || span.textContent.includes("Posts anzeigen"));

        if (!primaryButton) {
            console.log("🔍 Primärer Button nicht gefunden. Fallback wird verwendet.");
            return getAnyNewPostsButton();
        }
        return primaryButton.closest("div.css-146c3p1");
    }

    function getAnyNewPostsButton() {
        return Array.from(document.querySelectorAll("div"))
            .find(div => div.textContent && /Post[s]? anzeigen/i.test(div.textContent));
    }

    function clickNewPostsButton(button) {
        if (!button) {
            console.log("❌ Button ist nicht definiert.");
            return;
        }

        button.scrollIntoView({ behavior: "smooth", block: "center" });
        setTimeout(() => {
            button.click();
            console.log("✅ Button für neue Beiträge geklickt.");

            waitForNewPosts(() => {
                console.log("📥 Neue Beiträge wurden geladen.");
                scrollToLastReadPost();
            });
        }, 500);
    }

    function waitForNewPosts(callback) {
        const interval = setInterval(() => {
            const posts = document.querySelectorAll("article");
            if (posts.length > 0) {
                clearInterval(interval);
                callback();
            } else {
                console.log("⏳ Warte auf das Laden neuer Beiträge...");
            }
        }, 500);
    }

    function scrollToLastReadPost() {
        if (!lastReadPost) {
            console.log("❌ Keine Leseposition verfügbar. Überspringe.");
            return;
        }

        isSearching = true;

        waitForPageToLoad(() => {
            const interval = setInterval(() => {
                const matchedPost = findPostByData(lastReadPost);
                if (matchedPost) {
                    clearInterval(interval);
                    isSearching = false;
                    scrollToPost(matchedPost);
                    console.log(`🎯 Zuletzt gelesenen Beitrag gefunden: ${lastReadPost.timestamp}, @${lastReadPost.authorHandler}`);
                } else {
                    console.log("🔄 Beitrag nicht direkt gefunden. Suche weiter unten.");
                    window.scrollBy({ top: 500, behavior: "smooth" });
                }
            }, 1000);
        });
    }

    function scrollToPost(post) {
        if (!post) {
            console.log("❌ Kein Beitrag zum Scrollen gefunden.");
            return;
        }

        isAutoScrolling = true;
        post.scrollIntoView({ behavior: "smooth", block: "center" });
        setTimeout(() => {
            isAutoScrolling = false;
        }, 1000);
    }

    function findPostByData(data) {
        if (!data || !data.timestamp || !data.authorHandler) {
            console.log("❌ Ungültige Daten für die Suche nach einem Beitrag.");
            return null;
        }

        const posts = Array.from(document.querySelectorAll("article"));
        return posts.find(post => {
            const postTimestamp = getPostTimestamp(post);
            const authorHandler = getPostAuthorHandler(post);
            return postTimestamp === data.timestamp && authorHandler === data.authorHandler;
        });
    }

    function getPostTimestamp(post) {
        const timeElement = post.querySelector("time");
        return timeElement ? timeElement.getAttribute("datetime") : null;
    }

    function getPostAuthorHandler(post) {
        const handlerElement = post.querySelector('[role="link"][href*="/"]');
        if (handlerElement) {
            const handler = handlerElement.getAttribute("href");
            return handler && handler.startsWith("/") ? handler.slice(1) : null;
        }
        return null;
    }

    function waitForPageToLoad(callback) {
        const interval = setInterval(() => {
            if (document.readyState === "complete") {
                clearInterval(interval);
                callback();
            } else {
                console.log("⏳ Warte auf vollständiges Laden der Seite...");
            }
        }, 500);
    }

    function loadLastReadPost() {
        const savedData = GM_getValue("lastReadPost", null);
        if (savedData) {
            try {
                lastReadPost = JSON.parse(savedData);
            } catch (error) {
                console.error("❌ Fehler beim Laden der Leseposition. Initialisiere als null.", error);
                lastReadPost = null;
            }
        }
    }
})();