// ==UserScript==
// @name Twitter/X Enhanced Reading Tracker
// @name:de Twitter/X Erweiterter Lesefortschritt-Tracker
// @name:fr Twitter/X Suivi Amélioré de la Lecture
// @name:es Twitter/X Rastreador Mejorado de Lectura
// @name:it Twitter/X Tracker Avanzato di Lettura
// @name:pt Twitter/X Rastreador Avançado de Leitura
// @name:ru Twitter/X Улучшенный Трекер Прочтения
// @name:zh-CN Twitter/X 增强版阅读跟踪器
// @name:ja Twitter/X 強化型読書トラッカー
// @name:ko Twitter/X 향상된 읽기 추적기
// @name:hi Twitter/X उन्नत पठन ट्रैकर
// @name:ar Twitter/X متتبع القراءة المحسن
// @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.27
// @author Copiis
// @license MIT
// @match https://x.com/home
// @grant GM_setValue
// @grant GM_getValue
// @downloadURL none
// ==/UserScript==
(function () {
'use strict';
// Leseliste und Lesestellenliste initialisieren
let readList = JSON.parse(localStorage.getItem('readList')) || [];
let readingPosition = JSON.parse(localStorage.getItem('readingPosition')) || null;
const MAX_DAYS = 30;
const POST_SELECTOR = "span.css-1jxf684";
const TIME_SELECTOR = "time";
const NEW_POSTS_SELECTOR = "span.css-1jxf684"; // Selector für die Neue-Posts-Schaltfläche
let isScrollingManually = true;
let autoScrollInterval = null;
// Multilinguale Nachrichten
const messages = {
search_active: {
en: "Search active...",
de: "Suche aktiv...",
es: "Búsqueda activa...",
fr: "Recherche en cours...",
it: "Ricerca in corso..."
},
search_found: {
en: "Reading position found!",
de: "Lesestelle gefunden!",
es: "Posición de lectura encontrada!",
fr: "Position de lecture trouvée!",
it: "Posizione di lettura trovata!"
},
saved_read_list: {
en: "New post saved to reading list:",
de: "Neuer Beitrag in der Leseliste gespeichert:",
es: "Nuevo post guardado en la lista de lectura:",
fr: "Nouveau post enregistré dans la liste de lecture :",
it: "Nuovo post salvato nella lista di lettura:"
},
saved_reading_position: {
en: "New reading position saved:",
de: "Neue Lesestelle gespeichert:",
es: "Nueva posición de lectura guardada:",
fr: "Nouvelle position de lecture enregistrée :",
it: "Nuova posizione di lettura salvata:"
},
search_started: {
en: "Search started",
de: "Suche gestartet",
es: "Búsqueda iniciada",
fr: "Recherche démarrée",
it: "Ricerca avviata"
}
};
// Browser-Sprache erkennen
const userLanguage = navigator.language.slice(0, 2); // Z.B. 'de', 'en', etc.
function translateMessage(key) {
return messages[key][userLanguage] || messages[key].en; // Fallback auf Englisch
}
// Popup-Funktionen
function createPopup(message) {
let popup = document.createElement("div");
popup.id = "searchPopup";
popup.style.position = "fixed";
popup.style.top = "20%";
popup.style.left = "50%";
popup.style.transform = "translate(-50%, -50%)";
popup.style.backgroundColor = "rgba(0, 0, 0, 0.9)";
popup.style.color = "#fff";
popup.style.padding = "30px 50px";
popup.style.borderRadius = "10px";
popup.style.boxShadow = "0 8px 20px rgba(0, 0, 0, 0.7)";
popup.style.zIndex = "99999";
popup.style.fontSize = "24px";
popup.style.fontWeight = "bold";
popup.style.textAlign = "center";
popup.style.animation = "popup-glow 1.5s infinite alternate";
popup.innerHTML = message;
// Füge Animation über CSS hinzu
const style = document.createElement("style");
style.type = "text/css";
style.innerHTML = `
@keyframes popup-glow {
from {
box-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
}
to {
box-shadow: 0 0 40px rgba(255, 255, 255, 0.9);
}
}
`;
document.head.appendChild(style);
document.body.appendChild(popup);
}
function updatePopup(message) {
const popup = document.getElementById("searchPopup");
if (popup) {
popup.innerHTML = message;
}
}
function removePopup() {
const popup = document.getElementById("searchPopup");
if (popup) {
popup.remove();
}
}
// Hilfsfunktionen
function log(message) {
console.log(`[HandlerTracker] ${message}`);
}
function cleanupOldEntries() {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - MAX_DAYS);
readList = readList.filter(entry => new Date(entry.time) >= cutoffDate);
localStorage.setItem('readList', JSON.stringify(readList));
}
function isElementInViewport(element) {
const rect = element.getBoundingClientRect();
return rect.top >= 0 && rect.bottom <= window.innerHeight;
}
function findPostInViewport() {
const handlerSpans = document.querySelectorAll(POST_SELECTOR);
for (const handlerSpan of handlerSpans) {
if (isElementInViewport(handlerSpan)) {
const handlerText = handlerSpan.textContent.trim();
if (handlerText.startsWith("@")) { // Sicherstellen, dass es sich um einen Handler handelt
const timeElement = handlerSpan.closest('article')?.querySelector(TIME_SELECTOR);
if (timeElement) {
return {
handler: handlerText,
time: timeElement.getAttribute('datetime'),
};
}
}
}
}
return null;
}
function saveToReadList(post) {
if (!readList.some(entry => entry.handler === post.handler && entry.time === post.time)) {
readList.push(post);
cleanupOldEntries();
localStorage.setItem('readList', JSON.stringify(readList));
log(`${translateMessage("saved_read_list")} ${JSON.stringify(post)}`);
}
}
function saveToReadingPosition(post) {
if (!isScrollingManually) return; // Nur beim manuellen Scrollen speichern
const postTime = new Date(post.time);
const readingTime = readingPosition ? new Date(readingPosition.time) : null;
// Aktualisierung nur, wenn die neue Lesestelle jünger ist
if (!readingPosition || postTime > readingTime) {
readingPosition = post;
localStorage.setItem('readingPosition', JSON.stringify(post));
log(`${translateMessage("saved_reading_position")} ${JSON.stringify(post)}`);
}
}
function scrollToReadingPosition() {
if (readingPosition) {
log(translateMessage("search_started"));
createPopup(translateMessage("search_active"));
isScrollingManually = false; // Automatisches Scrollen beginnt
let direction = "down"; // Suchrichtung (nach unten starten)
autoScrollInterval = setInterval(() => {
const post = findPostInViewport();
if (post) {
const postTime = new Date(post.time);
const readingTime = new Date(readingPosition.time);
if (post.handler === readingPosition.handler && post.time === readingPosition.time) {
const foundMessage = `${translateMessage("search_found")}
Handler: ${post.handler}
Timestamp: ${post.time}`;
log(foundMessage);
updatePopup(foundMessage);
setTimeout(removePopup, 2000); // Popup nach 2 Sekunden entfernen
clearInterval(autoScrollInterval);
autoScrollInterval = null;
isScrollingManually = true; // Automatisches Scrollen beendet
return;
}
if (direction === "down" && postTime < readingTime) {
direction = "up"; // Richtung wechseln
}
}
// Scrollbewegung basierend auf der Richtung
if (direction === "down") {
window.scrollBy(0, window.innerHeight);
} else if (direction === "up") {
window.scrollBy(0, -window.innerHeight / 2); // Langsames Scrollen nach oben
}
}, 500);
}
}
function detectNewPostsButton() {
const newPostsButton = document.querySelector(NEW_POSTS_SELECTOR);
if (newPostsButton && newPostsButton.textContent.includes("Post anzeigen")) {
log("Neue-Posts-Schaltfläche erkannt, klicke darauf.");
newPostsButton.click();
return true; // Erfolgreich geklickt
}
return false; // Keine passende Schaltfläche gefunden
}
function stopAutoScrollOnManualScroll() {
if (!isScrollingManually && autoScrollInterval) {
clearInterval(autoScrollInterval);
autoScrollInterval = null;
isScrollingManually = true;
removePopup(); // Popup entfernen, wenn die automatische Suche manuell abgebrochen wird
}
}
// Hauptintervall zur Erkennung von Posts bei manuellem Scrollen
function detectPostsWhileScrolling() {
if (isScrollingManually) {
const post = findPostInViewport();
if (post) {
saveToReadList(post);
saveToReadingPosition(post);
}
}
}
// Warten auf vollständiges Laden der Seite
window.addEventListener("load", () => {
setTimeout(() => {
detectNewPostsButton();
scrollToReadingPosition();
}, 1000); // Verzögerung von 1 Sekunde
});
window.addEventListener('scroll', () => {
if (!isScrollingManually) {
stopAutoScrollOnManualScroll(); // Automatische Suche abbrechen
}
detectPostsWhileScrolling(); // Nur bei manuellem Scrollen
});
})();