X Timeline Sync

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

当前为 2024-12-08 提交的版本,查看 最新版本

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

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

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

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

您需要先安装一款用户脚本管理器扩展,例如 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           2024.12.9
// @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 () => {
    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 buttonsConfig = [
    { icon: "💾", title: "Leseposition exportieren", onClick: exportLastReadPost },
    { icon: "📂", title: "Gespeicherte Leseposition importieren", onClick: importLastReadPost },
    {
        icon: "🔍",
        title: "Suche manuell starten",
        onClick: () => {
            console.log("🔍 Manuelle Suche gestartet.");
            startSearchForLastReadPost();
        },
    },
];

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

    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 <= 50) {
            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 > 50) {
        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(); // Automatische Suche bleibt hier unverändert
    }, 500);
}

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

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

    console.log("🔍 Suche nach zuletzt gelesener Position gestartet.");

    let direction = 1; // 1 für nach unten, -1 für nach oben
    let scrollAmount = 2000; // Initiale Sprunggröße
    let previousScrollY = -1;

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

    window.addEventListener("keydown", handleSpaceKey);

    const search = () => {
        if (!isSearching) {
            popup.remove();
            return;
        }

        // Finde Beitrag, falls vorhanden
        const matchedPost = findPostByData(lastReadPost);
        if (matchedPost) {
            isSearching = false;
            scrollToPost(matchedPost);
            console.log(`🎯 Beitrag gefunden: ${lastReadPost.timestamp}, @${lastReadPost.authorHandler}`);
            popup.remove();
            return;
        }

        // Prüfe, ob wir feststecken (kein Scroll-Fortschritt mehr)
        if (window.scrollY === previousScrollY) {
            direction = -direction; // Richtung umkehren
            scrollAmount = Math.max(scrollAmount / 2, 500); // Schrittweite halbieren
        }
        previousScrollY = window.scrollY;

        // Scrollen
        window.scrollBy(0, direction * scrollAmount);

        // Nächster Animationsrahmen
        requestAnimationFrame(search);
    };

    // Start der Suche
    requestAnimationFrame(search);
}

    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);
    }
})();