您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Archive tweets into a sidebar and keep them continuously synced, even after multiple "Show more" clicks or React re-renders.
// ==UserScript== // @name Twitter/X Archive - bypass infinite scroll showing only part of a page at a time (Robust Sync) // @namespace http://tampermonkey.net/ // @version 1.0 // @description Archive tweets into a sidebar and keep them continuously synced, even after multiple "Show more" clicks or React re-renders. // @author You // @match https://twitter.com/* // @match https://x.com/* // @grant none // ==/UserScript== (function () { 'use strict'; // --- Create archive sidebar --- const archive = document.createElement('div'); Object.assign(archive.style, { position: 'fixed', top: '50px', right: '0', width: '450px', height: '90%', overflowY: 'auto', background: 'white', borderLeft: '2px solid #ccc', zIndex: 99999, fontSize: '14px', padding: '8px', boxSizing: 'border-box' }); archive.innerHTML = "<h3 style='margin:4px 0 8px;font-weight:bold;'>Archived Tweets (Robust Sync)</h3><div id='archive-contents'></div>"; document.body.appendChild(archive); const archiveContents = archive.querySelector('#archive-contents'); // --- Track tweets --- const seen = new Map(); // tweetId → { original, clone, observer } // --- Helper: create clone container --- function makeCloneContainer(article) { const div = document.createElement("div"); div.style.border = "1px solid #ddd"; div.style.margin = "6px 0"; div.style.padding = "6px"; div.style.background = "#fff"; div.style.maxWidth = "100%"; div.innerHTML = article.innerHTML; return div; } // --- Attach a new observer to keep clone synced --- function attachObserver(tweetId, article, clone) { // Disconnect old one if exists if (seen.get(tweetId)?.observer) { seen.get(tweetId).observer.disconnect(); } const innerObserver = new MutationObserver(() => { clone.innerHTML = article.innerHTML; }); innerObserver.observe(article, { childList: true, subtree: true, characterData: true }); // Update mapping seen.set(tweetId, { original: article, clone, observer: innerObserver }); } // --- Process a tweet article --- function processArticle(article) { const link = article.querySelector('a[href*="/status/"]'); if (!link) return; const tweetId = link.getAttribute('href'); if (!tweetId) return; // Already archived? if (seen.has(tweetId)) { const entry = seen.get(tweetId); if (entry.original !== article) { // Original was replaced → reattach observer console.log("♻️ Tweet DOM replaced, reattaching observer:", tweetId); attachObserver(tweetId, article, entry.clone); } return; } // First time seeing this tweet const clone = makeCloneContainer(article); archiveContents.appendChild(clone); console.log("📌 Archived new tweet:", tweetId); attachObserver(tweetId, article, clone); } // --- Global observer for new tweets --- const observer = new MutationObserver(mutations => { for (let m of mutations) { for (let node of m.addedNodes) { if (node.nodeType !== 1) continue; if (node.tagName === "ARTICLE") { processArticle(node); } const articles = node.querySelectorAll ? node.querySelectorAll("article") : []; articles.forEach(a => processArticle(a)); } } }); // --- Attach to timeline --- function watchTimeline() { const timeline = document.querySelector('[role="feed"], [aria-label^="Timeline"]'); if (timeline) { console.log("✅ Archive: timeline found, robust syncing active..."); const articles = timeline.querySelectorAll("article"); articles.forEach(a => processArticle(a)); observer.observe(timeline, { childList: true, subtree: true }); } else { console.log("⏳ Archive: timeline not found yet, retrying..."); setTimeout(watchTimeline, 1000); } } watchTimeline(); })();