X Timeline Sync

Tracks and syncs your last reading position on Twitter/X, with manual and automatic options. Ideal for keeping track of new posts without losing your place.

目前為 2025-02-26 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 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 Sync
// @description       Tracks and syncs your last reading position on Twitter/X, with manual and automatic options. Ideal for keeping track of new posts without losing your place.
// @description:de    Verfolgt und synchronisiert Ihre letzte Leseposition auf Twitter/X, mit manuellen und automatischen Optionen. Perfekt, um neue Beiträge im Blick zu behalten, ohne die aktuelle Position zu verlieren.
// @description:es    Rastrea y sincroniza tu última posición de lectura en Twitter/X, con opciones manuales y automáticas. Ideal para mantener el seguimiento de las publicaciones nuevas sin perder tu posición.
// @description:fr    Suit et synchronise votre dernière position de lecture sur Twitter/X, avec des options manuelles et automatiques. Idéal pour suivre les nouveaux posts sans perdre votre place actuelle.
// @description:zh-CN 跟踪并同步您在 Twitter/X 上的最后阅读位置,提供手动和自动选项。完美解决在查看新帖子时不丢失当前位置的问题。
// @description:ru    Отслеживает и синхронизирует вашу последнюю позицию чтения на Twitter/X с ручными и автоматическими опциями. Идеально подходит для просмотра новых постов без потери текущей позиции.
// @description:ja    Twitter/X での最後の読み取り位置を追跡して同期します。手動および自動オプションを提供します。新しい投稿を見逃さずに現在の位置を維持するのに最適です。
// @description:pt-BR Rastrea e sincroniza sua última posição de leitura no Twitter/X, com opções manuais e automáticas. Perfeito para acompanhar novos posts sem perder sua posição atual.
// @description:hi    Twitter/X पर आपकी अंतिम पठन स्थिति को ट्रैक और सिंक करता है, मैनुअल और स्वचालित विकल्पों के साथ। नई पोस्ट देखते समय अपनी वर्तमान स्थिति को खोए बिना इसे ट्रैक करें।
// @description:ar    يتتبع ويزامن آخر موضع قراءة لك على Twitter/X، مع خيارات يدوية وتلقائية. مثالي لتتبع المشاركات الجديدة دون فقدان موضعك الحالي.
// @description:it    Traccia e sincronizza la tua ultima posizione di lettura su Twitter/X, con opzioni manuali e automatiche. Ideale per tenere traccia dei nuovi post senza perdere la posizione attuale.
// @description:ko    Twitter/X에서 마지막 읽기 위치를 추적하고 동기화합니다. 수동 및 자동 옵션 포함. 새로운 게시물을 확인하면서 현재 위치를 잃지 않도록 이상적입니다.
// @icon              https://x.com/favicon.ico
// @namespace         http://tampermonkey.net/
// @version           2025-02-26.2
// @author            Copiis
// @license           MIT
// @match             https://x.com/home
// @grant             GM_setValue
// @grant             GM_getValue
// @grant             GM_download
// ==/UserScript==

