X Timeline Manager

跟踪并通过Tampermonkey的内部存储在Twitter/X上同步您的最后阅读位置。

目前为 2024-12-01 提交的版本。查看 最新版本

// ==UserScript==
// @name              X Timeline Manager
// @description       Tracks and syncs your last reading position on Twitter/X using Tampermonkey's internal storage.
// @description:de    Verfolgt und synchronisiert Ihre letzte Leseposition auf Twitter/X mit der internen Speicherung von Tampermonkey.
// @description:es    Rastrea y sincroniza tu última posición de lectura en Twitter/X utilizando el almacenamiento interno de Tampermonkey.
// @description:fr    Suit et synchronise votre dernière position de lecture sur Twitter/X en utilisant le stockage interne de Tampermonkey.
// @description:zh-CN 跟踪并通过Tampermonkey的内部存储在Twitter/X上同步您的最后阅读位置。
// @description:ru    Отслеживает и синхронизирует вашу последнюю позицию чтения на Twitter/X, используя внутреннее хранилище Tampermonkey.
// @description:ja    Tampermonkeyの内部ストレージを使用して、Twitter/Xでの最後の読書位置を追跡および同期します。
// @description:pt-BR Rastrea e sincroniza sua última posição de leitura no Twitter/X usando o armazenamento interno do Tampermonkey.
// @description:hi    Tampermonkey के आंतरिक संग्रहण का उपयोग करके Twitter/X पर आपकी अंतिम पठन स्थिति को ट्रैक और सिंक करता है.
// @description:ar    يتتبع ويزامن آخر موضع قراءة لك على Twitter/X باستخدام التخزين الداخلي لـ Tampermonkey.
// @icon              https://x.com/favicon.ico
// @namespace         http://tampermonkey.net/
// @version           2024.12.1
// @author            Copiis
// @license           MIT
// @match             https://x.com/home
// @grant             GM_setValue
// @grant             GM_getValue
// ==/UserScript==

/*
If you find this script useful and would like to support my work, consider making a small donation! 
Your generosity helps me maintain and improve projects like this one. 😊

Bitcoin (BTC): bc1quc5mkudlwwkktzhvzw5u2nruxyepef957p68r7
PayPal: https://www.paypal.com/paypalme/Coopiis?country.x=DE&locale.x=de_DE

Thank you for your support! ❤️
*/

(function () {
    let lastReadPost = null; // Letzte Leseposition
    let isAutoScrolling = false;
    let isSearching = false;

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

    async function initializeScript() {
        console.log("🔧 Lade Leseposition...");
        await loadLastReadPostFromFile();

        if (lastReadPost?.timestamp && lastReadPost?.authorHandler) {
            console.log(`📍 Geladene Leseposition: ${lastReadPost.timestamp}, @${lastReadPost.authorHandler}`);
            await startSearchForLastReadPost();
        } else {
            console.warn("⚠️ Keine gültige Leseposition gefunden. Suche übersprungen.");
        }

        console.log("🔍 Starte Beobachtung für neue Beiträge...");
        observeForNewPosts();

        window.addEventListener("scroll", () => {
            if (!isAutoScrolling && !isSearching) {
                markCentralVisiblePost(true);
            }
        });
    }

    async function loadLastReadPostFromFile() {
        try {
            const data = GM_getValue("lastReadPost", null);
            if (data) {
                lastReadPost = JSON.parse(data);
                console.log("✅ Leseposition erfolgreich geladen:", lastReadPost);
            } else {
                console.warn("⚠️ Keine gespeicherte Leseposition gefunden.");
                lastReadPost = null;
            }
        } catch (err) {
            console.error("⚠️ Fehler beim Laden der Leseposition:", err);
            lastReadPost = null;
        }
    }

    async function saveLastReadPostToFile() {
        try {
            if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler) {
                console.warn("⚠️ Keine gültige Leseposition vorhanden. Speichern übersprungen.");
                return;
            }

            GM_setValue("lastReadPost", JSON.stringify(lastReadPost));
            console.log("💾 Leseposition erfolgreich gespeichert:", lastReadPost);
        } catch (err) {
            console.error("❌ Fehler beim Speichern der Leseposition:", err);
        }
    }

    function observeForNewPosts() {
        const observer = new MutationObserver(() => {
            const newPostsIndicator = getNewPostsIndicator();

            if (newPostsIndicator && window.scrollY === 0) {
                // Nur auslösen, wenn der Benutzer oben auf der Seite ist
                console.log("🆕 Neue Beiträge erkannt. Klicke auf den Indikator.");
                clickNewPostsIndicator(newPostsIndicator);
            }
        });

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

    function getNewPostsIndicator() {
        return document.querySelector('div[aria-label*="ungelesene Elemente"]');
    }

    function clickNewPostsIndicator(indicator) {
        if (!indicator) {
            console.warn("⚠️ Kein Indikator für neue Beiträge gefunden.");
            return;
        }

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

            if (!isSearching) {
                console.log("🔄 Starte die Suche nach der Lesestelle nach dem Laden neuer Beiträge...");
                startSearchForLastReadPost();
            }
        }, 500);
    }

    async function startSearchForLastReadPost() {
        if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler) {
            console.log("❌ Keine gültige Leseposition verfügbar. Suche übersprungen.");
            return;
        }

        isSearching = true;
        isAutoScrolling = true;
        console.log("🔍 Suche nach der letzten Leseposition gestartet...");

        const searchInterval = setInterval(() => {
            const matchedPost = findPostByData(lastReadPost);
            if (matchedPost) {
                clearInterval(searchInterval);
                isSearching = false;
                isAutoScrolling = 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 findPostByData(data) {
        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 markCentralVisiblePost(save = true) {
        const centralPost = getCentralVisiblePost();
        if (!centralPost) {
            console.log("❌ Kein zentral sichtbarer Beitrag gefunden.");
            return;
        }

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

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

        if (!lastReadPost || new Date(postTimestamp) > new Date(lastReadPost.timestamp)) {
            lastReadPost = { timestamp: postTimestamp, authorHandler };
            console.log(`💾 Neuste Leseposition aktualisiert: ${postTimestamp}, @${authorHandler}`);
            if (save) saveLastReadPostToFile();
        }
    }

    function getCentralVisiblePost() {
        const posts = Array.from(document.querySelectorAll("article"));
        const centerY = window.innerHeight / 2;

        return posts.reduce((closestPost, currentPost) => {
            const rect = currentPost.getBoundingClientRect();
            const distanceToCenter = Math.abs(centerY - (rect.top + rect.bottom) / 2);

            if (!closestPost) return currentPost;

            const closestRect = closestPost.getBoundingClientRect();
            const closestDistance = Math.abs(centerY - (closestRect.top + closestRect.bottom) / 2);

            return distanceToCenter < closestDistance ? currentPost : closestPost;
        }, null);
    }

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

        isAutoScrolling = true;
        post.scrollIntoView({ behavior: "smooth", block: "center" });
        setTimeout(() => {
            isAutoScrolling = false;
            console.log("✅ Beitrag wurde erfolgreich zentriert!");
        }, 1000);
    }
})();

QingJ © 2025

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