Twitter/X Timeline Sync

跟踪并同步您在 Twitter/X 上的最后阅读位置,提供手动和自动选项。完美解决在查看新帖子时不丢失当前位置的问题。

当前为 2025-05-29 提交的版本,查看 最新版本

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

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

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

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name              Twitter/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-05-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!
//                    Bitcoin (BTC): bc1quc5mkudlwwkktzhvzw5u2nruxyepef957p68r7
//                    PayPal: https://www.paypal.com/paypalme/Coopiis?country.x=DE&locale.x=de_DE

(function () {
    let lastReadPost = null;
    let isAutoScrolling = false;
    let isSearching = false;
    let isTabFocused = true;
    let isScriptActivated = false;

    // Speichert die Lesestelle mit GM_setValue
    function saveLastReadPost(data) {
        try {
            const bookmarkData = JSON.stringify(data);
            GM_setValue("lastReadPost", bookmarkData);
            localStorage.setItem("lastReadPost", bookmarkData); // Fallback
            console.log("💾 Leseposition gespeichert:", bookmarkData);
        } catch (err) {
            console.error("❌ Fehler beim Speichern der Leseposition:", err);
            localStorage.setItem("lastReadPost", JSON.stringify(data));
            promptManualFallback(data);
        }
    }

    // Lädt die Lesestelle mit GM_getValue
    function loadLastReadPost(callback) {
        try {
            const storedData = GM_getValue("lastReadPost", null);
            if (storedData) {
                const data = JSON.parse(storedData);
                console.log("✅ Leseposition geladen:", data);
                callback(data);
            } else {
                console.warn("⚠️ Keine gespeicherte Leseposition gefunden.");
                callback(null);
            }
        } catch (err) {
            console.error("❌ Fehler beim Laden der Leseposition:", err);
            const storedPost = JSON.parse(localStorage.getItem("lastReadPost") || "{}");
            callback(storedPost);
        }
    }

    // Fallback: Manuelle Benachrichtigung
    function promptManualFallback(data) {
        const content = JSON.stringify(data);
        const message = `📝 Neue Leseposition: ${content}\nBitte speichere dies manuell, da der Speichervorgang fehlschlug.`;
        showPopup(message, 10000);
        console.log("📝 Bitte manuell speichern:", content);
    }

    // Initialisierung
    function initializeWhenDOMReady() {
        if (!window.location.href.includes("/home")) {
            console.log("🚫 Skript deaktiviert: Nicht auf der Home-Seite.");
            return;
        }
        console.log("🚀 Initialisiere Skript...");

        const observer = new MutationObserver((mutations, obs) => {
            if (document.body) {
                obs.disconnect();
                initializeScript().then(() => {
                    createButtons();
                    startPeriodicSave();
                }).catch(err => {
                    console.error("❌ Fehler bei der Initialisierung:", err);
                    showPopup("❌ Fehler beim Laden des Skripts.");
                });
            }
        });
        observer.observe(document.documentElement, { childList: true, subtree: true });
    }

    window.addEventListener("load", initializeWhenDOMReady);

    window.addEventListener("blur", () => {
        isTabFocused = false;
        saveLastReadPostToFile();
    });

    document.addEventListener("visibilitychange", () => {
        if (document.visibilityState === "hidden") {
            isTabFocused = false;
            saveLastReadPostToFile();
        } else {
            isTabFocused = true;
            console.log("🟢 Tab wieder sichtbar.");
        }
    });

    window.addEventListener("focus", () => {
        isTabFocused = true;
        console.log("🟢 Tab wieder fokussiert.");
    });

    function startPeriodicSave() {
        setInterval(() => {
            if (isTabFocused && lastReadPost && isScriptActivated) {
                saveLastReadPostToFile();
            }
        }, 30000);
    }

    function saveLastReadPostToFile() {
        try {
            if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler) {
                console.warn("⚠️ Keine gültige Leseposition.");
                return;
            }

            loadLastReadPost(existingPost => {
                if (
                    existingPost &&
                    existingPost.timestamp === lastReadPost.timestamp &&
                    existingPost.authorHandler === lastReadPost.authorHandler
                ) {
                    console.log("⏹️ Lesestelle unverändert.");
                    return;
                }

                saveLastReadPost(lastReadPost);
            });
        } catch (err) {
            console.error("❌ Fehler beim Speichern:", err);
            localStorage.setItem("lastReadPost", JSON.stringify(lastReadPost));
            promptManualFallback(lastReadPost);
        }
    }

    function loadNewestLastReadPost() {
        return new Promise(resolve => {
            loadLastReadPost(storedPost => {
                if (storedPost && storedPost.timestamp && storedPost.authorHandler) {
                    lastReadPost = storedPost;
                    console.log("✅ Leseposition geladen:", lastReadPost);
                } else {
                    const localPost = JSON.parse(localStorage.getItem("lastReadPost") || "{}");
                    if (localPost && localPost.timestamp && localPost.authorHandler) {
                        lastReadPost = localPost;
                        console.log("✅ Leseposition aus localStorage:", lastReadPost);
                    } else {
                        console.warn("⚠️ Keine Leseposition gefunden.");
                        showPopup("ℹ️ Scrolle, um eine Leseposition zu setzen.");
                    }
                }
                resolve();
            });
        });
    }

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

        window.addEventListener("scroll", () => {
            if (!isScriptActivated) {
                isScriptActivated = true;
                console.log("🛠️ DEBUG: Skript durch Scrollen aktiviert.");
                observeForNewPosts();
            }

            if (isAutoScrolling || isSearching) {
                console.log("⏹️ Scroll-Ereignis ignoriert.");
                return;
            }
            markTopVisiblePost(true);
        }, { passive: true });
    }

    function markTopVisiblePost(save = true) {
        const topPost = getTopVisiblePost();
        if (!topPost) {
            console.log("❌ Kein sichtbarer Beitrag.");
            return;
        }

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

        if (postTimestamp && postAuthorHandler) {
            const newPost = { timestamp: postTimestamp, authorHandler: postAuthorHandler };
            if (save && isScriptActivated) {
                loadLastReadPost(existingPost => {
                    if (!existingPost || new Date(postTimestamp) > new Date(existingPost.timestamp)) {
                        lastReadPost = newPost;
                        console.log("💾 Neue Leseposition:", lastReadPost);
                        if (isTabFocused) {
                            saveLastReadPostToFile();
                        }
                    }
                });
            }
        }
    }

    function getTopVisiblePost() {
        const posts = Array.from(document.querySelectorAll("article"));
        return posts.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 (!isScriptActivated) {
            console.log("⏹️ Suche abgebrochen: Skript nicht aktiviert.");
            showPopup("ℹ️ Bitte scrollen oder Lupe klicken.");
            return;
        }

        loadLastReadPost(storedData => {
            if (!storedData) {
                const localData = JSON.parse(localStorage.getItem("lastReadPost") || "{}");
                if (localData && localData.timestamp && localData.authorHandler) {
                    lastReadPost = localData;
                } else {
                    console.log("❌ Keine Leseposition gefunden.");
                    showPopup("❌ Keine Leseposition vorhanden.");
                    return;
                }
            } else {
                lastReadPost = storedData;
            }

            if (!lastReadPost.timestamp || !lastReadPost.authorHandler) {
                console.log("❌ Ungültige Leseposition:", lastReadPost);
                showPopup("❌ Ungültige Leseposition.");
                return;
            }

            console.log("🔍 Starte Suche:", lastReadPost);
            const popup = createSearchPopup();

            let direction = 1;
            let scrollAmount = 2000;
            let previousScrollY = -1;
            let searchAttempts = 0;
            const maxAttempts = 50;

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

            window.addEventListener("keydown", handleSpaceKey);

            const search = () => {
                if (!isSearching || searchAttempts >= maxAttempts) {
                    console.log("⏹️ Suche beendet: Max Versuche oder abgebrochen.");
                    isSearching = false;
                    popup.remove();
                    window.removeEventListener("keydown", handleSpaceKey);
                    return;
                }

                const visiblePosts = getVisiblePosts();
                const comparison = compareVisiblePostsToLastReadPost(visiblePosts);

                if (comparison === "match") {
                    const matchedPost = findPostByData(lastReadPost);
                    if (matchedPost) {
                        console.log("🎯 Beitrag gefunden:", lastReadPost);
                        isAutoScrolling = true;
                        scrollToPostWithHighlight(matchedPost);
                        isSearching = false;
                        popup.remove();
                        window.removeEventListener("keydown", handleSpaceKey);
                        return;
                    }
                } else if (comparison === "older") {
                    direction = -1;
                } else if (comparison === "newer") {
                    direction = 1;
                }

                if (window.scrollY === previousScrollY) {
                    scrollAmount = Math.max(scrollAmount / 2, 500);
                    direction = -direction;
                } else {
                    scrollAmount = Math.min(scrollAmount * 1.5, 3000);
                }

                previousScrollY = window.scrollY;
                searchAttempts++;

                requestAnimationFrame(() => {
                    window.scrollBy({
                        top: direction * scrollAmount,
                        behavior: "smooth"
                    });
                    setTimeout(search, 1000);
                });
            };

            isSearching = true;
            search();
        });
    }

    function createSearchPopup() {
        const popup = document.createElement("div");
        popup.style.position = "fixed";
        popup.style.bottom = "20px";
        popup.style.left = "50%";
        popup.style.transform = "translateX(-50%)";
        popup.style.backgroundColor = "rgba(0, 0, 0, 0.9)";
        popup.style.color = "#ffffff";
        popup.style.padding = "10px 20px";
        popup.style.borderRadius = "8px";
        popup.style.fontSize = "14px";
        popup.style.boxShadow = "0 0 10px rgba(255, 255, 255, 0.8)";
        popup.style.zIndex = "10000";
        popup.textContent = "🔍 Searching... Press SPACE to cancel.";
        document.body.appendChild(popup);
        return popup;
    }

    function compareVisiblePostsToLastReadPost(posts, customPosition = lastReadPost) {
        const validPosts = posts.filter(post => post.timestamp && post.authorHandler);

        if (validPosts.length === 0) {
            console.log("⚠️ Keine sichtbaren Beiträge.");
            return null;
        }

        const lastReadTime = new Date(customPosition.timestamp);

        const allOlder = validPosts.every(post => new Date(post.timestamp) < lastReadTime);
        const allNewer = validPosts.every(post => new Date(post.timestamp) > lastReadTime);

        if (validPosts.some(post => post.timestamp === customPosition.timestamp && post.authorHandler === customPosition.authorHandler)) {
            return "match";
        } else if (allOlder) {
            return "older";
        } else if (allNewer) {
            return "newer";
        } else {
            return "mixed";
        }
    }

    function scrollToPostWithHighlight(post) {
        if (!post) {
            console.log("❌ Kein Beitrag zum Scrollen.");
            return;
        }

        isAutoScrolling = true;

        post.style.outline = "none";
        post.style.boxShadow = "0 0 20px 10px rgba(255, 215, 0, 0.9)";
        post.style.animation = "none";

        const existingStyle = document.querySelector('#glowStyle');
        if (existingStyle) {
            existingStyle.remove();
        }

        post.scrollIntoView({ behavior: "smooth", block: "center" });

        const removeHighlightOnScroll = () => {
            if (!isAutoScrolling) {
                post.style.boxShadow = "none";
                console.log("✅ Highlight entfernt.");
                window.removeEventListener("scroll", removeHighlightOnScroll);
            }
        };

        setTimeout(() => {
            isAutoScrolling = false;
            window.addEventListener("scroll", removeHighlightOnScroll);
            console.log("✅ Beitrag zentriert.");
        }, 1000);
    }

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

    function observeForNewPosts() {
        let isProcessingIndicator = false;

        const observer = new MutationObserver(() => {
            if (!isScriptActivated) {
                console.log("⏹️ Beobachtung abgebrochen: Skript nicht aktiviert.");
                return;
            }

            if (window.scrollY <= 1 && !isSearching && !isProcessingIndicator && lastReadPost) {
                const newPostsIndicator = getNewPostsIndicator();
                if (newPostsIndicator) {
                    console.log("🆕 Neue Beiträge erkannt.");
                    isProcessingIndicator = true;
                    clickNewPostsIndicator(newPostsIndicator);
                    setTimeout(() => {
                        startRefinedSearchForLastReadPost();
                        isProcessingIndicator = false;
                    }, 2000);
                }
            }
        });

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

    function getNewPostsIndicator() {
        const buttons = document.querySelectorAll('button[role="button"]');
        for (const button of buttons) {
            const span = button.querySelector('span.r-poiln3');
            if (span) {
                const textContent = span.textContent || '';
                const postIndicatorPattern = /^\d+\s*(neue|new)?\s*(Post|Posts|Beitrag|Beiträge|Tweet|Tweets|Publicación|Publications|投稿|게시물|пост|постов|mensagem|mensagens|مشاركة|مشاركات)\b/i;
                if (postIndicatorPattern.test(textContent)) {
                    if (!button.dataset.processed) {
                        console.log(`🆕 Indikator gefunden: "${textContent}"`);
                        button.dataset.processed = 'true';
                        return button;
                    }
                }
            }
        }
        console.log("ℹ️ Kein Beitragsindikator gefunden.");
        return null;
    }

    function clickNewPostsIndicator(indicator) {
        if (!indicator) {
            console.log("⚠️ Kein Indikator gefunden.");
            return;
        }

        console.log("✅ Klicke auf Indikator...");
        try {
            indicator.click();
            console.log("✅ Indikator geklickt.");
        } catch (err) {
            console.error("❌ Fehler beim Klicken:", err);
        }
    }

    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 createButtons() {
        setTimeout(() => {
            try {
                if (!document.body) {
                    console.warn("⚠️ document.body nicht verfügbar.");
                    return;
                }

                const buttonContainer = document.createElement("div");
                let targetDiv = document.querySelector('div.css-175oi2r.r-dnmrzs.r-1559e4e');
                if (!targetDiv) {
                    targetDiv = document.querySelector('div[role="heading"]');
                    console.warn("⚠️ Primäres Ziel-<div> nicht gefunden. Fallback auf div[role='heading'].");
                }

                let leftOffset = 60;
                let topOffset = 15;

                if (targetDiv) {
                    const rect = targetDiv.getBoundingClientRect();
                    leftOffset = rect.right + 10;
                    topOffset = rect.top + (rect.height / 2) - 18;
                    console.log("🛠️ DEBUG: Ziel-<div> gefunden. Position:", { left: leftOffset, top: topOffset });
                } else {
                    console.warn("⚠️ Kein Ziel-<div> gefunden. Fallback-Position.");
                }

                buttonContainer.style.position = "fixed";
                buttonContainer.style.top = `${topOffset}px`;
                buttonContainer.style.left = `${leftOffset}px`;
                buttonContainer.style.zIndex = "10000";
                buttonContainer.style.display = "flex";
                buttonContainer.style.alignItems = "center";
                buttonContainer.style.visibility = "visible";

                const buttonsConfig = [
                    {
                        icon: "🔍",
                        title: "Start manual search",
                        onClick: () => {
                            console.log("🔍 Manuelle Suche gestartet.");
                            if (!isScriptActivated) {
                                isScriptActivated = true;
                                console.log("🛠️ DEBUG: Skript durch Lupen-Klick aktiviert.");
                                observeForNewPosts();
                            }
                            startRefinedSearchForLastReadPost();
                        },
                    },
                ];

                buttonsConfig.forEach(({ icon, title, onClick }) => {
                    const button = createButton(icon, title, onClick);
                    buttonContainer.appendChild(button);
                });

                document.body.appendChild(buttonContainer);
                console.log("🛠️ DEBUG: Button-Container erstellt:", buttonContainer);
            } catch (err) {
                console.error("❌ Fehler beim Erstellen des Buttons:", err);
                showPopup("❌ Fehler beim Anzeigen der Lupe.");
            }
        }, 10000);
    }

    function createButton(icon, title, onClick) {
        const button = document.createElement("div");
        button.style.width = "36px";
        button.style.height = "36px";
        button.style.backgroundColor = "rgba(0, 0, 0, 0.9)";
        button.style.color = "#ffffff";
        button.style.borderRadius = "50%";
        button.style.display = "flex";
        button.style.justifyContent = "center";
        button.style.alignItems = "center";
        button.style.cursor = "pointer";
        button.style.fontSize = "18px";
        button.style.boxShadow = "0 0 10px rgba(255, 255, 255, 0.5)";
        button.style.transition = "transform 0.2s, box-shadow 0.3s";
        button.style.zIndex = "10001";
        button.style.visibility = "visible";
        button.textContent = icon;
        button.title = title;

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

        button.addEventListener("mouseenter", () => {
            button.style.boxShadow = "0 0 15px rgba(255, 255, 255, 0.7)";
            button.style.transform = "scale(1.1)";
        });

        button.addEventListener("mouseleave", () => {
            button.style.boxShadow = "0 0 10px rgba(255, 255, 255, 0.5)";
            button.style.transform = "scale(1)";
        });

        return button;
    }

    function showPopup(message, duration = 3000) {
        const popup = document.createElement("div");
        popup.style.position = "fixed";
        popup.style.bottom = "20px";
        popup.style.right = "20px";
        popup.style.backgroundColor = "rgba(0, 0, 0, 0.9)";
        popup.style.color = "#ffffff";
        popup.style.padding = "10px 20px";
        popup.style.borderRadius = "8px";
        popup.style.fontSize = "14px";
        popup.style.boxShadow = "0 0 10px rgba(255, 255, 255, 0.8)";
        popup.style.zIndex = "10000";
        popup.style.maxWidth = "400px";
        popup.style.whiteSpace = "pre-wrap";
        popup.textContent = message;

        document.body.appendChild(popup);

        setTimeout(() => {
            popup.remove();
        }, duration);
    }
})();