Tracks and syncs your last reading position on Twitter/X using a local file for cross-device sync.
目前為
// ==UserScript==
// @name X Timeline Manager
// @description Tracks and syncs your last reading position on Twitter/X using a local file for cross-device sync.
// @description:de Verfolgt und synchronisiert Ihre letzte Leseposition auf Twitter/X mithilfe einer lokalen Datei für geräteübergreifende Synchronisierung.
// @description:es Rastrea y sincroniza tu última posición de lectura en Twitter/X utilizando un archivo local para sincronización entre dispositivos.
// @description:fr Suit et synchronise votre dernière position de lecture sur Twitter/X en utilisant un fichier local pour la synchronisation entre appareils.
// @description:zh-CN 跟踪并通过本地文件在设备之间同步您在 Twitter/X 上的最后阅读位置。
// @description:ru Отслеживает и синхронизирует вашу последнюю позицию чтения на Twitter/X с помощью локального файла для синхронизации между устройствами.
// @description:ja ローカルファイルを使用して、Twitter/Xでの最後の読書位置を追跡し、デバイス間で同期します。
// @description:pt-BR Rastreia e sincroniza sua última posição de leitura no Twitter/X usando um arquivo local para sincronização entre dispositivos.
// @description:hi Twitter/X पर आपकी अंतिम पठन स्थिति को ट्रैक और सिंक करता है, स्थानीय फ़ाइल के माध्यम से उपकरणों के बीच सिंक्रनाइज़ेशन करता है。
// @icon https://cdn-icons-png.flaticon.com/128/14417/14417460.png
// @namespace http://tampermonkey.net/
// @version 2024.11.29.2
// @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
const fileName = "last_read_position.json"; // Name der Datei für die Leseposition
let folderHandle = null; // Globaler Ordnerzugriff
let isAutoScrolling = false; // Markiert, ob das Skript automatisch scrollt
let isSearching = false; // Markiert, ob nach Leseposition gesucht wird
let popup; // Referenz für das Popup
window.onload = async () => {
console.log("🚀 Seite vollständig geladen. Initialisiere Skript...");
const folderMetadata = localStorage.getItem("folderHandle");
if (!folderMetadata) {
console.warn("⚠️ Kein gespeicherter Ordnerzugriff gefunden.");
showPopup(); // Zeige Popup, um Benutzer zum Auswählen eines Ordners aufzufordern
} else {
console.log("✅ Speicherordner gefunden. Bitte autorisieren Sie den Zugriff.");
showPopup(); // Ordnerzugriff erneut erlauben
}
};
function showPopup() {
popup = document.createElement("div");
popup.textContent = "Set up a sync folder to save your reading position.";
popup.style.position = "fixed";
popup.style.top = "50%";
popup.style.left = "50%";
popup.style.transform = "translate(-50%, -50%)";
popup.style.backgroundColor = "black";
popup.style.color = "white";
popup.style.padding = "20px";
popup.style.borderRadius = "8px";
popup.style.boxShadow = "0 4px 8px rgba(0, 0, 0, 0.3)";
popup.style.textAlign = "center";
popup.style.zIndex = "10000";
const button = document.createElement("button");
button.textContent = "Set Sync Folder";
button.style.marginTop = "10px";
button.style.padding = "10px 15px";
button.style.fontSize = "14px";
button.style.backgroundColor = "white";
button.style.color = "black";
button.style.border = "none";
button.style.borderRadius = "4px";
button.style.cursor = "pointer";
button.addEventListener("click", async () => {
console.log("🗂 Benutzer öffnet Ordner-Auswahldialog...");
folderHandle = await selectFolderHandle();
if (folderHandle) {
console.log("✅ Ordner erfolgreich ausgewählt.");
popup.remove(); // Popup ausblenden
saveFolderHandleMetadata();
await initializeScript();
} else {
console.warn("⚠️ Kein Ordner ausgewählt. Bitte erneut versuchen.");
}
});
popup.appendChild(button);
document.body.appendChild(popup);
}
async function initializeScript() {
console.log("🔧 Lade Leseposition...");
await loadLastReadPostFromFile();
if (folderHandle && lastReadPost?.timestamp && lastReadPost?.authorHandler) {
console.log(`📍 Geladene Leseposition: ${lastReadPost.timestamp}, @${lastReadPost.authorHandler}`);
startSearchForLastReadPost();
} else {
console.log("❌ Keine gültige Leseposition oder Ordnerzugriff. Suche übersprungen.");
}
console.log("🔍 Starte Beobachtung für neue Beiträge...");
observeForNewPosts();
window.addEventListener("scroll", () => {
if (!isAutoScrolling && !isSearching) {
markCentralVisiblePost(true);
}
});
}
async function selectFolderHandle() {
try {
return await window.showDirectoryPicker();
} catch (err) {
console.warn("⚠️ Zugriff auf lokalen Ordner verweigert oder fehlgeschlagen:", err);
return null;
}
}
function saveFolderHandleMetadata() {
try {
localStorage.setItem("folderHandle", "true");
console.log("💾 Speicherordner erfolgreich gespeichert.");
} catch (err) {
console.error("❌ Fehler beim Speichern des Speicherordners:", err);
}
}
async function getFileHandle(create = false) {
if (!folderHandle) {
console.warn("⚠️ Kein gültiger Ordnerzugriff. Datei kann nicht geöffnet werden.");
return null;
}
try {
return await folderHandle.getFileHandle(fileName, { create });
} catch (err) {
console.warn("⚠️ Datei konnte nicht abgerufen werden:", err);
return null;
}
}
async function loadLastReadPostFromFile() {
try {
const handle = await getFileHandle(false);
if (handle) {
console.log("📄 Datei gefunden. Lese Leseposition...");
const file = await handle.getFile();
const text = await file.text();
lastReadPost = JSON.parse(text);
console.log("✅ Leseposition erfolgreich geladen:", lastReadPost);
} else {
console.warn("⚠️ Keine Datei gefunden. Erstelle eine neue Leseposition-Datei.");
await saveLastReadPostToFile();
}
} catch (err) {
console.warn("⚠️ Leseposition konnte nicht aus der Datei gelesen werden:", err);
}
}
async function saveLastReadPostToFile() {
if (!folderHandle) {
console.warn("⚠️ Kein Ordnerzugriff verfügbar. Überspringe das Speichern.");
return;
}
if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler) {
console.log("❌ Keine gültige Leseposition gefunden. Speichere Standardwerte.");
return;
}
try {
const handle = await getFileHandle(true);
if (!handle) {
console.warn("⚠️ Datei-Handle nicht verfügbar. Speicherung abgebrochen.");
return;
}
const writable = await handle.createWritable();
await writable.write(JSON.stringify(lastReadPost, null, 2));
await writable.close();
console.log("💾 Leseposition erfolgreich gespeichert:", lastReadPost);
} catch (err) {
console.error("❌ Fehler beim Speichern der Leseposition:", err);
}
}
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 interval = setInterval(() => {
const matchedPost = findPostByData(lastReadPost);
if (matchedPost) {
clearInterval(interval);
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 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 observeForNewPosts() {
const observer = new MutationObserver(() => {
const newPostsButton = getNewPostsButton();
if (newPostsButton) {
console.log("🆕 Neue Beiträge gefunden. Klicke auf den Button.");
clickNewPostsButton(newPostsButton);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function getNewPostsButton() {
return Array.from(document.querySelectorAll("div.css-146c3p1"))
.find(div => div.textContent && /Post anzeigen|Posts anzeigen/i.test(div.textContent));
}
function clickNewPostsButton(button) {
if (!button) {
console.log("❌ Button ist nicht definiert.");
return;
}
button.scrollIntoView({ behavior: "smooth", block: "center" });
setTimeout(() => {
button.click();
console.log("✅ Button für neue Beiträge geklickt.");
}, 500);
}
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();
} else {
console.log(`⚠️ Ältere Leseposition ignoriert: ${postTimestamp}, @${authorHandler}`);
}
}
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);
}
})();