Greasy Fork is available in English.
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.
Version au
// ==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.19.1// @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; // Letzte Lesepositionlet 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 loadLastReadPostFromDownloadFolder(); // Lade Lesestelle aus dem Download-Ordnerawait initializeScript();createButtons();};async function initializeScript() {console.log("🔧 Lade Leseposition...");await loadLastReadPostFromFile();observeForNewPosts();// Scroll-Listener für manuelles Scrollen hinzufügenwindow.addEventListener("scroll", () => {if (isAutoScrolling || isSearching) {console.log("⏹️ Scroll-Ereignis ignoriert (automatischer Modus aktiv).");return;}// Obersten sichtbaren Beitrag markierenmarkTopVisiblePost(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;}}function startRefinedSearchForLastReadPost() {if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler) {console.log("❌ Keine gültige Leseposition verfügbar. Suche übersprungen.");return;}console.log("🔍 Verfeinerte Suche gestartet...");const popup = createSearchPopup();let direction = 1; // 1 = nach unten, -1 = nach obenlet scrollAmount = 2000; // Anfangsschrittweitelet 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;}const visiblePosts = getVisiblePosts();const comparison = compareVisiblePostsToLastReadPost(visiblePosts);if (comparison === "match") {const matchedPost = findPostByData(lastReadPost);if (matchedPost) {console.log("🎯 Beitrag gefunden:", lastReadPost);scrollToPostWithHighlight(matchedPost);isSearching = false;popup.remove();window.removeEventListener("keydown", handleSpaceKey);return;}} else if (comparison === "older") {direction = -1; // Nach oben scrollen} else if (comparison === "newer") {direction = 1; // Nach unten scrollen}if (window.scrollY === previousScrollY) {scrollAmount = Math.max(scrollAmount / 2, 500); // Schrittweite halbieren bei Stillstanddirection = -direction; // Richtung umkehren} else {scrollAmount = Math.min(scrollAmount * 1.5, 3000); // Schrittweite erhöhen}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 = "🔍 Verfeinerte Suche läuft... Drücke SPACE, um abzubrechen.";document.body.appendChild(popup);return popup;}function compareVisiblePostsToLastReadPost(posts) {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(lastReadPost.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 === lastReadPost.timestamp && post.authorHandler === lastReadPost.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 = "3px solid rgba(255, 255, 0, 0.8)";post.style.transition = "outline 1.5s ease-in-out";post.scrollIntoView({ behavior: "smooth", block: "center" });setTimeout(() => {post.style.outline = "3px solid rgba(255, 255, 0, 0)";}, 2000);setTimeout(() => {post.style.outline = "none";isAutoScrolling = false;console.log("✅ Beitrag erfolgreich zentriert und Hervorhebung entfernt!");}, 4500);}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;}// Aktuell gespeicherte Lesestelle abrufenconst existingData = GM_getValue("lastReadPost", null);if (existingData) {const existingPost = JSON.parse(existingData);// Vergleich: Ist die neue Lesestelle wirklich neuer?if (existingPost.timestamp === lastReadPost.timestamp &&existingPost.authorHandler === lastReadPost.authorHandler) {console.log("⏹️ Lesestelle ist identisch mit der gespeicherten. Kein Download erforderlich.");return;}// Falls die neue Lesestelle älter ist, nicht überschreibenif (new Date(existingPost.timestamp) > new Date(lastReadPost.timestamp)) {console.log("⏹️ Gespeicherte Lesestelle ist neuer. Kein Download erforderlich.");return;}}// Speichern der neuen LesestelleGM_setValue("lastReadPost", JSON.stringify(lastReadPost));console.log("💾 Leseposition erfolgreich gespeichert:", lastReadPost);// Herunterladen der neuen Lesestelleawait downloadLastReadPost();} catch (err) {console.error("❌ Fehler beim Speichern der Leseposition:", err);}}async function downloadLastReadPost() {if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler) {console.warn("⚠️ Keine gültige Leseposition zum Herunterladen.");return;}try {const data = JSON.stringify(lastReadPost, null, 2);const sanitizedHandler = lastReadPost.authorHandler.replace(/[^a-zA-Z0-9-_]/g, ""); // Sonderzeichen entfernenconst timestamp = new Date(lastReadPost.timestamp).toISOString().replace(/[:.-]/g, "_");const fileName = `${sanitizedHandler}_${timestamp}.json`;// Lösche ältere Lesestellen aus dem Download-Ordnerawait deleteOldReadingPositions(sanitizedHandler);GM_download({url: `data:application/json;charset=utf-8,${encodeURIComponent(data)}`,name: fileName,onload: () => console.log(`✅ Lesestelle erfolgreich heruntergeladen: ${fileName}`),onerror: (err) => console.error("❌ Fehler beim Herunterladen der Lesestelle:", err),});} catch (error) {console.error("❌ Fehler beim Herunterladen der Lesestelle:", error);}}async function deleteOldReadingPositions(handler) {console.log(`🗑️ Ältere Lesestellen für den Handler "${handler}" werden simuliert entfernt.`);// Tampermonkey kann keine Dateien direkt löschen. Dies ist eine Simulation.// In einer echten Implementierung könnte eine serverseitige Lösung verwendet werden.}async function loadLastReadPostFromDownloadFolder() {// Simulation des Ladens einer Datei (ersetzt durch API-Aufrufe oder Datei-Parsing-Logik)console.log("📂 Lade vorhandene Lesestelle aus dem Download-Ordner...");// Beispiel-Daten wiederherstellenconst exampleData = GM_getValue("lastReadPost", null);if (exampleData) {lastReadPost = JSON.parse(exampleData);console.log("✅ Lesestelle aus Download-Ordner geladen:", lastReadPost);}}function observeForNewPosts() {const observer = new MutationObserver(() => {if (window.scrollY <= 50) {const newPostsIndicator = getNewPostsIndicator();if (newPostsIndicator) {console.log("🆕 Neue Beiträge erkannt. Automatische Suche wird gestartet...");clickNewPostsIndicator(newPostsIndicator);setTimeout(() => {startRefinedSearchForLastReadPost();}, 1000);}}});observer.observe(document.body, {childList: true,subtree: true,});}function getNewPostsIndicator() {const buttons = document.querySelectorAll('button[role="button"]');for (const button of buttons) {const innerDiv = button.querySelector('div[style*="text-overflow: unset;"]');if (innerDiv) {const span = innerDiv.querySelector('span');if (span && /^\d+\s/.test(span.textContent.trim())) {console.log(`🆕 Neuer Beitrags-Indikator gefunden: "${span.textContent.trim()}"`);return button;}}}console.warn("⚠️ Kein neuer Beitragsindikator gefunden.");return null;}function clickNewPostsIndicator(indicator) {if (!indicator) {console.warn("⚠️ Kein Indikator für neue Beiträge gefunden.");return;}console.log("✅ Neuer Beitragsindikator wird geklickt...");indicator.scrollIntoView({ behavior: "smooth", block: "center" });setTimeout(() => {indicator.click();console.log("✅ Neuer Beitragsindikator wurde erfolgreich geklickt.");}, 500);}function markTopVisiblePost(save = true) {if (isAutoScrolling || isSearching) {console.log("⏹️ Automatische Aktionen oder Suche 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;}if (save && !isAutoScrolling && !isSearching) {if (!lastReadPost || new Date(postTimestamp) > new Date(lastReadPost.timestamp)) {lastReadPost = { timestamp: postTimestamp, authorHandler };console.log(`💾 Lesestelle aktualisiert: ${postTimestamp}, @${authorHandler}`);saveLastReadPostToFile();} else {console.log("⏹️ Lesestelle nicht aktualisiert, da keine neueren Beiträge gefunden wurden.");}}}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*="/"]');if (handlerElement) {const handler = handlerElement.getAttribute("href");return handler && handler.startsWith("/") ? handler.slice(1) : null;}return null;}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: "Gespeicherte Leseposition laden",onClick: async () => {await importLastReadPost();},},{icon: "🔍",title: "Suche manuell starten",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 = "0 0 10px rgba(255, 255, 255, 0.8)";button.style.transition = "transform 0.2s";button.textContent = icon;button.title = title;button.addEventListener("click", onClick);button.addEventListener("mouseenter", () => {button.style.transform = "scale(1.1)";});button.addEventListener("mouseleave", () => {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) {lastReadPost = importedData;await saveLastReadPostToFile();showPopup("✅ Leseposition erfolgreich geladen. Navigiere zur Leseposition...");console.log("✅ Importierte Leseposition:", lastReadPost);// Navigiere direkt zur Lesepositionconst matchedPost = findPostByData(lastReadPost);if (matchedPost) {scrollToPostWithHighlight(matchedPost);} else {showPopup("⚠️ Beitrag konnte nicht gefunden werden.");console.warn("⚠️ Kein Beitrag zur geladenen Leseposition gefunden.");}} 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 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);}})();