您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展(如 Stylus)后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
(我已经安装了用户样式管理器,让我安装!)
// ==UserScript==
// @name X Timeline Manager
// @description Automatically tracks and saves your last reading position on Twitter/X, allowing seamless resumption after refreshing or navigating away.
// @description:de Verfolgt und speichert automatisch Ihren letzten Lesefortschritt auf Twitter/X, sodass Sie nach einem Refresh oder Verlassen der Seite nahtlos fortfahren können.
// @description:fr Suit et enregistre automatiquement votre dernière position de lecture sur Twitter/X, permettant une reprise facile après un rafraîchissement ou un changement de page.
// @description:es Realiza un seguimiento y guarda automáticamente tu última posición de lectura en Twitter/X, permitiendo continuar sin problemas después de actualizar o cambiar de página.
// @description:it Tiene traccia e salva automaticamente la tua ultima posizione di lettura su Twitter/X, consentendo una ripresa fluida dopo il refresh o la navigazione altrove.
// @description:pt Acompanha e salva automaticamente sua última posição de leitura no Twitter/X, permitindo retomar sem interrupções após atualizar ou navegar para outro lugar.
// @description:ru Автоматически отслеживает и сохраняет вашу последнюю позицию чтения в Twitter/X, позволяя беспрепятственно продолжить чтение после обновления или перехода на другую страницу.
// @description:zh-CN 自动跟踪并保存您在 Twitter/X 上的最后阅读位置,允许在刷新或导航后无缝恢复。
// @description:ja Twitter/X での最後の読書位置を自動的に追跡して保存し、更新やページ遷移後にシームレスに再開できるようにします。
// @description:ko Twitter/X에서 마지막 읽기 위치를 자동으로 추적하고 저장하여 새로 고침하거나 다른 페이지로 이동한 후에도 원활하게 이어갈 수 있습니다.
// @description:hi Twitter/X पर आपके अंतिम पढ़ने की स्थिति को स्वचालित रूप से ट्रैक और सहेजता है, जिससे ताज़ा करने या दूसरी जगह नेविगेट करने के बाद भी आसानी से फिर से शुरू किया जा सके।
// @description:ar يتتبع ويحفظ تلقائيًا آخر موضع قراءة لك على Twitter/X، مما يسمح بالاستئناف بسلاسة بعد التحديث أو التنقل بعيدًا。
// @description:ar يقوم بتحميل المنشورات الجديدة تلقائيًا على X.com/Twitter ويعيدك إلى موضع القراءة.
// @icon https://cdn-icons-png.flaticon.com/128/14417/14417460.png
// @supportURL https://www.paypal.com/paypalme/Coopiis?country.x=DE&locale.x=de_DE
// @namespace http://tampermonkey.net/
// @version 2024.11.28
// @author Copiis
// @license MIT
// @match https://x.com/home
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function () {
let lastReadPost = null; // Die einzige Lesestelle
let readingList = []; // Leseliste: Liste der letzten 30 Tage
let isAutoScrolling = false; // Unterscheidet zwischen Skript- und Benutzer-Scrolling
let isSearching = false; // Verhindert das Markieren neuer Beiträge während der Suche
window.onload = () => {
console.log("Seite vollständig geladen. Initialisiere Script...");
initializeScript();
};
function initializeScript() {
loadReadingList(); // Leseliste aus dem Speicher laden
loadLastReadPost(); // Letzte Lesestelle laden
if (!readingList.length) {
console.log("Keine Beiträge indexiert. Versuche, den neuesten Beitrag als Lesestelle zu markieren.");
markNewestPostAsFirstReadPost();
}
if (lastReadPost) {
console.log(`Lesestelle geladen: ${lastReadPost.timestamp}, @${lastReadPost.authorHandler}`);
waitForPageToLoad(() => {
scrollToLastReadPost();
});
} else {
console.log("Keine gespeicherte Lesestelle vorhanden.");
}
// Beobachte manuelles Scrollen
window.addEventListener('scroll', () => {
if (!isAutoScrolling && !isSearching) {
markCentralVisiblePost();
indexVisiblePosts();
}
});
// Beobachte Mutationen für das Erscheinen von neuen Posts
const observer = new MutationObserver(() => {
const newPostsButton = getNewPostsButton();
if (newPostsButton) {
console.log("Neue Posts gefunden. Klicke auf den Button.");
clickNewPostsButton(newPostsButton);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function saveLastReadPost() {
if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler) {
console.log("Ungültige Lesestelle, keine Speicherung durchgeführt.");
return;
}
GM_setValue("lastReadPost", JSON.stringify(lastReadPost));
console.log(`Lesestelle gespeichert: ${lastReadPost.timestamp}, @${lastReadPost.authorHandler}`);
}
function scrollToPost(post) {
if (!post) {
console.log("Kein Beitrag zum Scrollen gefunden.");
return;
}
isAutoScrolling = true; // Markiere, dass das Skript scrollt
post.scrollIntoView({ behavior: "smooth", block: "center" }); // Beitrag in die Mitte scrollen
setTimeout(() => {
isAutoScrolling = false; // Scrollen durch das Skript abgeschlossen
}, 1000);
console.log("Beitrag wurde zentriert gescrollt.");
}
function findPostByData(data) {
if (!data || !data.timestamp || !data.authorHandler) {
console.log("Ungültige Daten für die Suche nach einem Beitrag.");
return null;
}
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 scrollToLastReadPost() {
if (!lastReadPost) {
console.log("Keine Lesestelle vorhanden, zum Springen übersprungen.");
return;
}
isSearching = true;
waitForPageToLoad(() => {
const interval = setInterval(() => {
const matchedPost = findPostByData(lastReadPost);
if (matchedPost) {
clearInterval(interval);
isSearching = false;
scrollToPost(matchedPost);
console.log(`Zuletzt gelesenen Beitrag gefunden: ${lastReadPost.timestamp}, @${lastReadPost.authorHandler}`);
} else {
console.log("Gespeicherter Beitrag nicht direkt gefunden. Suche weiter unten.");
window.scrollBy({ top: 500, behavior: "smooth" });
}
}, 1000);
});
}
function markCentralVisiblePost() {
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 keinen gültigen Timestamp oder Handler.");
return;
}
if (lastReadPost && lastReadPost.timestamp === postTimestamp && lastReadPost.authorHandler === authorHandler) {
console.log(`Beitrag ist bereits die aktuelle Lesestelle: ${postTimestamp}, @${authorHandler}`);
return;
}
lastReadPost = { timestamp: postTimestamp, authorHandler };
console.log(`Neue Lesestelle markiert: ${postTimestamp}, @${authorHandler}`);
saveLastReadPost();
}
function markNewestPostAsFirstReadPost() {
const posts = Array.from(document.querySelectorAll("article"));
if (posts.length === 0) {
console.log("Keine Beiträge sichtbar, kann keinen ersten Beitrag markieren.");
return;
}
const newestPost = posts[0];
const postTimestamp = getPostTimestamp(newestPost);
const authorHandler = getPostAuthorHandler(newestPost);
if (postTimestamp && authorHandler) {
lastReadPost = { timestamp: postTimestamp, authorHandler };
console.log(`Erster Beitrag als Lesestelle markiert: ${postTimestamp}, @${authorHandler}`);
saveLastReadPost();
indexVisiblePosts();
} else {
console.log("Erster sichtbarer Beitrag hat keinen gültigen Timestamp oder Handler.");
}
}
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 indexVisiblePosts() {
const posts = Array.from(document.querySelectorAll("article"));
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
posts.forEach(post => {
const timestamp = getPostTimestamp(post);
const handler = getPostAuthorHandler(post);
if (timestamp && handler) {
const postDate = new Date(timestamp);
if (postDate >= thirtyDaysAgo) {
const exists = readingList.some(
item => item.timestamp === timestamp && item.authorHandler === handler
);
if (!exists) {
readingList.push({ timestamp, authorHandler: handler });
console.log(`Beitrag zur Leseliste hinzugefügt: ${timestamp}, @${handler}`);
}
}
}
});
saveReadingList();
}
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 waitForPageToLoad(callback) {
const interval = setInterval(() => {
if (document.readyState === "complete") {
console.log("Seite vollständig geladen. Starte Callback.");
clearInterval(interval);
callback();
} else {
console.log("Warte auf vollständiges Laden der Seite...");
}
}, 500);
}
function getNewPostsButton() {
const buttons = Array.from(document.querySelectorAll("div.css-146c3p1"));
const newPostsButton = buttons.find(div => {
const span = div.querySelector("span.css-1jxf684");
const text = span?.textContent.trim();
return text && /\d+\s\w*\sPost[s]?/i.test(text);
});
if (!newPostsButton) {
console.log("Neuer Posts-Bereich nicht gefunden.");
}
return newPostsButton;
}
function clickNewPostsButton(button) {
button.scrollIntoView({ behavior: "smooth", block: "center" });
button.click();
console.log(`Button für neue Posts geklickt: "${button.textContent.trim()}"`);
waitForNewPosts(() => {
console.log("Neue Beiträge wurden geladen.");
scrollToLastReadPost();
});
}
function waitForNewPosts(callback) {
const interval = setInterval(() => {
const posts = document.querySelectorAll("article");
if (posts.length > 0) {
clearInterval(interval);
callback();
} else {
console.log("Warte auf das Laden neuer Beiträge...");
}
}, 500);
}
function saveReadingList() {
GM_setValue("readingList", JSON.stringify(readingList));
console.log("Leseliste gespeichert.");
}
function loadReadingList() {
const savedData = GM_getValue("readingList", "[]");
try {
readingList = JSON.parse(savedData);
console.log(`Leseliste geladen: ${readingList.length} Einträge.`);
} catch (error) {
console.error("Fehler beim Laden der Leseliste. Initialisiere neue Liste.", error);
readingList = [];
saveReadingList();
}
}
function loadLastReadPost() {
const savedData = GM_getValue("lastReadPost", null);
if (savedData) {
try {
lastReadPost = JSON.parse(savedData);
console.log(`Lesestelle geladen: ${lastReadPost.timestamp}, @${lastReadPost.authorHandler}`);
} catch (error) {
console.error("Fehler beim Laden der Lesestelle. Initialisiere als null.", error);
lastReadPost = null;
}
}
}
})();