// ==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 タイムライン自動読み込みと中断のない読書 // @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で新しい投稿を自動的に読み込み、読書位置をそのまま保持します。新しい投稿を読み込む前に、最後に見えるハンドル(例:@ユーザー名)に仮想マーカーを設定し、このマーカーにビューを復元します。 // @namespace http://tampermonkey.net/ // @version 2024.12.21 // @icon https://cdn-icons-png.flaticon.com/128/14417/14417460.png // @author Copiis // @match https://x.com/* // @grant none // @license MIT // @downloadURL none // ==/UserScript== (function () { let isAutomationActive = false; let isAutoScrolling = false; // Status für automatisches Scrollen let savedTopPostInfo = null; // Speichert Name, Handler und erstes Wort des Beitrags // Funktion zur Überprüfung der Sichtbarkeit und Aktivität function checkVisibility() { if (document.visibilityState === 'visible' && document.hasFocus()) { console.log("[DEBUG] X.com ist jetzt aktiv und sichtbar."); } else { console.log("[DEBUG] X.com ist entweder im Hintergrund oder nicht aktiv."); disableAutomationByScrolling(); // Automatik ausschalten } } // Automatik durch Scrollen deaktivieren function disableAutomationByScrolling() { console.log("[DEBUG] Automatik wird durch Scrollen deaktiviert."); window.scrollBy(0, 10); // Scrollt 10 Pixel nach unten isAutomationActive = false; // Automatik deaktivieren } // Initialisiere die Sichtbarkeitsprüfung function initVisibilityListeners() { document.addEventListener('visibilitychange', checkVisibility); window.addEventListener('focus', checkVisibility); window.addEventListener('blur', checkVisibility); // Initial prüfen checkVisibility(); } // Starte die kontinuierliche Überprüfung, sobald die Seite geladen wird document.addEventListener('DOMContentLoaded', () => { console.log("[DEBUG] Seite geladen. Starte Überprüfung für sichtbaren Handler..."); checkForVisibleHandler(); initVisibilityListeners(); }); // Funktion, um kontinuierlich nach einem sichtbaren Handler zu suchen function checkForVisibleHandler() { const checkInterval = setInterval(() => { const topPost = getTopVisiblePost(); if (topPost) { savedTopPostInfo = getPostInfo(topPost); console.log("[DEBUG] Sichtbarer Beitrag gefunden und gespeichert:", savedTopPostInfo); clearInterval(checkInterval); // Stoppe die Überprüfung, sobald ein Beitrag gefunden wurde } }, 100); // Überprüfe alle 100ms } const observer = new MutationObserver(() => { if (isAtTopOfPage() && !isAutomationActive) { console.log("[DEBUG] Scrollen zum oberen Ende erkannt. Automatik wird aktiviert."); extractPostInfoFromTopPost(); isAutomationActive = true; } if (isAutomationActive) { const newPostsButton = getNewPostsButton(); if (newPostsButton) { console.log("[DEBUG] 'Neue Posts anzeigen'-Button gefunden, wird geklickt."); newPostsButton.click(); waitForNewPostsToLoad(); } } }); observer.observe(document.body, { childList: true, subtree: true }); window.addEventListener('scroll', () => { if (isAutoScrolling) { console.log("[DEBUG] Automatisches Scrollen erkannt. Scroll-Event ignoriert."); return; } if (window.scrollY === 0 && !isAutomationActive) { console.log("[DEBUG] Scrollen zum oberen Ende erkannt. Automatik wird aktiviert."); extractPostInfoFromTopPost(); isAutomationActive = true; } else if (window.scrollY > 0 && isAutomationActive) { console.log("[DEBUG] Automatik deaktiviert, da nicht oben."); isAutomationActive = false; } }); function isAtTopOfPage() { return window.scrollY === 0; } function getNewPostsButton() { return Array.from(document.querySelectorAll('button, span')).find((button) => /neue Posts anzeigen|Post anzeigen/i.test(button.textContent.trim()) ); } function extractPostInfoFromTopPost() { const topPost = getTopVisiblePost(); if (topPost) { savedTopPostInfo = getPostInfo(topPost); console.log("[DEBUG] Oberster Beitrag gespeichert:", savedTopPostInfo); } else { console.log("[DEBUG] Kein oberster Beitrag gefunden."); } } function getPostInfo(post) { const nameElement = post.querySelector('a[role="link"] div[dir="ltr"] span'); const name = nameElement?.textContent?.trim() || "Unbekannt"; const handlerElement = post.querySelector('a[role="link"]'); const handler = handlerElement?.getAttribute("href") || "Unbekannt"; const postContentElement = post.querySelector('div[data-testid="tweetText"]'); // Text des Beitrags const postContent = postContentElement?.textContent?.trim() || ""; const firstWord = postContent.split(/\s+/)[0]; // Erstes Wort des tatsächlichen Beitrags return { name, handler, firstWord }; } function getTopVisiblePost() { const posts = Array.from(document.querySelectorAll('article')); console.log(`[DEBUG] Anzahl gefundener Beiträge: ${posts.length}`); return posts.length > 0 ? posts[0] : null; } function waitForNewPostsToLoad() { console.log("[DEBUG] Warte auf das Laden neuer Beiträge..."); const checkInterval = setInterval(() => { const matchedPost = findPostByInfo(savedTopPostInfo); if (matchedPost) { clearInterval(checkInterval); console.log("[DEBUG] Gespeicherter Beitrag nach Laden gefunden. Scrollen..."); scrollToPost(matchedPost, 'end'); // Scrollt so, dass der gespeicherte Beitrag am unteren Rand sichtbar ist } }, 100); } function findPostByInfo(postInfo) { if (!postInfo) return null; const posts = Array.from(document.querySelectorAll('article')); return posts.find((post) => { const nameElement = post.querySelector('a[role="link"] div[dir="ltr"] span'); const name = nameElement?.textContent?.trim(); const handlerElement = post.querySelector('a[role="link"]'); const handler = handlerElement?.getAttribute("href"); const postContentElement = post.querySelector('div[data-testid="tweetText"]'); // Text des Beitrags const postContent = postContentElement?.textContent?.trim() || ""; const firstWord = postContent.split(/\s+/)[0]; // Erstes Wort des tatsächlichen Beitrags return ( name === postInfo.name && handler === postInfo.handler && firstWord === postInfo.firstWord // Verhindert Duplikate ); }); } function scrollToPost(post, position = 'end') { console.log(`[DEBUG] Scrollen zum gespeicherten Beitrag (Position: ${position})...`); isAutoScrolling = true; // Setzt den Status, dass ein automatisches Scrollen läuft post.scrollIntoView({ behavior: 'smooth', block: position }); setTimeout(() => { isAutoScrolling = false; // Scrollen abgeschlossen isAutomationActive = false; console.log("[DEBUG] Automatisches Scrollen beendet. Automatik deaktiviert."); }, 1000); // Warte, bis das Scrollen abgeschlossen ist } })();