Greasy Fork is available in English.
Отслеживает и синхронизирует вашу последнюю позицию чтения на Twitter/X с ручными и автоматическими опциями. Идеально подходит для просмотра новых постов без потери текущей позиции.
Вам также может понравится YouTube Downloader & Thumbnail Hider & Shorts Remover.
// ==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 2025-03-20 // @author Copiis // @license MIT // @match https://x.com/home // @grant GM_setValue // @grant GM_getValue // @grant GM_download // ==/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 downloadTriggered = false; let lastDownloadedPost = null; 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 loadNewestLastReadPost(); await initializeScript(); createButtons(); }; window.addEventListener("blur", async () => { isTabFocused = false; console.log("🌐 Tab nicht mehr fokussiert."); if (lastReadPost && !downloadTriggered) { // Prüfe, ob sich die aktuelle Leseposition von der zuletzt heruntergeladenen unterscheidet if ( !lastDownloadedPost || lastDownloadedPost.timestamp !== lastReadPost.timestamp || lastDownloadedPost.authorHandler !== lastReadPost.authorHandler ) { downloadTriggered = true; console.log("📥 Speichere und lade aktuelle Leseposition herunter..."); await saveLastReadPostToFile(); // Speichert lokal await downloadLastReadPost(); // Löst den Download aus lastDownloadedPost = { ...lastReadPost }; // Aktualisiere die zuletzt heruntergeladene Position downloadTriggered = false; } else { console.log("⏹️ Leseposition ist identisch mit der zuletzt heruntergeladenen. Download übersprungen."); } } }); window.addEventListener("focus", () => { isTabFocused = true; downloadTriggered = false; console.log("🟢 Tab wieder fokussiert."); }); async function initializeScript() { console.log("🔧 Lade Leseposition..."); await loadLastReadPostFromFile(); observeForNewPosts(); window.addEventListener("scroll", () => { if (isAutoScrolling || isSearching) { console.log("⏹️ Scroll-Ereignis ignoriert (automatischer Modus aktiv)."); return; } markTopVisiblePost(true); }); } async function downloadLastReadPost() { if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler) { console.warn("⚠️ Keine gültige Leseposition zum Herunterladen."); return; } // Prüfe, ob die Leseposition bereits heruntergeladen wurde if ( lastDownloadedPost && lastDownloadedPost.timestamp === lastReadPost.timestamp && lastDownloadedPost.authorHandler === lastReadPost.authorHandler ) { console.log("⏹️ Leseposition ist identisch mit der zuletzt heruntergeladenen. Download übersprungen."); return; } try { const data = JSON.stringify(lastReadPost, null, 2); const sanitizedHandler = lastReadPost.authorHandler.replace(/[^a-zA-Z0-9-_]/g, ""); const timestamp = new Date(lastReadPost.timestamp).toISOString().replace(/[:.-]/g, "_"); const fileName = `${timestamp}_${sanitizedHandler}.json`; GM_download({ url: `data:application/json;charset=utf-8,${encodeURIComponent(data)}`, name: fileName, onload: () => { console.log(`✅ Leseposition erfolgreich heruntergeladen: ${fileName}`); // Aktualisiere lastDownloadedPost nach erfolgreichem Download lastDownloadedPost = { ...lastReadPost }; }, onerror: (err) => console.error("❌ Fehler beim Herunterladen der Leseposition:", err), }); } catch (error) { console.error("❌ Fehler beim Herunterladen der Leseposition:", error); } } async function loadNewestLastReadPost() { try { const localData = GM_getValue("lastReadPost", null); if (localData) { lastReadPost = JSON.parse(localData); console.log("✅ Lokale Leseposition beim Start geladen:", lastReadPost); } else { console.warn("⚠️ Keine gespeicherte Leseposition gefunden."); } } catch (err) { console.error("❌ Fehler beim Laden der neuesten Leseposition:", err); } } 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."); } } catch (err) { console.error("❌ Fehler beim Laden der Leseposition:", err); } } function markTopVisiblePost(save = true) { const topPost = getTopVisiblePost(); if (!topPost) { console.log("❌ Kein oberster sichtbarer Beitrag gefunden."); return; } const postTimestamp = getPostTimestamp(topPost); const authorHandler = getPostAuthorHandler(topPost); if (postTimestamp && authorHandler) { const newPost = { timestamp: postTimestamp, authorHandler }; const existingData = GM_getValue("lastReadPost", null); const existingPost = existingData ? JSON.parse(existingData) : null; if (save && (!existingPost || new Date(postTimestamp) > new Date(existingPost.timestamp))) { lastReadPost = newPost; console.log("💾 Neue Leseposition erkannt und aktualisiert:", lastReadPost); if (isTabFocused) { saveLastReadPostToFile(); // Speichert nur lokal, kein Download } } } } 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() { const storedData = GM_getValue("lastReadPost", null); if (!storedData) { console.log("❌ Keine gespeicherte Leseposition gefunden."); showPopup("❌ Keine gespeicherte Leseposition vorhanden."); return; } try { lastReadPost = JSON.parse(storedData); if (!lastReadPost.timestamp || !lastReadPost.authorHandler) { console.log("❌ Gespeicherte Leseposition ist ungültig:", lastReadPost); showPopup("❌ Ungültige gespeicherte Leseposition."); return; } } catch (err) { console.error("❌ Fehler beim Parsen der gespeicherten Leseposition:", err); showPopup("❌ Fehler bei der gespeicherten Leseposition."); return; } console.log("🔍 Starte verfeinerte Suche mit gespeicherter Position:", lastReadPost); const popup = createSearchPopup(); let direction = 1; let scrollAmount = 2000; let previousScrollY = -1; function handleSpaceKey(event) { if (event.code === "Space") { console.log("⏹️ Suche manuell gestoppt."); isSearching = false; popup.remove(); window.removeEventListener("keydown", handleSpaceKey); } } window.addEventListener("keydown", handleSpaceKey); const search = () => { if (!isSearching) { popup.remove(); 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; window.scrollBy(0, direction * scrollAmount); setTimeout(search, 300); }; isSearching = true; search(); } function startRefinedSearchForLastReadPostWithPosition(position) { if (!position || !position.timestamp || !position.authorHandler) { console.log("❌ Ungültige Leseposition für Suche:", position); showPopup("❌ Ungültige Leseposition."); return; } console.log("🔍 Starte verfeinerte Suche mit temporärer Position:", position); const popup = createSearchPopup(); let direction = 1; let scrollAmount = 2000; let previousScrollY = -1; function handleSpaceKey(event) { if (event.code === "Space") { console.log("⏹️ Suche manuell gestoppt."); isSearching = false; popup.remove(); window.removeEventListener("keydown", handleSpaceKey); } } window.addEventListener("keydown", handleSpaceKey); const search = () => { if (!isSearching) { popup.remove(); return; } const visiblePosts = getVisiblePosts(); const comparison = compareVisiblePostsToLastReadPost(visiblePosts, position); if (comparison === "match") { const matchedPost = findPostByData(position); if (matchedPost) { console.log("🎯 Beitrag gefunden:", position); 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; window.scrollBy(0, direction * scrollAmount); setTimeout(search, 300); }; 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 gefunden."); 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 gefunden."); return; } isAutoScrolling = true; post.style.outline = "none"; post.style.boxShadow = "0 0 20px 10px rgba(255, 215, 0, 0.9)"; post.style.animation = "glow 2s infinite"; if (!document.querySelector('#glowStyle')) { const style = document.createElement('style'); style.id = 'glowStyle'; style.textContent = ` @keyframes glow { 0% { box-shadow: 0 0 20px 10px rgba(255, 215, 0, 0.9); } 50% { box-shadow: 0 0 5px 2px rgba(255, 215, 0, 0.6); } 100% { box-shadow: 0 0 20px 10px rgba(255, 215, 0, 0.9); } } `; document.head.appendChild(style); } post.scrollIntoView({ behavior: "smooth", block: "center" }); const removeHighlightOnScroll = () => { if (!isAutoScrolling) { post.style.boxShadow = "none"; post.style.animation = "none"; console.log("✅ Highlight entfernt nach manuellem Scroll."); window.removeEventListener("scroll", removeHighlightOnScroll); } }; setTimeout(() => { isAutoScrolling = false; window.addEventListener("scroll", removeHighlightOnScroll); console.log("✅ Beitrag zentriert, warte auf manuellen Scroll."); }, 1000); } function getVisiblePosts() { const posts = Array.from(document.querySelectorAll("article")); return posts.map(post => ({ element: post, timestamp: getPostTimestamp(post), authorHandler: getPostAuthorHandler(post), })); } async function saveLastReadPostToFile() { try { if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler) { console.warn("⚠️ Keine gültige Leseposition vorhanden. Speichern übersprungen."); return; } const existingData = GM_getValue("lastReadPost", null); if (existingData) { const existingPost = JSON.parse(existingData); if ( existingPost.timestamp === lastReadPost.timestamp && existingPost.authorHandler === lastReadPost.authorHandler ) { console.log("⏹️ Lesestelle ist identisch mit der gespeicherten. Speichern übersprungen."); return; } } GM_setValue("lastReadPost", JSON.stringify(lastReadPost)); console.log("💾 Leseposition erfolgreich lokal gespeichert:", lastReadPost); } catch (err) { console.error("❌ Fehler beim Speichern der Leseposition:", err); } } async function deleteOldReadingPositions(handler) { console.log(`🗑️ Ältere Lesestellen für den Handler "${handler}" werden simuliert entfernt.`); } function observeForNewPosts() { const observer = new MutationObserver(() => { if (window.scrollY <= 3 && !isSearching) { const newPostsIndicator = getNewPostsIndicator(); if (newPostsIndicator) { console.log("🆕 Neue Beiträge erkannt. Starte automatische Suche..."); clickNewPostsIndicator(newPostsIndicator); setTimeout(() => { startRefinedSearchForLastReadPost(); }, 1500); } } }); observer.observe(document.body, { childList: true, subtree: true, }); } function getNewPostsIndicator() { const elements = document.querySelectorAll('[aria-label]'); for (const element of elements) { const ariaLabel = element.getAttribute('aria-label'); if (ariaLabel && ariaLabel.includes('undefined')) { console.log(`🆕 Neuer Beitrags-Indikator gefunden mit aria-label: "${ariaLabel}"`); const button = element.closest('button[role="button"]') || element; if (button) { return button; } } } console.warn("⚠️ Kein neuer Beitragsindikator gefunden."); return null; } function clickNewPostsIndicator(indicator) { if (!indicator) { console.warn("⚠️ Kein neuer Beitragsindikator gefunden."); return; } console.log("✅ Klicke auf neuen Beitragsindikator..."); indicator.click(); console.log("✅ Neuer Beitragsindikator erfolgreich geklickt."); } 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() { const buttonContainer = document.createElement("div"); buttonContainer.style.position = "fixed"; buttonContainer.style.top = "50%"; buttonContainer.style.left = "3px"; buttonContainer.style.transform = "translateY(-50%)"; buttonContainer.style.display = "flex"; buttonContainer.style.flexDirection = "column"; buttonContainer.style.gap = "3px"; buttonContainer.style.zIndex = "10000"; const buttonsConfig = [ { icon: "📂", title: "Load saved position", onClick: async () => { await importLastReadPost(); }, }, { icon: "🔍", title: "Start manual search", onClick: () => { console.log("🔍 Manuelle Suche gestartet."); startRefinedSearchForLastReadPost(); }, }, ]; 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 = "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 = "inset 0 0 10px rgba(255, 255, 255, 0.5)"; button.style.transition = "transform 0.2s, box-shadow 0.3s"; button.textContent = icon; button.title = title; button.addEventListener("click", () => { button.style.boxShadow = "inset 0 0 20px rgba(255, 255, 255, 0.8)"; button.style.transform = "scale(0.9)"; setTimeout(() => { button.style.boxShadow = "inset 0 0 10px rgba(255, 255, 255, 0.5)"; button.style.transform = "scale(1)"; }, 300); onClick(); }); button.addEventListener("mouseenter", () => { button.style.boxShadow = "inset 0 0 15px rgba(255, 255, 255, 0.7)"; button.style.transform = "scale(1.1)"; }); button.addEventListener("mouseleave", () => { button.style.boxShadow = "inset 0 0 10px rgba(255, 255, 255, 0.5)"; button.style.transform = "scale(1)"; }); 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.r###lt); if (importedData.timestamp && importedData.authorHandler) { console.log("✅ Importierte Leseposition geladen (wird nicht intern gespeichert):", importedData); showPopup("✅ Position geladen. Suche startet..."); const tempPosition = importedData; const matchedPost = findPostByData(tempPosition); if (matchedPost) { scrollToPostWithHighlight(matchedPost); } else { startRefinedSearchForLastReadPostWithPosition(tempPosition); } } else { throw new Error("Ungültige Position"); } } catch (error) { console.error("❌ Fehler beim Importieren der Leseposition:", error); showPopup("❌ Fehler: Ungültige Position."); } }; reader.readAsText(file); } }); document.body.appendChild(input); input.click(); document.body.removeChild(input); } 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); } })();