(function() {
    'use strict';

    let lastReadPost = null;
    let isAutoScrolling = false;
    let isSearching = false;
    let isTabFocused = true;
    let downloadTriggered = false;
    let isPostLoading = false;
    let hasScrolledAfterLoad = false;
    let saveToDownloadFolder = GM_getValue("saveToDownloadFolder", true);

    const translations = {
        en: {
            scriptDisabled: "🚫 Script disabled: Not on the home page.",
            pageLoaded: "🚀 Page fully loaded. Initializing script...",
            tabBlur: "🌐 Tab lost focus.",
            downloadStart: "📥 Starting download of last read position...",
            alreadyDownloaded: "🗂️ Position already downloaded.",
            tabFocused: "🟢 Tab refocused.",
            saveSuccess: "✅ Last read position saved:",
            saveFail: "⚠️ No valid position to save.",
            noPostFound: "❌ No top visible post found.",
            highlightSuccess: "✅ Post highlighted successfully.",
            searchStart: "🔍 Refined search started...",
            searchCancel: "⏹️ Search manually canceled.",
            contentLoadWait: "⌛ Waiting for content to load...",
            toggleSaveOn: "💾 Save to download folder enabled",
            toggleSaveOff: "🚫 Save to download folder disabled"
        },
        de: {
            scriptDisabled: "🚫 Skript deaktiviert: Nicht auf der Home-Seite.",
            pageLoaded: "🚀 Seite vollständig geladen. Initialisiere Skript...",
            tabBlur: "🌐 Tab hat den Fokus verloren.",
            downloadStart: "📥 Starte Download der letzten Leseposition...",
            alreadyDownloaded: "🗂️ Leseposition bereits im Download-Ordner vorhanden.",
            tabFocused: "🟢 Tab wieder fokussiert.",
            saveSuccess: "✅ Leseposition erfolgreich gespeichert:",
            saveFail: "⚠️ Keine gültige Leseposition zum Speichern.",
            noPostFound: "❌ Kein oberster sichtbarer Beitrag gefunden.",
            highlightSuccess: "✅ Beitrag erfolgreich hervorgehoben.",
            searchStart: "🔍 Verfeinerte Suche gestartet...",
            searchCancel: "⏹️ Suche manuell abgebrochen.",
            contentLoadWait: "⌛ Warte darauf, dass der Inhalt geladen wird...",
            toggleSaveOn: "💾 Speichern im Download-Ordner aktiviert",
            toggleSaveOff: "🚫 Speichern im Download-Ordner deaktiviert"
        }
    };

    const userLang = navigator.language.split('-')[0];
    const t = (key) => translations[userLang]?.[key] || translations.en[key];

    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    const observer = new IntersectionObserver(
        entries => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    const postData = {
                        timestamp: getPostTimestamp(entry.target),
                        authorHandler: getPostAuthorHandler(entry.target)
                    };
                }
            });
        },
        { threshold: 0.1 }
    );

    function observeVisiblePosts() {
        const articles = document.querySelectorAll('article');
        const viewportHeight = window.innerHeight;
        const buffer = viewportHeight * 2;

        for (let article of articles) {
            const rect = article.getBoundingClientRect();
            if (rect.top < buffer && rect.bottom > -buffer) {
                observer.observe(article);
            } else {
                observer.unobserve(article);
            }
        }
    }

    function loadNewestLastReadPost() {
        const data = GM_getValue("lastReadPost", null);
        if (data) {
            lastReadPost = JSON.parse(data);
            console.log(t("saveSuccess"), lastReadPost);
        } else {
            console.warn(t("saveFail"));
        }
    }

    function loadLastReadPostFromFile() {
        loadNewestLastReadPost();
    }

    function saveLastReadPostToFile() {
        if (lastReadPost && lastReadPost.timestamp && lastReadPost.authorHandler) {
            GM_setValue("lastReadPost", JSON.stringify(lastReadPost));
            console.log(t("saveSuccess"), lastReadPost);
        } else {
            console.warn(t("saveFail"));
        }
    }

    function downloadLastReadPost() {
        if (!saveToDownloadFolder) {
            console.log("Saving to download folder is disabled.");
            return;
        }
        if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler) {
            console.warn(t("saveFail"));
            return;
        }
        try {
            const data = JSON.stringify(lastReadPost, null, 2);
            const sanitizedHandler = lastReadPost.authorHandler.replace(/[^a-zA-Z0-9-_]/g, "");
            const timestamp = new Date(lastReadPost.timestamp).toISOString().replace(/[:.-]/g, "_");
            const fileName = `${sanitizedHandler}_${timestamp}.json`;

            GM_download({
                url: `data:application/json;charset=utf-8,${encodeURIComponent(data)}`,
                name: fileName,
                onload: () => console.log(`${t("saveSuccess")} ${fileName}`),
                onerror: (err) => console.error("❌ Error downloading:", err),
            });
        } catch (error) {
            console.error("❌ Download error:", error);
        }
    }

    function markTopVisiblePost(save = true) {
        const topPost = getTopVisiblePost();
        if (!topPost) {
            console.log(t("noPostFound"));
            return;
        }

        const postTimestamp = getPostTimestamp(topPost);
        const authorHandler = getPostAuthorHandler(topPost);

        if (postTimestamp && authorHandler) {
            if (save && (!lastReadPost || new Date(postTimestamp) > new Date(lastReadPost.timestamp))) {
                lastReadPost = { timestamp: postTimestamp, authorHandler };
                saveLastReadPostToFile();
            }
        }
    }

    function getTopVisiblePost() {
        return Array.from(document.querySelectorAll("article")).find(post => {
            const rect = post.getBoundingClientRect();
            return rect.top >= 0 && rect.bottom > 0;
        });
    }

    function getPostTimestamp(post) {
        const timeElement = post.querySelector("time");
        return timeElement ? timeElement.getAttribute("datetime") : null;
    }

    function getPostAuthorHandler(post) {
        const handlerElement = post.querySelector('[role="link"][href*="/"]');
        return handlerElement ? handlerElement.getAttribute("href").slice(1) : null;
    }

    function startRefinedSearchForLastReadPost() {
        if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler || isPostLoading) return;

        console.log(t("searchStart"));
        const popup = createSearchPopup();
        let direction = 1;
        let scrollAmount = 200;
        let scrollSpeed = 1000;
        let scrollInterval = 200;
        let lastComparison = null;
        let initialAdjusted = false;
        let lastScrollDirection = null;
        let lastScrollY = 0;
        let jumpMultiplier = 1;

        function handleSpaceKey(event) {
            if (event.code === "Space") {
                console.log(t("searchCancel"));
                isSearching = false;
                popup.remove();
                window.removeEventListener("keydown", handleSpaceKey);
            }
        }

        window.addEventListener("keydown", handleSpaceKey);

        function adjustScrollParameters(lastReadTime) {
            const visiblePosts = getVisiblePosts();
            if (visiblePosts.length === 0) return { amount: 200, speed: 1000 };

            const nearestPost = visiblePosts.reduce((closest, post) => {
                const postTime = new Date(post.timestamp);
                const diffCurrent = Math.abs(postTime - lastReadTime);
                const diffClosest = Math.abs(new Date(closest.timestamp) - lastReadTime);
                return diffCurrent < diffClosest ? post : closest;
            });

            const nearestTime = new Date(nearestPost.timestamp);
            const timeDifference = Math.abs(lastReadTime - nearestTime) / (1000 * 60);

            let newScrollAmount = 200;
            let newScrollSpeed = 1000;

            if (timeDifference < 5) {
                newScrollAmount = 50;
                newScrollSpeed = 500;
                jumpMultiplier = 1;
            } else if (timeDifference < 30) {
                newScrollAmount = 100;
                newScrollSpeed = 1000;
                jumpMultiplier = 1;
            } else if (timeDifference < 60) {
                newScrollAmount = 200;
                newScrollSpeed = 1500;
                jumpMultiplier = 1.5;
            } else if (timeDifference < 1440) {
                newScrollAmount = 500;
                newScrollSpeed = 2000;
                jumpMultiplier = 2;
            } else {
                newScrollAmount = 1000;
                newScrollSpeed = 3000;
                jumpMultiplier = 3;
            }

            newScrollAmount = Math.max(50, Math.min(newScrollAmount * jumpMultiplier, window.innerHeight * 2));
            return { amount: newScrollAmount, speed: newScrollSpeed };
        }

        async function search() {
            if (!isSearching) {
                popup.remove();
                return;
            }

            const visiblePosts = getVisiblePosts();
            if (visiblePosts.length === 0 && !isPostLoading) {
                setTimeout(search, scrollInterval);
                return;
            }

            if (!initialAdjusted) {
                const comparison = compareVisiblePostsToLastReadPost(visiblePosts);
                adjustInitialScroll(comparison);
                initialAdjusted = true;
            }

            const comparison = compareVisiblePostsToLastReadPost(visiblePosts);
            const lastReadTime = new Date(lastReadPost.timestamp);
            let nearestVisiblePostTime = null;

            if (visiblePosts.length > 0) {
                nearestVisiblePostTime = new Date(visiblePosts[0].timestamp);
            }

            const { amount, speed } = adjustScrollParameters(lastReadTime);
            scrollAmount = amount;
            scrollSpeed = speed;
            scrollInterval = Math.max(30, 1000 / (scrollSpeed / scrollAmount));

            const distanceToBottom = document.documentElement.scrollHeight - (window.scrollY + window.innerHeight);
            if (distanceToBottom < window.innerHeight) {
                scrollAmount = Math.max(50, scrollAmount / 2);
                scrollSpeed = Math.max(500, scrollSpeed / 2);
                scrollInterval = Math.max(30, 1000 / (scrollSpeed / scrollAmount));
            }

            if (comparison === "match") {
                const matchedPost = findPostByData(lastReadPost);
                if (matchedPost) {
                    scrollToPostWithHighlight(matchedPost);
                    isSearching = false;
                    popup.remove();
                    window.removeEventListener("keydown", handleSpaceKey);
                    setTimeout(() => {
                        isAutoScrolling = false;
                    }, 1000);
                    return;
                }
            } else if (comparison === "older") {
                direction = -1;
                if (lastComparison === "newer") jumpMultiplier *= 0.5;
            } else if (comparison === "newer") {
                direction = 1;
                if (lastComparison === "older") jumpMultiplier *= 0.5;
            } else if (comparison === "mixed") {
                scrollAmount = Math.max(50, scrollAmount / 2);
                scrollSpeed = Math.max(500, scrollSpeed / 2);
                scrollInterval = Math.max(30, 1000 / (scrollSpeed / scrollAmount));
            }

            if (window.scrollY === 0 && direction === -1) {
                direction = 1;
                jumpMultiplier *= 2;
            } else if (distanceToBottom < 50 && direction === 1) {
                direction = -1;
                jumpMultiplier *= 2;
            }

            lastComparison = comparison;
            lastScrollDirection = direction;
            lastScrollY = window.scrollY;

            console.log(`Scroll-Richtung: ${direction}, Betrag: ${scrollAmount}px, Geschwindigkeit: ${scrollSpeed}px/s, Intervall: ${scrollInterval}ms, Position: ${window.scrollY}, Zeitdifferenz: ${nearestVisiblePostTime ? Math.abs(lastReadTime - nearestVisiblePostTime) : 'N/A'}`);

            requestAnimationFrame(() => {
                window.scrollBy(0, direction * scrollAmount);
                setTimeout(search, scrollInterval);
            });
        }

        isSearching = true;
        search();
    }

    function adjustInitialScroll(comparison) {
        const initialScrollAmount = 2000;
        if (comparison === "older") {
            for (let i = 0; i < 3; i++) {
                window.scrollBy(0, -initialScrollAmount / 3);
            }
        } else if (comparison === "newer") {
            for (let i = 0; i < 3; i++) {
                window.scrollBy(0, initialScrollAmount / 3);
            }
        }
    }

    function createSearchPopup() {
        const popup = document.createElement("div");
        popup.style.cssText = `position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.9); color: #fff; padding: 10px 20px; border-radius: 8px; font-size: 14px; box-shadow: 0 0 10px rgba(255, 255, 255, 0.8); z-index: 10000;`;
        popup.textContent = "🔍 Refined search in progress... Press SPACE to cancel.";
        document.body.appendChild(popup);
        return popup;
    }

    function compareVisiblePostsToLastReadPost(posts) {
        const validPosts = posts.filter(post => post.timestamp && post.authorHandler);
        if (validPosts.length === 0) return null;

        const lastReadTime = new Date(lastReadPost.timestamp);

        if (validPosts.some(post => post.timestamp === lastReadPost.timestamp && post.authorHandler === lastReadPost.authorHandler)) {
            return "match";
        } else if (validPosts.every(post => new Date(post.timestamp) < lastReadTime)) {
            return "older";
        } else if (validPosts.every(post => new Date(post.timestamp) > lastReadTime)) {
            return "newer";
        } else {
            return "mixed";
        }
    }

    function scrollToPostWithHighlight(post) {
        if (!post) return;
        isAutoScrolling = true;

        post.style.cssText = `outline: none; box-shadow: 0 0 30px 10px rgba(255, 223, 0, 1); background-color: rgba(255, 223, 0, 0.3); border-radius: 12px; transform: scale(1.1); transition: all 0.3s ease;`;

        const postRect = post.getBoundingClientRect();
        const viewportHeight = window.innerHeight;
        const scrollY = window.scrollY;
        const scrollTo = scrollY + postRect.top - viewportHeight / 2 + postRect.height / 2;

        window.scrollTo({
            top: scrollTo,
            behavior: 'smooth'
        });

        setTimeout(() => {
            let scrollHandler = function() {
                post.style.cssText = "";
                window.removeEventListener('scroll', scrollHandler);
                console.log(t("highlightSuccess"));
            };
            window.addEventListener('scroll', scrollHandler);
        }, 500);
    }

    function getVisiblePosts() {
        return Array.from(document.querySelectorAll("article")).map(post => ({
            element: post,
            timestamp: getPostTimestamp(post),
            authorHandler: getPostAuthorHandler(post)
        })).filter(post => post.timestamp && post.authorHandler);
    }

    function findPostByData(data) {
        return Array.from(document.querySelectorAll("article")).find(post => {
            const postTimestamp = getPostTimestamp(post);
            const authorHandler = getPostAuthorHandler(post);
            return postTimestamp === data.timestamp && authorHandler === data.authorHandler;
        });
    }

    function createButtons() {
        const container = document.createElement("div");
        container.style.cssText = `position: fixed; top: 50%; left: 3px; transform: translateY(-50%); display: flex; flex-direction: column; gap: 3px; z-index: 10000;`;

        let toggleButton;

        const buttons = [
            { icon: "📂", title: "Load saved reading position", onClick: importLastReadPost },
            { icon: "🔍", title: "Start manual search", onClick: startRefinedSearchForLastReadPost },
            {
                icon: saveToDownloadFolder ? "💾" : "🚫",
                title: "Toggle save to download folder",
                onClick: function() {
                    saveToDownloadFolder = !saveToDownloadFolder;
                    GM_setValue("saveToDownloadFolder", saveToDownloadFolder);
                    toggleButton.style.background = saveToDownloadFolder ? "rgba(0, 255, 0, 0.9)" : "rgba(255, 0, 0, 0.9)";
                    toggleButton.textContent = saveToDownloadFolder ? "💾" : "🚫";
                    console.log(saveToDownloadFolder ? t("toggleSaveOn") : t("toggleSaveOff"));
                }
            }
        ];

        buttons.forEach(({ icon, title, onClick }) => {
            const button = document.createElement("div");
            button.style.cssText = `width: 36px; height: 36px; background: ${icon === "💾" || icon === "🚫" ? (saveToDownloadFolder ? "rgba(0, 255, 0, 0.9)" : "rgba(255, 0, 0, 0.9)") : "rgba(0, 0, 0, 0.9)"}; color: #fff; border-radius: 50%; display: flex; justify-content: center; align-items: center; cursor: pointer; font-size: 18px; box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.5); transition: all 0.2s ease;`;
            button.title = title;
            button.textContent = icon;

            button.addEventListener("click", function() {
                button.style.boxShadow = "inset 0 0 20px rgba(255, 255, 255, 0.8)";
                button.style.transform = "scale(0.9)";
                setTimeout(() => {
                    button.style.boxShadow = "inset 0 0 10px rgba(255, 255, 255, 0.5)";
                    button.style.transform = "scale(1)";
                    onClick();
                }, 300);
            });

            ["mouseenter", "mouseleave"].forEach(event =>
                button.addEventListener(event, () => button.style.transform = event === "mouseenter" ? "scale(1.1)" : "scale(1)")
            );

            if (icon === "💾" || icon === "🚫") {
                toggleButton = button;
            }

            container.appendChild(button);
        });

        document.body.appendChild(container);
    }

    function importLastReadPost() {
        const input = document.createElement("input");
        input.type = "file";
        input.accept = "application/json";
        input.style.display = "none";

        input.addEventListener("change", (event) => {
            const file = event.target.files[0];
            if (file) {
                const reader = new FileReader();
                reader.onload = () => {
                    try {
                        const importedData = JSON.parse(reader.result);
                        if (importedData.timestamp && importedData.authorHandler) {
                            lastReadPost = importedData;
                            saveLastReadPostToFile();
                            startRefinedSearchForLastReadPost();
                        } else {
                            throw new Error("Invalid reading position");
                        }
                    } catch (error) {
                        console.error("❌ Error importing reading position:", error);
                    }
                };
                reader.readAsText(file);
            }
        });

        document.body.appendChild(input);
        input.click();
        document.body.removeChild(input);
    }

    function observeForNewPosts() {
        const targetNode = document.querySelector('div[aria-label="Timeline: Your Home Timeline"]') || document.body;

        const mutationObserver = new MutationObserver(mutations => {
            for (const mutation of mutations) {
                if (mutation.type === 'childList') {
                    checkForNewPosts();
                }
            }
        });

        mutationObserver.observe(targetNode, { childList: true, subtree: true });
    }

    function getNewPostsIndicator() {
        const indicator = document.querySelector('div[aria-label*="undefined"]') ||
                         document.querySelector('div[aria-label*="new"]');
        console.log(`[Debug] Neuer Beitragsindikator gefunden: ${indicator ? 'Ja' : 'Nein'}`);
        return indicator;
    }

    function clickNewPostsIndicator(indicator, preservedScrollY) {
        if (indicator && indicator.offsetParent !== null) {
            console.log("Versuche, auf den neuen Beitrag-Indikator zu klicken.");
            indicator.click();
            console.log("Klick auf den neuen Beitrag-Indikator war erfolgreich.");

            const timelineNode = document.querySelector('div[aria-label="Timeline: Your Home Timeline"]') || document.body;
            let mutationTimeout;

            const observer = new MutationObserver(() => {
                if (window.scrollY !== preservedScrollY) {
                    window.scrollTo(0, preservedScrollY);
                    console.log(`[Debug] Scroll-Position korrigiert auf: ${preservedScrollY}`);
                }

                clearTimeout(mutationTimeout);
                mutationTimeout = setTimeout(() => {
                    observer.disconnect();
                    console.log(t("newPostsLoaded"));
                }, 3000);
            });

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

            setTimeout(() => {
                window.scrollTo(0, preservedScrollY);
            }, 100);
        } else {
            console.log("Kein klickbarer Indikator gefunden.");
        }
    }

    async function checkForNewPosts() {
        if (window.scrollY <= 10) {
            const newPostsIndicator = getNewPostsIndicator();
            if (newPostsIndicator) {
                const preservedScrollY = window.scrollY;
                console.log("🆕 Neue Beiträge in der Nähe des oberen Randes erkannt. Klicke auf Indikator...");
                clickNewPostsIndicator(newPostsIndicator, preservedScrollY);
                hasScrolledAfterLoad = false;

                try {
                    console.log(t("contentLoadWait"));
                    await waitForTimelineLoad(5000);
                    console.log("Inhalt geladen, starte verfeinerte Suche...");
                    startRefinedSearchForLastReadPost();
                } catch (error) {
                    console.error("❌ Fehler beim Warten auf das Laden der Timeline:", error.message);
                    console.log("[Debug] Starte Suche trotz Fehler.");
                    startRefinedSearchForLastReadPost();
                }
            } else {
                console.log("[Debug] Kein neuer Beitragsindikator gefunden.");
            }
        } else {
            console.log("[Debug] Scroll-Position nicht oben, keine neuen Beiträge geprüft.");
        }
    }

    async function waitForTimelineLoad(maxWaitTime = 3000) {
        const startTime = Date.now();
        return new Promise((resolve) => {
            const checkInterval = setInterval(() => {
                const loadingSpinner = document.querySelector('div[role="progressbar"]') ||
                                       document.querySelector('div.css-175oi2r.r-1pi2tsx.r-1wtj0ep.r-ymttw5.r-1f1sjgu');
                const timeElapsed = Date.now() - startTime;

                if (!loadingSpinner) {
                    console.log("[Debug] Ladeindikator verschwunden, resolve.");
                    clearInterval(checkInterval);
                    resolve(true);
                } else if (timeElapsed > maxWaitTime) {
                    console.log("[Debug] Timeout erreicht, starte Suche trotz Ladeindikator.");
                    clearInterval(checkInterval);
                    resolve(true);
                }
            }, 200);
        });
    }

    async function initializeScript() {
        console.log(t("pageLoaded"));
        try {
            await loadLastReadPostFromFile();
            observeForNewPosts();
            observeVisiblePosts();

            window.addEventListener("scroll", debounce(() => {
                observeVisiblePosts();
                if (!isAutoScrolling && !isSearching) {
                    if (hasScrolledAfterLoad) {
                        markTopVisiblePost(true);
                    } else {
                        hasScrolledAfterLoad = true;
                    }
                }
            }, 500));
        } catch (error) {
            console.error("❌ Fehler bei der Initialisierung des Skripts:", error);
        }
    }

    window.onload = async () => {
        if (!window.location.href.includes("/home")) {
            console.log(t("scriptDisabled"));
            return;
        }
        console.log(t("pageLoaded"));
        try {
            await loadNewestLastReadPost();
            await initializeScript();
            createButtons();
        } catch (error) {
            console.error("❌ Fehler beim Seitenladen:", error);
        }
    };

    window.addEventListener("blur", async () => {
        console.log(t("tabBlur"));
        if (lastReadPost && !downloadTriggered) {
            downloadTriggered = true;
            if (!(await isFileAlreadyDownloaded())) {
                console.log(t("downloadStart"));
                await downloadLastReadPost();
                await markDownloadAsComplete();
            } else {
                console.log(t("alreadyDownloaded"));
            }
            downloadTriggered = false;
        }
    });

    window.addEventListener("focus", () => {
        isTabFocused = true;
        downloadTriggered = false;
        console.log(t("tabFocused"));
    });

    async function isFileAlreadyDownloaded() {
        const localFiles = await GM_getValue("downloadedPosts", []);
        const fileSignature = `${lastReadPost.authorHandler}_${lastReadPost.timestamp}`;
        return localFiles.includes(fileSignature);
    }

    async function markDownloadAsComplete() {
        const localFiles = await GM_getValue("downloadedPosts", []);
        const fileSignature = `${lastReadPost.authorHandler}_${lastReadPost.timestamp}`;
        if (!localFiles.includes(fileSignature)) {
            localFiles.push(fileSignature);
            GM_setValue("downloadedPosts", localFiles);
        }
    }
})();