Greasy Fork is available in English.
跟踪并通过Tampermonkey的内部存储在Twitter/X上同步您的最后阅读位置。
当前为
// ==UserScript==
// @name X Timeline Manager
// @description Tracks and syncs your last reading position on Twitter/X using Tampermonkey's internal storage.
// @description:de Verfolgt und synchronisiert Ihre letzte Leseposition auf Twitter/X mit der internen Speicherung von Tampermonkey.
// @description:es Rastrea y sincroniza tu última posición de lectura en Twitter/X utilizando el almacenamiento interno de Tampermonkey.
// @description:fr Suit et synchronise votre dernière position de lecture sur Twitter/X en utilisant le stockage interne de Tampermonkey.
// @description:zh-CN 跟踪并通过Tampermonkey的内部存储在Twitter/X上同步您的最后阅读位置。
// @description:ru Отслеживает и синхронизирует вашу последнюю позицию чтения на Twitter/X, используя внутреннее хранилище Tampermonkey.
// @description:ja Tampermonkeyの内部ストレージを使用して、Twitter/Xでの最後の読書位置を追跡および同期します。
// @description:pt-BR Rastrea e sincroniza sua última posição de leitura no Twitter/X usando o armazenamento interno do Tampermonkey.
// @description:hi Tampermonkey के आंतरिक संग्रहण का उपयोग करके Twitter/X पर आपकी अंतिम पठन स्थिति को ट्रैक और सिंक करता है.
// @description:ar يتتبع ويزامن آخر موضع قراءة لك على Twitter/X باستخدام التخزين الداخلي لـ Tampermonkey.
// @icon https://x.com/favicon.ico
// @namespace http://tampermonkey.net/
// @version 2024.12.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 Leseposition
let isAutoScrolling = false;
let isSearching = false;
window.onload = async () => {
console.log("🚀 Seite vollständig geladen. Initialisiere Skript...");
await initializeScript();
};
async function initializeScript() {
console.log("🔧 Lade Leseposition...");
await loadLastReadPostFromFile();
if (lastReadPost?.timestamp && lastReadPost?.authorHandler) {
console.log(`📍 Geladene Leseposition: ${lastReadPost.timestamp}, @${lastReadPost.authorHandler}`);
await startSearchForLastReadPost();
} else {
console.warn("⚠️ Keine gültige Leseposition gefunden. Suche übersprungen.");
}
console.log("🔍 Starte Beobachtung für neue Beiträge...");
observeForNewPosts();
window.addEventListener("scroll", () => {
if (!isAutoScrolling && !isSearching) {
markCentralVisiblePost(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);
}
}
function observeForNewPosts() {
const observer = new MutationObserver(() => {
const newPostsIndicator = getNewPostsIndicator();
if (newPostsIndicator && window.scrollY === 0) {
// Nur auslösen, wenn der Benutzer oben auf der Seite ist
console.log("🆕 Neue Beiträge erkannt. Klicke auf den Indikator.");
clickNewPostsIndicator(newPostsIndicator);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function getNewPostsIndicator() {
return document.querySelector('div[aria-label*="ungelesene Elemente"]');
}
function clickNewPostsIndicator(indicator) {
if (!indicator) {
console.warn("⚠️ Kein Indikator für neue Beiträge gefunden.");
return;
}
indicator.scrollIntoView({ behavior: "smooth", block: "center" });
setTimeout(() => {
indicator.click();
console.log("✅ Indikator für neue Beiträge geklickt.");
if (!isSearching) {
console.log("🔄 Starte die Suche nach der Lesestelle nach dem Laden neuer Beiträge...");
startSearchForLastReadPost();
}
}, 500);
}
async function startSearchForLastReadPost() {
if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler) {
console.log("❌ Keine gültige Leseposition verfügbar. Suche übersprungen.");
return;
}
isSearching = true;
isAutoScrolling = true;
console.log("🔍 Suche nach der letzten Leseposition gestartet...");
const searchInterval = setInterval(() => {
const matchedPost = findPostByData(lastReadPost);
if (matchedPost) {
clearInterval(searchInterval);
isSearching = false;
isAutoScrolling = false;
scrollToPost(matchedPost);
console.log(`🎯 Zuletzt gelesenen Beitrag gefunden: ${lastReadPost.timestamp}, @${lastReadPost.authorHandler}`);
} else {
console.log("🔄 Beitrag nicht direkt gefunden. Suche weiter unten.");
window.scrollBy({ top: 500, behavior: "smooth" });
}
}, 1000);
}
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 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 markCentralVisiblePost(save = true) {
const centralPost = getCentralVisiblePost();
if (!centralPost) {
console.log("❌ Kein zentral sichtbarer Beitrag gefunden.");
return;
}
const postTimestamp = getPostTimestamp(centralPost);
const authorHandler = getPostAuthorHandler(centralPost);
if (!postTimestamp || !authorHandler) {
console.log("❌ Zentral sichtbarer Beitrag hat keine gültigen Daten.");
return;
}
if (!lastReadPost || new Date(postTimestamp) > new Date(lastReadPost.timestamp)) {
lastReadPost = { timestamp: postTimestamp, authorHandler };
console.log(`💾 Neuste Leseposition aktualisiert: ${postTimestamp}, @${authorHandler}`);
if (save) saveLastReadPostToFile();
}
}
function getCentralVisiblePost() {
const posts = Array.from(document.querySelectorAll("article"));
const centerY = window.innerHeight / 2;
return posts.reduce((closestPost, currentPost) => {
const rect = currentPost.getBoundingClientRect();
const distanceToCenter = Math.abs(centerY - (rect.top + rect.bottom) / 2);
if (!closestPost) return currentPost;
const closestRect = closestPost.getBoundingClientRect();
const closestDistance = Math.abs(centerY - (closestRect.top + closestRect.bottom) / 2);
return distanceToCenter < closestDistance ? currentPost : closestPost;
}, 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);
}
})();