X Timeline Manager

跟踪并通过本地文件在设备之间同步您在 Twitter/X 上的最后阅读位置。

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

您需要先安装一款用户脚本管理器扩展,例如 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 a local file for cross-device sync.
// @description:de     Verfolgt und synchronisiert Ihre letzte Leseposition auf Twitter/X mithilfe einer lokalen Datei für geräteübergreifende Synchronisierung.
// @description:es     Rastrea y sincroniza tu última posición de lectura en Twitter/X utilizando un archivo local para sincronización entre dispositivos.
// @description:fr     Suit et synchronise votre dernière position de lecture sur Twitter/X en utilisant un fichier local pour la synchronisation entre appareils.
// @description:zh-CN  跟踪并通过本地文件在设备之间同步您在 Twitter/X 上的最后阅读位置。
// @description:ru     Отслеживает и синхронизирует вашу последнюю позицию чтения на Twitter/X с помощью локального файла для синхронизации между устройствами.
// @description:ja     ローカルファイルを使用して、Twitter/Xでの最後の読書位置を追跡し、デバイス間で同期します。
// @description:pt-BR  Rastreia e sincroniza sua última posição de leitura no Twitter/X usando um arquivo local para sincronização entre dispositivos.
// @description:hi     Twitter/X पर आपकी अंतिम पठन स्थिति को ट्रैक और सिंक करता है, स्थानीय फ़ाइल के माध्यम से उपकरणों के बीच सिंक्रनाइज़ेशन करता है。
// @icon               https://cdn-icons-png.flaticon.com/128/14417/14417460.png
// @namespace          http://tampermonkey.net/
// @version            2024.11.29
// @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
    const fileName = "last_read_position.json"; // Name der Datei für die Leseposition
    let folderHandle = null; // Globaler Ordnerzugriff
    let isAutoScrolling = false; // Markiert, ob das Skript automatisch scrollt
    let isSearching = false; // Markiert, ob nach Leseposition gesucht wird
    let popup; // Referenz für das Popup

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

        // Prüfe, ob ein gespeicherter Ordner existiert
        const folderMetadata = localStorage.getItem("folderHandle");
        if (!folderMetadata) {
            console.warn("⚠️ Kein gespeicherter Ordnerzugriff gefunden.");
            showPopup(); // Zeige Popup, um Benutzer zum Auswählen eines Ordners aufzufordern
        } else {
            console.log("✅ Speicherordner gefunden. Bitte autorisieren Sie den Zugriff.");
            showPopup(); // Ordnerzugriff erneut erlauben
        }
    };

    function showPopup() {
        popup = document.createElement("div");
        popup.textContent = "Set up a sync folder to save your reading position.";
        popup.style.position = "fixed";
        popup.style.top = "50%";
        popup.style.left = "50%";
        popup.style.transform = "translate(-50%, -50%)";
        popup.style.backgroundColor = "black";
        popup.style.color = "white";
        popup.style.padding = "20px";
        popup.style.borderRadius = "8px";
        popup.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.3)";
        popup.style.textAlign = "center";
        popup.style.zIndex = "10000";

        const button = document.createElement("button");
        button.textContent = "Set Sync Folder";
        button.style.marginTop = "10px";
        button.style.padding = "10px 15px";
        button.style.fontSize = "14px";
        button.style.backgroundColor = "white";
        button.style.color = "black";
        button.style.border = "none";
        button.style.borderRadius = "4px";
        button.style.cursor = "pointer";

        button.addEventListener("click", async () => {
            console.log("🗂 Benutzer öffnet Ordner-Auswahldialog...");
            folderHandle = await selectFolderHandle();
            if (folderHandle) {
                console.log("✅ Ordner erfolgreich ausgewählt.");
                popup.remove(); // Popup ausblenden
                saveFolderHandleMetadata();
                await initializeScript();
            } else {
                console.warn("⚠️ Kein Ordner ausgewählt. Bitte erneut versuchen.");
            }
        });

        popup.appendChild(button);
        document.body.appendChild(popup);
    }

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

        if (lastReadPost?.timestamp && lastReadPost?.authorHandler) {
            console.log(`📍 Geladene Leseposition: ${lastReadPost.timestamp}, @${lastReadPost.authorHandler}`);
            startSearchForLastReadPost();
        } else {
            console.log("❌ Keine gespeicherte Leseposition gefunden. Initialisiere Standard-Leseposition.");
            saveLastReadPostToFile(); // Speichere Standard-Leseposition
        }

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

        // Überwache manuelles Scrollen
        window.addEventListener("scroll", () => {
            if (!isAutoScrolling && !isSearching) {
                markCentralVisiblePost(true); // Speichere Leseposition nur bei manuellem Scrollen
            }
        });
    }

    async function selectFolderHandle() {
        try {
            return await window.showDirectoryPicker();
        } catch (err) {
            console.warn("⚠️ Zugriff auf lokalen Ordner verweigert oder fehlgeschlagen:", err);
            return null;
        }
    }

    function saveFolderHandleMetadata() {
        try {
            localStorage.setItem("folderHandle", "true");
            console.log("💾 Speicherordner erfolgreich gespeichert.");
        } catch (err) {
            console.error("❌ Fehler beim Speichern des Speicherordners:", err);
        }
    }

    async function getFileHandle(create = false) {
        if (!folderHandle) {
            console.warn("⚠️ Kein gültiger Ordnerzugriff. Datei kann nicht geöffnet werden.");
            return null;
        }

        try {
            return await folderHandle.getFileHandle(fileName, { create });
        } catch (err) {
            console.warn("⚠️ Datei konnte nicht abgerufen werden:", err);
            return null;
        }
    }

    async function loadLastReadPostFromFile() {
        try {
            const handle = await getFileHandle(false); // Datei öffnen, falls vorhanden
            if (handle) {
                console.log("📄 Datei gefunden. Lese Leseposition...");
                const file = await handle.getFile();
                const text = await file.text();
                lastReadPost = JSON.parse(text);
                console.log("✅ Leseposition erfolgreich geladen:", lastReadPost);
            } else {
                console.warn("⚠️ Keine Datei gefunden. Erstelle eine neue Leseposition-Datei.");
                await saveLastReadPostToFile();
            }
        } catch (err) {
            console.warn("⚠️ Leseposition konnte nicht aus der Datei gelesen werden:", err);
        }
    }

    async function saveLastReadPostToFile() {
        if (!folderHandle) {
            console.warn("⚠️ Kein Ordnerzugriff verfügbar. Überspringe das Speichern.");
            return;
        }

        if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler) {
            console.log("❌ Keine gültige Leseposition gefunden. Speichere Standardwerte.");
            return;
        }

        try {
            const handle = await getFileHandle(true); // Datei erstellen oder öffnen
            if (!handle) {
                console.warn("⚠️ Datei-Handle nicht verfügbar. Speicherung abgebrochen.");
                return;
            }

            const writable = await handle.createWritable();
            await writable.write(JSON.stringify(lastReadPost, null, 2));
            await writable.close();
            console.log("💾 Leseposition erfolgreich gespeichert:", lastReadPost);
        } catch (err) {
            console.error("❌ Fehler beim Speichern der Leseposition:", err);
        }
    }

    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 interval = setInterval(() => {
            const matchedPost = findPostByData(lastReadPost);
            if (matchedPost) {
                clearInterval(interval);
                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) {
        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 observeForNewPosts() {
        const observer = new MutationObserver(() => {
            if (window.scrollY > 3) {
                console.log("🔒 Button-Suche übersprungen: Scroll-Position ist nicht nahe der Oberseite.");
                return;
            }

            const newPostsButton = getNewPostsButton();
            if (newPostsButton) {
                console.log("🆕 Neue Beiträge gefunden. Klicke auf den Button.");
                clickNewPostsButton(newPostsButton);
                startSearchForLastReadPost(); // Suche erneut starten
            }
        });

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

    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();
        } else {
            console.log(`⚠️ Ältere Leseposition ignoriert: ${postTimestamp}, @${authorHandler}`);
        }
    }

    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 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.");
        }, 500);
    }

    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 scrollToPost(post) {
        if (!post) {
            console.log("❌ Kein Beitrag zum Scrollen gefunden.");
            return;
        }

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