X Timeline Manager

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

// ==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.");
    }
})();