X Timeline Sync

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

目前为 2024-12-07 提交的版本。查看 最新版本

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

(function () {
    let lastReadPost = null; // Letzte Leseposition
    let isAutoScrolling = false;
    let isSearching = false;

    window.onload = async () => {
        // URL-Bedingung hinzufügen
        if (!window.location.href.includes("/home")) {
            console.log("🚫 Skript deaktiviert: Nicht auf der Home-Seite.");
            return;
        }
        console.log("🚀 Seite vollständig geladen. Initialisiere Skript...");
        await initializeScript();
        createButtons();
    };

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

    observeForNewPosts(); // Beobachtung für neue Beiträge aktivieren

    // Scroll-Listener für manuelles Scrollen hinzufügen
window.addEventListener("scroll", () => {
    if (isAutoScrolling || isSearching) {
        console.log("⏹️ Scroll-Ereignis ignoriert (automatischer Modus aktiv).");
        return;
    }
    // Obersten sichtbaren Beitrag markieren
    markTopVisiblePost(true);
});

}

    async function loadLastReadPostFromFile() {
        try {
            const data = GM_getValue("lastReadPost", null);
            if (data) {
                lastReadPost = JSON.parse(data);
                console.log("✅ Leseposition erfolgreich geladen:", lastReadPost);
            } else {
                console.warn("⚠️ Keine gespeicherte Leseposition gefunden.");
                lastReadPost = null;
            }
        } catch (err) {
            console.error("⚠️ Fehler beim Laden der Leseposition:", err);
            lastReadPost = null;
        }
    }

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

            GM_setValue("lastReadPost", JSON.stringify(lastReadPost));
            console.log("💾 Leseposition erfolgreich gespeichert:", lastReadPost);
        } catch (err) {
            console.error("❌ Fehler beim Speichern der Leseposition:", err);
        }
    }

    async function exportLastReadPost() {
    if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler) {
        console.warn("⚠️ Keine gültige Leseposition zum Exportieren.");
        showPopup("⚠️ Keine gültige Leseposition verfügbar.");
        return;
    }

    try {
        const data = JSON.stringify(lastReadPost, null, 2);
        const sanitizedHandler = lastReadPost.authorHandler.replace(/[^a-zA-Z0-9-_]/g, ""); // Sonderzeichen entfernen
        const timestamp = new Date(lastReadPost.timestamp).toISOString().replace(/[:.-]/g, "_");
        const fileName = `${sanitizedHandler}_${timestamp}.json`;

        const blob = new Blob([data], { type: "application/json" });

        const a = document.createElement("a");
        a.href = URL.createObjectURL(blob);
        a.download = fileName;
        a.style.display = "none";
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);

        showPopup(`✅ Datei "${fileName}" wurde erfolgreich generiert und heruntergeladen.`);
    } catch (error) {
        console.error("❌ Fehler beim Exportieren der Leseposition:", error);
        showPopup("❌ Fehler: Leseposition konnte nicht exportiert werden.");
    }
}

function createButtons() {
    const buttonContainer = document.createElement("div");
    buttonContainer.style.position = "fixed";
    buttonContainer.style.top = "10px";
    buttonContainer.style.right = "10px"; // Positionierung auf der rechten Seite
    buttonContainer.style.display = "flex";
    buttonContainer.style.flexDirection = "column"; // Anordnung untereinander
    buttonContainer.style.gap = "10px"; // Abstand zwischen den Buttons
    buttonContainer.style.zIndex = "10000";

    const saveButton = createButton("💾", "Leseposition exportieren", exportLastReadPost);
    const importButton = createButton("📂", "Gespeicherte Leseposition importieren", importLastReadPost);
    const searchButton = createButton("🔍", "Suche manuell starten", startSearchForLastReadPost);

    buttonContainer.appendChild(saveButton);
    buttonContainer.appendChild(importButton);
    buttonContainer.appendChild(searchButton);

    document.body.appendChild(buttonContainer);
}

    function createButton(icon, title, onClick) {
        const button = document.createElement("div");
        button.style.width = "30px";
        button.style.height = "30px";
        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 = "16px";
        button.style.boxShadow = "0 0 8px rgba(255, 255, 255, 0.8)";
        button.textContent = icon;
        button.title = title;

        button.addEventListener("click", onClick);
        return button;
    }

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

        input.addEventListener("change", async (event) => {
            const file = event.target.files[0];
            if (file) {
                const reader = new FileReader();
                reader.onload = async () => {
                    try {
                        const importedData = JSON.parse(reader.result);
                        if (importedData.timestamp && importedData.authorHandler) {
                            lastReadPost = importedData;
                            await saveLastReadPostToFile();
                            showPopup("✅ Leseposition erfolgreich importiert.");
                            console.log("✅ Importierte Leseposition:", lastReadPost);
                            await startSearchForLastReadPost();
                        } else {
                            throw new Error("Ungültige Leseposition");
                        }
                    } catch (error) {
                        console.error("❌ Fehler beim Importieren der Leseposition:", error);
                        showPopup("❌ Fehler: Ungültige Leseposition.");
                    }
                };
                reader.readAsText(file);
            }
        });

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

