// ==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 // @downloadURL none // ==/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); } })();