// ==UserScript== // @name X.com Timeline Auto-Load with Uninterrupted Reading // @name:de X.com Timeline Auto-Load mit unterbrechungsfreiem Lesen // @name:fr X.com Timeline Auto-Load avec lecture ininterrompue // @name:es Carga automática de la línea de tiempo de X.com con lectura sin interrupciones // @name:it Caricamento automatico della timeline di X.com con lettura ininterrotta // @name:zh X.com 时间线自动加载,无缝阅读 // @name:ja X.com タイムライン自動読み込みと中断のない読書 // @namespace http://tampermonkey.net/ // @description Automatically loads new posts on X.com while keeping the reading position intact. Sets a virtual marker at the last visible handler (e.g., @username) before loading new posts and restores the view to this marker. // @description:de Lädt automatisch neue Beiträge auf X.com, ohne die Leseposition zu verlieren. Setzt eine virtuelle Markierung am letzten sichtbaren Handler (z. B. @Benutzername) vor dem Laden neuer Beiträge und stellt die Ansicht zu dieser Markierung wieder her. // @description:fr Charge automatiquement les nouveaux messages sur X.com tout en conservant la position de lecture. Place un marqueur virtuel au dernier handle visible (par exemple, @nomutilisateur) avant de charger les nouveaux messages et restaure la vue à ce marqueur. // @description:es Carga automáticamente nuevos posts en X.com mientras mantiene la posición de lectura intacta. Coloca un marcador virtual en el último manejador visible (por ejemplo, @nombredeusuario) antes de cargar nuevos posts y restaura la vista a ese marcador. // @description:it Carica automaticamente nuovi post su X.com mantenendo intatta la posizione di lettura. Imposta un segnalibro virtuale sull'ultimo handle visibile (es. @nomeutente) prima di caricare nuovi post e ripristina la vista su quel segnalibro. // @description:zh 在X.com上自动加载新帖子,同时保持阅读位置不变。在加载新帖子之前,在最后一个可见的处理器(例如@用户名)处设置一个虚拟标记,并将视图恢复到该标记。 // @description:ja X.comで新しい投稿を自動的に読み込み、読書位置をそのまま保持します。新しい投稿を読み込む前に、最後に見えるハンドル(例:@ユーザー名)に仮想マーカーを設定し、このマーカーにビューを復元します。 // @author Copiis // @version 2024.12.22-5 // @license MIT // @match https://x.com/home // @icon https://cdn-icons-png.flaticon.com/128/14417/14417460.png // @grant GM_setValue // @grant GM_getValue // @downloadURL none // ==/UserScript== (function () { let isAutomationActive = false; let isAutoScrolling = false; let savedTopPostData = null; // Prüfen, ob die aktuelle Seite /home ist function isHomePage() { return window.location.pathname === "/home"; } window.onload = () => { if (!isHomePage()) { console.log("Nicht auf der /home-Seite. Automatik deaktiviert."); return; // Automatik deaktivieren, wenn nicht auf /home } console.log("Auf der /home-Seite. Initialisiere Script..."); initializeScript(); }; function initializeScript() { loadSavedData(); if (savedTopPostData) { console.log(`Gespeicherte Daten gefunden. Versuche zum gespeicherten Beitrag zu scrollen: Handler: ${savedTopPostData.authorHandler}, Timestamp: ${savedTopPostData.timestamp}`); scrollToSavedPost(); } else { console.log("Keine gespeicherten Daten gefunden. Automatik startet erst, wenn der Benutzer manuell scrollt."); } const observer = new MutationObserver(() => { if (!isHomePage()) { console.log("Nicht auf der /home-Seite. Beobachter deaktiviert."); observer.disconnect(); return; } if (isAtTopOfPage() && !isAutomationActive) { activateAutomation(); } if (isAutomationActive) { const newPostsButton = getNewPostsButton(); if (newPostsButton) { console.log("Neue Beiträge erkannt. Klicke auf den 'Neue Posts anzeigen'-Button..."); newPostsButton.click(); waitForNewPostsToLoad(() => scrollToSavedPost()); } } }); observer.observe(document.body, { childList: true, subtree: true }); window.addEventListener('scroll', () => { if (!isHomePage()) { console.log("Nicht auf der /home-Seite. Scroll-Listener deaktiviert."); return; } if (isAutoScrolling) return; if (window.scrollY === 0 && !isAutomationActive) { activateAutomation(); } else if (window.scrollY > 0 && isAutomationActive) { deactivateAutomation(); } }); } function scrollToSavedPost() { const interval = setInterval(() => { if (!isPageFullyLoaded()) { console.log("Warte auf vollständiges Laden der Seite..."); return; } const matchedPost = findPostByData(savedTopPostData); const oldestVisibleTimestamp = getOldestVisibleTimestamp(); if (matchedPost) { clearInterval(interval); console.log("Gespeicherter Beitrag gefunden. Scrollen..."); scrollToPost(matchedPost, "center"); } else if (isTimestampOlderThanThreeHours(oldestVisibleTimestamp)) { clearInterval(interval); console.log("Ältester sichtbarer Beitrag ist mehr als 3 Stunden älter als der gespeicherte Beitrag. Scrollen gestoppt."); } else if (!isAtBottomOfPage()) { console.log("Scrollen nach unten, um mehr Beiträge zu laden..."); scrollToBottomWithDelay(() => { console.log("Warten auf neue Beiträge nach Scrollen..."); }); } else { console.log("Weitere Beiträge werden geladen..."); } }, 1000); } function isTimestampOlderThanThreeHours(timestamp) { if (!timestamp || !savedTopPostData || !savedTopPostData.timestamp) return false; const savedDate = new Date(savedTopPostData.timestamp); const visibleDate = new Date(timestamp); const diffInHours = (savedDate - visibleDate) / (1000 * 60 * 60); return diffInHours >= 3; } function getOldestVisibleTimestamp() { const posts = Array.from(document.querySelectorAll("article")); let oldestTimestamp = null; posts.forEach((post) => { const timestamp = getPostTimestamp(post); if (timestamp) { const postDate = new Date(timestamp); if (!oldestTimestamp || postDate < new Date(oldestTimestamp)) { oldestTimestamp = timestamp; } } }); return oldestTimestamp; } function scrollToBottomWithDelay(callback) { window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" }); setTimeout(() => { if (isPageFullyLoaded()) { callback(); } else { console.log("Seite ist noch nicht vollständig geladen. Weitere Wartezeit..."); setTimeout(callback, 2000); // Zusätzliche Wartezeit, wenn noch nicht geladen } }, 3000); // Wartezeit nach Scrollen, angepasst für langsame Ladezeiten } function activateAutomation() { isAutomationActive = true; console.log("Automatik aktiviert."); saveTopPostData(); } function deactivateAutomation() { isAutomationActive = false; console.log("Automatik deaktiviert."); } function saveTopPostData() { const topPost = getTopVisiblePost(); if (topPost) { savedTopPostData = { timestamp: getPostTimestamp(topPost), authorHandler: getPostAuthorHandler(topPost), }; saveData(savedTopPostData); } } function loadSavedData() { const savedData = GM_getValue("topPostData", null); if (savedData) { savedTopPostData = JSON.parse(savedData); } } function saveData(data) { GM_setValue("topPostData", JSON.stringify(data)); console.log(`Daten dauerhaft gespeichert: Handler: ${data.authorHandler}, Timestamp: ${data.timestamp}`); } function waitForNewPostsToLoad(callback) { const checkInterval = setInterval(() => { if (isPageFullyLoaded()) { clearInterval(checkInterval); callback(); } }, 500); } function findPostByData(data) { if (!data || !data.timestamp || !data.authorHandler) return null; const posts = Array.from(document.querySelectorAll("article")); return posts.find((post) => { const postTimestamp = getPostTimestamp(post); const postAuthorHandler = getPostAuthorHandler(post); return postTimestamp === data.timestamp && postAuthorHandler === data.authorHandler; }); } function getPostTimestamp(post) { const timeElement = post.querySelector("time"); return timeElement?.getAttribute("datetime") || null; } function getPostAuthorHandler(post) { const authorElement = post.querySelector(".css-1jxf684.r-bcqeeo.r-1ttztb7.r-qvutc0.r-poiln3"); return authorElement?.textContent.trim() || null; } function getTopVisiblePost() { const posts = Array.from(document.querySelectorAll("article")); return posts.length > 0 ? posts[0] : null; } function getNewPostsButton() { return Array.from(document.querySelectorAll("button, span")).find((button) => /neue Posts anzeigen|Post anzeigen/i.test(button.textContent.trim()) ); } function scrollToPost(post, position = "center") { isAutoScrolling = true; post.scrollIntoView({ behavior: "smooth", block: position }); setTimeout(() => { isAutoScrolling = false; }, 1000); } function isPageFullyLoaded() { return document.readyState === "complete"; } function isAtTopOfPage() { return window.scrollY === 0; } function isAtBottomOfPage() { return window.innerHeight + window.scrollY >= document.body.scrollHeight - 1; } })();