// ==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.2 // @author Copiis // @license MIT // @match https://x.com/home // @grant GM_setValue // @grant GM_getValue // @downloadURL none // ==/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..."); 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 (folderHandle && lastReadPost?.timestamp && lastReadPost?.authorHandler) { console.log(`📍 Geladene Leseposition: ${lastReadPost.timestamp}, @${lastReadPost.authorHandler}`); startSearchForLastReadPost(); } else { console.log("❌ Keine gültige Leseposition oder Ordnerzugriff. Suche übersprungen."); } console.log("🔍 Starte Beobachtung für neue Beiträge..."); observeForNewPosts(); window.addEventListener("scroll", () => { if (!isAutoScrolling && !isSearching) { markCentralVisiblePost(true); } }); } 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); 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); 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) { 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 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 observeForNewPosts() { const observer = new MutationObserver(() => { const newPostsButton = getNewPostsButton(); if (newPostsButton) { console.log("🆕 Neue Beiträge gefunden. Klicke auf den Button."); clickNewPostsButton(newPostsButton); } }); observer.observe(document.body, { childList: true, subtree: true }); } function getNewPostsButton() { return Array.from(document.querySelectorAll("div.css-146c3p1")) .find(div => div.textContent && /Post anzeigen|Posts 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 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); } })();