返回首頁 

Greasy Fork is available in English.

X Timeline Sync

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 18/12/2024. Voir la dernière version.


Installer ce script?
// ==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.18.1// @author            Copiis// @license           MIT// @match             https://x.com/home// @grant             GM_setValue// @grant             GM_getValue// ==/UserScript==//// If you find this script useful and would like to support my work, consider making a small donation!// Your generosity helps me maintain and improve projects like this one. 😊// Bitcoin (BTC): bc1quc5mkudlwwkktzhvzw5u2nruxyepef957p68r7// PayPal: https://www.paypal.com/paypalme/Coopiis?country.x=DE&locale.x=de_DE//// Thank you for your support! ❤️(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 initializeScript();createButtons();};async function initializeScript() {console.log("🔧 Lade Leseposition...");await loadLastReadPostFromFile();observeForNewPosts(); // Beobachtung für neue Beiträge aktivieren// 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;}}async function saveLastReadPostToFile() {try {if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler) {console.warn("⚠️ Keine gültige Leseposition vorhanden. Speichern übersprungen.");return;}GM_setValue("lastReadPost", JSON.stringify(lastReadPost));console.log("💾 Leseposition erfolgreich gespeichert:", lastReadPost);} catch (err) {console.error("❌ Fehler beim Speichern der Leseposition:", err);}}async function exportLastReadPost() {if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler) {console.warn("⚠️ Keine gültige Leseposition zum Exportieren.");showPopup("⚠️ Keine gültige Leseposition verfügbar.");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`;const blob = new Blob([data], { type: "application/json" });const a = document.createElement("a");a.href = URL.createObjectURL(blob);a.download = fileName;a.style.display = "none";document.body.appendChild(a);a.click();document.body.removeChild(a);} catch (error) {console.error("❌ Fehler beim Exportieren der Leseposition:", error);showPopup("❌ Fehler: Leseposition konnte nicht exportiert werden.");}}function createButtons() {const buttonContainer = document.createElement("div");buttonContainer.style.position = "fixed";buttonContainer.style.top = "50%"; // Mittig in der HöhebuttonContainer.style.left = "3px"; // Sehr nahe am linken RandbuttonContainer.style.transform = "translateY(-50%)"; // Exakte ZentrierungbuttonContainer.style.display = "flex";buttonContainer.style.flexDirection = "column"; // Anordnung untereinanderbuttonContainer.style.gap = "3px"; // Abstand zwischen den ButtonsbuttonContainer.style.zIndex = "10000";const buttonsConfig = [{ icon: "💾", title: "Leseposition exportieren", onClick: exportLastReadPost },{ icon: "📂", title: "Gespeicherte Leseposition importieren", onClick: 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"; // Größere Breitebutton.style.height = "36px"; // Größere Höhebutton.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"; // Größere Schriftbutton.style.boxShadow = "0 0 10px rgba(255, 255, 255, 0.8)"; // Stärkerer Schattenbutton.style.transition = "transform 0.2s"; // Animation beim Hoverbutton.textContent = icon;button.title = title;button.addEventListener("click", onClick);button.addEventListener("mouseenter", () => {button.style.transform = "scale(1.1)"; // Leichtes Vergrößern beim Hover});button.addEventListener("mouseleave", () => {button.style.transform = "scale(1)"; // Zurück zur ursprünglichen Größe});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 importiert.");console.log("✅ Importierte Leseposition:", lastReadPost);await startRefinedSearchForLastReadPost();} 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 observeForNewPosts() {const observer = new MutationObserver(() => {if (window.scrollY <= 50) { // Wenn der Nutzer am oberen Rand der Seite istconst newPostsIndicator = getNewPostsIndicator();if (newPostsIndicator) {console.log("🆕 Neue Beiträge erkannt. Automatische Suche wird gestartet...");clickNewPostsIndicator(newPostsIndicator);setTimeout(() => {startRefinedSearchForLastReadPost(); // Automatische Suche nach der Leseposition}, 1000); // 1 Sekunde warten, damit neue Beiträge vollständig geladen sind}}});observer.observe(document.body, {childList: true,subtree: true,});}function getNewPostsIndicator() {// Suche nach allen Buttons mit style "text-overflow: unset"const buttons = document.querySelectorAll('button[role="button"]');for (const button of buttons) {const innerDiv = button.querySelector('div[style*="text-overflow: unset;"]'); // Überprüfen des inneren Divsif (innerDiv) {const span = innerDiv.querySelector('span');if (span && /^\d+\s/.test(span.textContent.trim())) { // Prüfen, ob Text mit einer Zahl beginntconsole.log(`🆕 Neuer Beitrags-Indikator gefunden: "${span.textContent.trim()}"`);return button; // Button zurückgeben}}}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" }); // Scroll zum ButtonsetTimeout(() => {indicator.click(); // Klick ausführenconsole.log("✅ Neuer Beitragsindikator wurde erfolgreich geklickt.");}, 500);}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); // Visuelle HervorhebungisSearching = 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 {// Dynamische Anpassung der SchrittweitescrollAmount = Math.min(scrollAmount * 1.5, 3000); // Schrittweite vergrößern, maximal 3000px}previousScrollY = window.scrollY;// Scrollenwindow.scrollBy(0, direction * scrollAmount);// Nächster SuchdurchlaufsetTimeout(search, 300);};isSearching = true;search();}function getVisiblePosts() {const posts = Array.from(document.querySelectorAll("article"));return posts.map(post => ({element: post,timestamp: getPostTimestamp(post),authorHandler: getPostAuthorHandler(post),}));}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;// Visuelle Hervorhebung hinzufügenpost.style.outline = "3px solid rgba(255, 255, 0, 0.8)";post.style.transition = "outline 1.5s ease-in-out"; // Langsamer und sanfter Übergang// Scrollen und Hervorhebung animierenpost.scrollIntoView({ behavior: "smooth", block: "center" });// Rahmen sanft verblassen lassensetTimeout(() => {post.style.outline = "3px solid rgba(255, 255, 0, 0)"; // Transparenter Rahmen}, 2000); // Start nach 3 Sekunden// Rahmen nach dem Verblassen komplett entfernensetTimeout(() => {post.style.outline = "none";isAutoScrolling = false;console.log("✅ Beitrag erfolgreich zentriert und Hervorhebung entfernt!");}, 4500); // Nach dem Übergang von 1,5 Sekunden entfernen}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 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 getTopVisiblePost() {const posts = Array.from(document.querySelectorAll("article")); // Alle Beiträge sammelnreturn posts.find(post => {const rect = post.getBoundingClientRect();return rect.top >= 0 && rect.bottom > 0; // Oberster sichtbarer Beitrag});}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;}// Nur speichern, wenn explizit manuell gescrollt wurdeif (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 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 scrollToPost(post) {if (!post) {console.log("❌ Kein Beitrag zum Scrollen gefunden.");return;}isAutoScrolling = true;post.scrollIntoView({ behavior: "smooth", block: "center" });setTimeout(() => {isAutoScrolling = false;console.log("✅ Beitrag wurde erfolgreich zentriert!");}, 1000);}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);}})();