X Timeline Manager

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

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴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.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
    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...");

        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 (folderHandle && lastReadPost?.timestamp && lastReadPost?.authorHandler) {
            console.log(`📍 Geladene Leseposition: ${lastReadPost.timestamp}, @${lastReadPost.authorHandler}`);
            startSearchForLastReadPost();
        } else {
            console.log("❌ Keine gültige Leseposition oder Ordnerzugriff. Suche übersprungen.");
        }

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

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

    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);
            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);
            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) {
        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 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);
    }

    function observeForNewPosts() {
        const observer = new MutationObserver(() => {
            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 getNewPostsButton() {
        return Array.from(document.querySelectorAll("div.css-146c3p1"))
            .find(div => div.textContent && /Post anzeigen|Posts 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 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);
    }
})();