function observeForNewPosts() {
    const observer = new MutationObserver(() => {
        // Nur neue Beiträge beobachten, wenn der Nutzer nahe genug am oberen Rand ist
        if (window.scrollY <= 5) {
            const newPostsIndicator = getNewPostsIndicator();

            if (newPostsIndicator) {
                console.log("🆕 Neue Beiträge erkannt. Automatische Suche wird gestartet...");
                clickNewPostsIndicator(newPostsIndicator);
            }
        }
    });

    observer.observe(document.body, {
    childList: true,
    subtree: true,
    attributes: false, // Verhindere unnötige Trigger durch Attributänderungen
});

}

    function getNewPostsIndicator() {
        return document.querySelector('div[aria-label*="ungelesene Elemente"]');
    }

function clickNewPostsIndicator(indicator) {
    if (window.scrollY > 3) {
        console.log("❌ Button für neue Beiträge wurde nicht geklickt, da der Nutzer nicht am oberen Rand ist.");
        return;
    }

    if (!indicator) {
        console.warn("⚠️ Kein Indikator für neue Beiträge gefunden.");
        return;
    }

    console.log("✅ Indikator für neue Beiträge wird geklickt...");
    indicator.scrollIntoView({ behavior: "smooth", block: "center" });
    setTimeout(() => {
        indicator.click();
        console.log("✅ Neue Beiträge erfolgreich geladen.");
        startSearchForLastReadPost();
    }, 500);
}

function startSearchForLastReadPost() {
    if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler) {
        console.log("❌ Keine gültige Leseposition verfügbar. Suche übersprungen.");
        return;
    }

    isSearching = true;
    isAutoScrolling = true;

    const popup = document.createElement("div");
    popup.id = "search-popup";
    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 = "🔍 Suche läuft... Drücke SPACE, um abzubrechen.";
    document.body.appendChild(popup);

    console.log("🔍 Suche gestartet.");

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

    window.addEventListener("keydown", handleSpaceKey);

    const searchInterval = setInterval(() => {
        const matchedPost = findPostByData(lastReadPost);

        if (matchedPost) {
            clearInterval(searchInterval);
            isSearching = false;
            isAutoScrolling = false;
            scrollToPost(matchedPost);
            console.log(`🎯 Zuletzt gelesenen Beitrag gefunden: ${lastReadPost.timestamp}, @${lastReadPost.authorHandler}`);
            popup.remove();
            window.removeEventListener("keydown", handleSpaceKey);
        } else {
            const visiblePosts = Array.from(document.querySelectorAll("article"));
            const allOlder = visiblePosts.every(post => {
                const postTimestamp = getPostTimestamp(post);
                return new Date(postTimestamp) < new Date(lastReadPost.timestamp);
            });

            if (allOlder) {
                console.log("🔄 Alle sichtbaren Beiträge sind älter. Suche nach oben.");
                window.scrollBy({ top: -500, behavior: "smooth" });
            } 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 getTopVisiblePost() {
    const posts = Array.from(document.querySelectorAll("article")); // Alle Beiträge sammeln
    return posts.find(post => {
        const rect = post.getBoundingClientRect();
        return rect.top >= 0 && rect.bottom > 0; // Oberster sichtbarer Beitrag
    });
}

function markTopVisiblePost(save = true) {
    if (isAutoScrolling || isSearching) {
        console.log("⏹️ Automatische Aktionen aktiv, Markierung übersprungen.");
        return;
    }

    const topPost = getTopVisiblePost();
    if (!topPost) {
        console.log("❌ Kein oberster sichtbarer Beitrag gefunden.");
        return;
    }

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

    if (!postTimestamp || !authorHandler) {
        console.log("❌ Oberster sichtbarer Beitrag hat keine gültigen Daten.");
        return;
    }

    // Leseposition nur speichern, wenn sie neuer ist als die aktuelle
    if (!lastReadPost || new Date(postTimestamp) > new Date(lastReadPost.timestamp)) {
        lastReadPost = { timestamp: postTimestamp, authorHandler };
        console.log(`💾 Lesestelle aktualisiert: ${postTimestamp}, @${authorHandler}`);
        if (save) saveLastReadPostToFile();
    } else {
        console.log("⏹️ Lesestelle nicht aktualisiert, da keine neueren Beiträge gefunden wurden.");
    }
}

    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;
            console.log("✅ Beitrag wurde erfolgreich zentriert!");
        }, 1000);
    }

    function showPopup(message) {
        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.textContent = message;

        document.body.appendChild(popup);

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址