X Timeline Manager

Tracks and syncs your last reading position on Twitter/X using a local file for cross-device sync.

目前為 2024-11-28 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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.");
    }
})();