Greasy Fork

Greasy Fork is available in English.

X Timeline Manager

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

当前为 2024-12-02 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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.2
// @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();

            // Schwellenwert für die Scrollposition von oben auf <= 3 ändern
            if (newPostsIndicator && window.scrollY <= 3) {
                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);
    }
})();