您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
跟踪并同步您在 Twitter/X 上的最后阅读位置,提供手动和自动选项。完美解决在查看新帖子时不丢失当前位置的问题。
- // ==UserScript==
- // @name Twitter/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 2025.6.9
- // @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!
- // Bitcoin (BTC): bc1quc5mkudlwwkktzhvzw5u2nruxyepef957p68r7
- // PayPal: https://www.paypal.com/paypalme/Coopiis?country.x=DE&locale.x=de_DE
- (function () {
- 'use strict';
- // Übersetzungen für alle Popup-Nachrichten
- const translations = {
- en: {
- noValidPosition: "❌ No valid reading position to download.",
- alreadyDownloaded: "ℹ️ This reading position has already been downloaded.",
- downloadSuccess: "✅ Reading position downloaded as {fileName}.",
- downloadFailed: "❌ Download failed. Reading position copied to clipboard. Please paste it into a .json file manually.",
- downloadClipboardFailed: "❌ Download and clipboard copy failed. Please save manually.",
- noPositionFound: "ℹ️ Scroll to set a reading position.",
- scriptError: "❌ Error loading the script.",
- invalidPosition: "❌ Invalid reading position.",
- fileSelectError: "❌ Please select a JSON file.",
- fileReadError: "❌ Error reading the file.",
- fileDialogError: "❌ Error opening file dialog.",
- fileLoadSuccess: "✅ Reading position successfully loaded!",
- buttonsError: "❌ Error displaying buttons.",
- searchPopup: "🔍 Searching... Press SPACE to cancel.",
- searchNoPosition: "❌ No reading position available.",
- searchScrollPrompt: "ℹ️ Please scroll or click the magnifier."
- },
- de: {
- noValidPosition: "❌ Keine gültige Leseposition zum Downloaden.",
- alreadyDownloaded: "ℹ️ Diese Leseposition wurde bereits heruntergeladen.",
- downloadSuccess: "✅ Leseposition als {fileName} heruntergeladen.",
- downloadFailed: "❌ Download fehlgeschlagen. Leseposition wurde in die Zwischenablage kopiert. Bitte manuell in eine .json-Datei einfügen.",
- downloadClipboardFailed: "❌ Download und Kopieren fehlgeschlagen. Bitte manuell speichern.",
- noPositionFound: "ℹ️ Scrolle, um eine Leseposition zu setzen.",
- scriptError: "❌ Fehler beim Laden des Skripts.",
- invalidPosition: "❌ Ungültige Leseposition.",
- fileSelectError: "❌ Bitte wähle eine JSON-Datei aus.",
- fileReadError: "❌ Fehler beim Lesen der Datei.",
- fileDialogError: "❌ Fehler beim Öffnen des Datei-Dialogs.",
- fileLoadSuccess: "✅ Leseposition erfolgreich geladen!",
- buttonsError: "❌ Fehler beim Anzeigen der Buttons.",
- searchPopup: "🔍 Suche läuft... Drücke LEERTASTE zum Abbrechen.",
- searchNoPosition: "❌ Keine Leseposition vorhanden.",
- searchScrollPrompt: "ℹ️ Bitte scrollen oder Lupe klicken."
- },
- es: {
- noValidPosition: "❌ No hay posición de lectura válida para descargar.",
- alreadyDownloaded: "ℹ️ Esta posición de lectura ya ha sido descargada.",
- downloadSuccess: "✅ Posición de lectura descargada como {fileName}.",
- downloadFailed: "❌ Falló la descarga. La posición de lectura se copió al portapapeles. Pégala manualmente en un archivo .json.",
- downloadClipboardFailed: "❌ Falló la descarga y la copia al portapapeles. Por favor, guarda manualmente.",
- noPositionFound: "ℹ️ Desplázate para establecer una posición de lectura.",
- scriptError: "❌ Error al cargar el script.",
- invalidPosition: "❌ Posición de lectura no válida.",
- fileSelectError: "❌ Por favor, selecciona un archivo JSON.",
- fileReadError: "❌ Error al leer el archivo.",
- fileDialogError: "❌ Error al abrir el diálogo de archivo.",
- fileLoadSuccess: "✅ ¡Posición de lectura cargada con éxito!",
- buttonsError: "❌ Error al mostrar los botones.",
- searchPopup: "🔍 Buscando... Presiona ESPACIO para cancelar.",
- searchNoPosition: "❌ No hay posición de lectura disponible.",
- searchScrollPrompt: "ℹ️ Por favor, desplázate o haz clic en la lupa."
- },
- fr: {
- noValidPosition: "❌ Aucune position de lecture valide à télécharger.",
- alreadyDownloaded: "ℹ️ Cette position de lecture a déjà été téléchargée.",
- downloadSuccess: "✅ Position de lecture téléchargée sous {fileName}.",
- downloadFailed: "❌ Échec du téléchargement. Position de lecture copiée dans le presse-papiers. Veuillez la coller manuellement dans un fichier .json.",
- downloadClipboardFailed: "❌ Échec du téléchargement et de la copie dans le presse-papiers. Veuillez sauvegarder manuellement.",
- noPositionFound: "ℹ️ Faites défiler pour définir une position de lecture.",
- scriptError: "❌ Erreur lors du chargement du script.",
- invalidPosition: "❌ Position de lecture invalide.",
- fileSelectError: "❌ Veuillez sélectionner un fichier JSON.",
- fileReadError: "❌ Erreur lors de la lecture du fichier.",
- fileDialogError: "❌ Erreur lors de l'ouverture de la boîte de dialogue.",
- fileLoadSuccess: "✅ Position de lecture chargée avec succès !",
- buttonsError: "❌ Erreur lors de l'affichage des boutons.",
- searchPopup: "🔍 Recherche en cours... Appuyez sur ESPACE pour annuler.",
- searchNoPosition: "❌ Aucune position de lecture disponible.",
- searchScrollPrompt: "ℹ️ Veuillez faire défiler ou cliquer sur la loupe."
- },
- 'zh-CN': {
- noValidPosition: "❌ 没有有效的阅读位置可以下载。",
- alreadyDownloaded: "ℹ️ 此阅读位置已下载。",
- downloadSuccess: "✅ 阅读位置已下载为 {fileName}。",
- downloadFailed: "❌ 下载失败。阅读位置已复制到剪贴板。请手动粘贴到 .json 文件中。",
- downloadClipboardFailed: "❌ 下载和剪贴板复制失败。请手动保存。",
- noPositionFound: "ℹ️ 滚动以设置阅读位置。",
- scriptError: "❌ 加载脚本时出错。",
- invalidPosition: "❌ 无效的阅读位置。",
- fileSelectError: "❌ 请选择一个 JSON 文件。",
- fileReadError: "❌ 读取文件时出错。",
- fileDialogError: "❌ 打开文件对话框时出错。",
- fileLoadSuccess: "✅ 阅读位置加载成功!",
- buttonsError: "❌ 显示按钮时出错。",
- searchPopup: "🔍 正在搜索... 按空格键取消。",
- searchNoPosition: "❌ 没有可用的阅读位置。",
- searchScrollPrompt: "ℹ️ 请滚动或点击放大镜。"
- },
- ru: {
- noValidPosition: "❌ Нет действительной позиции чтения для загрузки.",
- alreadyDownloaded: "ℹ️ Эта позиция чтения уже была загружена.",
- downloadSuccess: "✅ Позиция чтения загружена как {fileName}.",
- downloadFailed: "❌ Не удалось выполнить загрузку. Позиция чтения скопирована в буфер обмена. Пожалуйста, вставьте вручную в файл .json.",
- downloadClipboardFailed: "❌ Не удалось выполнить загрузку и копирование в буфер обмена. Пожалуйста, сохраните вручную.",
- noPositionFound: "ℹ️ Прокрутите, чтобы установить позицию чтения.",
- scriptError: "❌ Ошибка при загрузке скрипта.",
- invalidPosition: "❌ Недействительная позиция чтения.",
- fileSelectError: "❌ Пожалуйста, выберите файл JSON.",
- fileReadError: "❌ Ошибка при чтении файла.",
- fileDialogError: "❌ Ошибка при открытии диалогового окна.",
- fileLoadSuccess: "✅ Позиция чтения успешно загружена!",
- buttonsError: "❌ Ошибка при отображении кнопок.",
- searchPopup: "🔍 Поиск... Нажмите ПРОБЕЛ для отмены.",
- searchNoPosition: "❌ Позиция чтения недоступна.",
- searchScrollPrompt: "ℹ️ Прокрутите или нажмите на лупу."
- },
- ja: {
- noValidPosition: "❌ ダウンロードする有効な読み取り位置がありません。",
- alreadyDownloaded: "ℹ️ この読み取り位置はすでにダウンロードされています。",
- downloadSuccess: "✅ 読み取り位置が{fileName}としてダウンロードされました。",
- downloadFailed: "❌ ダウンロードに失敗しました。読み取り位置がクリップボードにコピーされました。手動で.jsonファイルに貼り付けてください。",
- downloadClipboardFailed: "❌ ダウンロードおよびクリップボードへのコピーに失敗しました。手動で保存してください。",
- noPositionFound: "ℹ️ スクロールして読み取り位置を設定してください。",
- scriptError: "❌ スクリプトの読み込み中にエラーが発生しました。",
- invalidPosition: "❌ 無効な読み取り位置です。",
- fileSelectError: "❌ JSONファイルを選択してください。",
- fileReadError: "❌ ファイルの読み込み中にエラーが発生しました。",
- fileDialogError: "❌ ファイルダイアログのオープン中にエラーが発生しました。",
- fileLoadSuccess: "✅ 読み取り位置が正常にロードされました!",
- buttonsError: "❌ ボタンの表示中にエラーが発生しました。",
- searchPopup: "🔍 検索中... スペースキーを押してキャンセル。",
- searchNoPosition: "❌ 読み取り位置がありません。",
- searchScrollPrompt: "ℹ️ スクロールするか、虫眼鏡をクリックしてください。"
- },
- 'pt-BR': {
- noValidPosition: "❌ Nenhuma posição de leitura válida para download.",
- alreadyDownloaded: "ℹ️ Esta posição de leitura já foi baixada.",
- downloadSuccess: "✅ Posição de leitura baixada como {fileName}.",
- downloadFailed: "❌ Falha no download. Posição de leitura copiada para a área de transferência. Cole manualmente em um arquivo .json.",
- downloadClipboardFailed: "❌ Falha no download e na cópia para a área de transferência. Por favor, salve manualmente.",
- noPositionFound: "ℹ️ Role para definir uma posição de leitura.",
- scriptError: "❌ Erro ao carregar o script.",
- invalidPosition: "❌ Posição de leitura inválida.",
- fileSelectError: "❌ Por favor, selecione um arquivo JSON.",
- fileReadError: "❌ Erro ao ler o arquivo.",
- fileDialogError: "❌ Erro ao abrir o diálogo de arquivo.",
- fileLoadSuccess: "✅ Posição de leitura carregada com sucesso!",
- buttonsError: "❌ Erro ao exibir os botões.",
- searchPopup: "🔍 Pesquisando... Pressione ESPAÇO para cancelar.",
- searchNoPosition: "❌ Nenhuma posição de leitura disponível.",
- searchScrollPrompt: "ℹ️ Role ou clique na lupa."
- },
- hi: {
- noValidPosition: "❌ डाउनलोड करने के लिए कोई वैध पढ़ने की स्थिति नहीं है।",
- alreadyDownloaded: "ℹ️ यह पढ़ने की स्थिति पहले ही डाउनलोड की जा चुकी है।",
- downloadSuccess: "✅ पढ़ने की स्थिति {fileName} के रूप में डाउनलोड की गई।",
- downloadFailed: "❌ डाउनलोड विफल। पढ़ने की स्थिति क्लिपबोर्ड में कॉपी की गई है। कृपया इसे मैन्युअल रूप से .json फ़ाइल में पेस्ट करें।",
- downloadClipboardFailed: "❌ डाउनलोड और क्लिपबोर्ड कॉपी विफल। कृपया मैन्युअल रूप से सहेजें।",
- noPositionFound: "ℹ️ पढ़ने की स्थिति सेट करने के लिए स्क्रॉल करें।",
- scriptError: "❌ स्क्रिप्ट लोड करने में त्रुटि।",
- invalidPosition: "❌ अमान्य पढ़ने की स्थिति।",
- fileSelectError: "❌ कृपया एक JSON फ़ाइल चुनें।",
- fileReadError: "❌ फ़ाइल पढ़ने में त्रुटि।",
- fileDialogError: "❌ फ़ाइल डायलॉग खोलने में त्रुटि।",
- fileLoadSuccess: "✅ पढ़ने की स्थिति सफलतापूर्वक लोड की गई!",
- buttonsError: "❌ बटनों को प्रदर्शित करने में त्रुटि।",
- searchPopup: "🔍 खोज चल रही है... रद्द करने के लिए स्पेस दबाएं।",
- searchNoPosition: "❌ कोई पढ़ने की स्थिति उपलब्ध नहीं है।",
- searchScrollPrompt: "ℹ️ कृपया स्क्रॉल करें या मैग्नीफायर पर क्लिक करें।"
- },
- ar: {
- noValidPosition: "❌ لا توجد مواضع قراءة صالحة للتحميل.",
- alreadyDownloaded: "ℹ️ تم تحميل موضع القراءة هذا بالفعل.",
- downloadSuccess: "✅ تم تحميل موضع القراءة باسم {fileName}.",
- downloadFailed: "❌ فشل التحميل. تم نسخ موضع القراءة إلى الحافظة. يرجى لصقه يدويًا في ملف .json.",
- downloadClipboardFailed: "❌ فشل التحميل والنسخ إلى الحافظة. يرجى الحفظ يدويًا.",
- noPositionFound: "ℹ️ قم بالتمرير لتحديد موضع القراءة.",
- scriptError: "❌ خطأ أثناء تحميل السكربت.",
- invalidPosition: "❌ موضع قراءة غير صالح.",
- fileSelectError: "❌ يرجى اختيار ملف JSON.",
- fileReadError: "❌ خطأ أثناء قراءة الملف.",
- fileDialogError: "❌ خطأ أثناء فتح حوار الملف.",
- fileLoadSuccess: "✅ تم تحميل موضع القراءة بنجاح!",
- buttonsError: "❌ خطأ أثناء عرض الأزرار.",
- searchPopup: "🔍 جارٍ البحث... اضغط على مفتاح المسافة للإلغاء.",
- searchNoPosition: "❌ لا يوجد موضع قراءة متاح.",
- searchScrollPrompt: "ℹ️ يرجى التمرير أو النقر على العدسة المكبرة."
- },
- it: {
- noValidPosition: "❌ Nessuna posizione di lettura valida da scaricare.",
- alreadyDownloaded: "ℹ️ Questa posizione di lettura è già stata scaricata.",
- downloadSuccess: "✅ Posizione di lettura scaricata come {fileName}.",
- downloadFailed: "❌ Download fallito. Posizione di lettura copiata negli appunti. Incollala manualmente in un file .json.",
- downloadClipboardFailed: "❌ Download e copia negli appunti falliti. Salva manualmente.",
- noPositionFound: "ℹ️ Scorri per impostare una posizione di lettura.",
- scriptError: "❌ Errore durante il caricamento dello script.",
- invalidPosition: "❌ Posizione di lettura non valida.",
- fileSelectError: "❌ Seleziona un file JSON.",
- fileReadError: "❌ Errore durante la lettura del file.",
- fileDialogError: "❌ Errore durante l'apertura della finestra di dialogo.",
- fileLoadSuccess: "✅ Posizione di lettura caricata con successo!",
- buttonsError: "❌ Errore durante la visualizzazione dei pulsanti.",
- searchPopup: "🔍 Ricerca in corso... Premi SPAZIO per annullare.",
- searchNoPosition: "❌ Nessuna posizione di lettura disponibile.",
- searchScrollPrompt: "ℹ️ Scorri o fai clic sulla lente d'ingrandimento."
- },
- ko: {
- noValidPosition: "❌ 다운로드할 유효한 읽기 위치가 없습니다.",
- alreadyDownloaded: "ℹ️ 이 읽기 위치는 이미 다운로드되었습니다.",
- downloadSuccess: "✅ 읽기 위치가 {fileName}으로 다운로드되었습니다.",
- downloadFailed: "❌ 다운로드 실패. 읽기 위치가 클립보드에 복사되었습니다. .json 파일에 수동으로 붙여넣으세요.",
- downloadClipboardFailed: "❌ 다운로드 및 클립보드 복사 실패. 수동으로 저장하세요.",
- noPositionFound: "ℹ️ 읽기 위치를 설정하려면 스크롤하세요.",
- scriptError: "❌ 스크립트 로드 중 오류가 발생했습니다.",
- invalidPosition: "❌ 유효하지 않은 읽기 위치입니다.",
- fileSelectError: "❌ JSON 파일을 선택하세요.",
- fileReadError: "❌ 파일 읽기 중 오류가 발생했습니다.",
- fileDialogError: "❌ 파일 대화 상자를 여는 중 오류가 발생했습니다.",
- fileLoadSuccess: "✅ 읽기 위치가 성공적으로 로드되었습니다!",
- buttonsError: "❌ 버튼 표시 중 오류가 발생했습니다.",
- searchPopup: "🔍 검색 중... 취소하려면 스페이스바를 누르세요.",
- searchNoPosition: "❌ 사용 가능한 읽기 위치가 없습니다.",
- searchScrollPrompt: "ℹ️ 스크롤하거나 돋보기를 클릭하세요."
- }
- };
- // Funktion zur Erkennung der Benutzersprache
- function getUserLanguage() {
- const lang = navigator.language || navigator.languages[0] || 'en';
- const langCode = lang.split('-')[0];
- return translations[lang] || translations[langCode] ? lang : 'en';
- }
- // Funktion zum Abrufen der übersetzten Nachricht
- function getTranslatedMessage(key, lang, params = {}) {
- const translation = translations[lang] || translations['en'];
- let message = translation[key] || translations['en'][key] || key;
- Object.keys(params).forEach(param => {
- message = message.replace(`{${param}}`, params[param]);
- });
- return message;
- }
- let lastReadPost = null;
- let isAutoScrolling = false;
- let isSearching = false;
- let isScriptActivated = false;
- let currentPost = null;
- let lastHighlightedPost = null;
- const downloadedPosts = new Set();
- function loadLastReadPost(callback) {
- try {
- const storedPost = GM_getValue("lastReadPost", null);
- if (storedPost) {
- const parsedPost = JSON.parse(storedPost);
- console.log("🛠️ DEBUG: Geladene Leseposition:", parsedPost);
- callback(parsedPost);
- } else {
- console.log("⏹️ Keine gespeicherte Leseposition gefunden.");
- callback(null);
- }
- } catch (err) {
- console.error("❌ Fehler beim Laden der Leseposition:", err);
- callback(null);
- }
- }
- function saveLastReadPost(post) {
- if (!post || !post.timestamp || !post.authorHandler) {
- console.log("❌ Ungültige Leseposition, Speicherung abgebrochen:", post);
- return;
- }
- let attempts = 0;
- const maxAttempts = 3;
- function trySave() {
- try {
- const postData = JSON.stringify(post);
- GM_setValue("lastReadPost", postData);
- console.log("💾 Leseposition erfolgreich mit GM_setValue gespeichert:", postData);
- localStorage.setItem("lastReadPost", postData);
- console.log("💾 Fallback in localStorage gespeichert:", localStorage.getItem("lastReadPost"));
- } catch (err) {
- attempts++;
- console.error(`❌ Fehler beim Speichern der Leseposition (Versuch ${attempts}/${maxAttempts}):`, err);
- if (attempts < maxAttempts) {
- console.log("🔄 Wiederhole Speicherversuch...");
- setTimeout(trySave, 1000);
- } else {
- console.error("❌ Maximale Speicherversuche erreicht. Fallback auf localStorage.");
- localStorage.setItem("lastReadPost", JSON.stringify(post));
- promptManualFallback(post);
- }
- }
- }
- trySave();
- }
- function promptManualFallback(data) {
- const content = JSON.stringify(data);
- const message = `📝 Neue Leseposition: ${content}\nBitte speichere dies manuell, da der Speichervorgang fehlschlug.`;
- showPopup(message, 10000);
- console.log("📝 Bitte manuell speichern:", content);
- }
- function downloadLastReadPost() {
- try {
- if (!currentPost || !currentPost.timestamp || !currentPost.authorHandler) {
- console.warn("⚠️ Keine gültige Leseposition zum Speichern:", currentPost);
- showPopup("noValidPosition", 5000);
- return;
- }
- const postKey = `${currentPost.timestamp}-${currentPost.authorHandler}`;
- if (downloadedPosts.has(postKey)) {
- console.log("⏹️ Leseposition bereits heruntergeladen:", postKey);
- showPopup("alreadyDownloaded", 5000);
- return;
- }
- console.log("🛠️ DEBUG: Starte manuellen Download-Prozess für Leseposition:", currentPost);
- const date = new Date(currentPost.timestamp);
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, "0");
- const day = String(date.getDate()).padStart(2, "0");
- const hour = String(date.getHours()).padStart(2, "0");
- const minute = String(date.getMinutes()).padStart(2, "0");
- const second = String(date.getSeconds()).padStart(2, "0");
- const fileName = `${year}${month}${day}_${hour}${minute}${second}-${currentPost.authorHandler}.json`;
- console.log("📄 Generierter Dateiname:", fileName);
- const fileContent = JSON.stringify(currentPost, null, 2);
- const blob = new Blob([fileContent], { type: "application/json" });
- const url = URL.createObjectURL(blob);
- const a = document.createElement("a");
- a.href = url;
- a.download = fileName;
- a.style.display = "none";
- document.body.appendChild(a);
- console.log("🔗 Download-Element erstellt:", a);
- try {
- a.click();
- console.log(`💾 Leseposition als Datei gespeichert: ${fileName}`);
- showPopup("downloadSuccess", 5000, { fileName });
- downloadedPosts.add(postKey);
- } catch (clickErr) {
- console.error("❌ Fehler beim Auslösen des Downloads:", clickErr);
- if (!navigator.clipboard) {
- console.error("❌ Clipboard-API nicht verfügbar.");
- showPopup("downloadClipboardFailed", 10000);
- promptManualFallback(currentPost);
- return;
- }
- navigator.clipboard.writeText(fileContent).then(() => {
- console.log("📋 Leseposition in Zwischenablage kopiert.");
- showPopup("downloadFailed", 10000, { fileName });
- downloadedPosts.add(postKey);
- }).catch(clipErr => {
- console.error("❌ Fehler beim Kopieren in die Zwischenablage:", clipErr);
- showPopup("downloadClipboardFailed", 10000);
- promptManualFallback(currentPost);
- });
- }
- setTimeout(() => {
- try {
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- console.log("🧹 Download-Element entfernt und URL freigegeben.");
- } catch (cleanupErr) {
- console.error("❌ Fehler beim Aufräumen:", cleanupErr);
- }
- }, 3000);
- } catch (err) {
- console.error("❌ Fehler beim Speichern der Datei:", err);
- showPopup("downloadClipboardFailed", 5000);
- promptManualFallback(currentPost);
- }
- }
- function loadNewestLastReadPost() {
- return new Promise(resolve => {
- loadLastReadPost(storedPost => {
- if (storedPost && storedPost.timestamp && storedPost.authorHandler) {
- lastReadPost = storedPost;
- console.log("✅ Leseposition geladen:", lastReadPost);
- } else {
- const localPost = JSON.parse(localStorage.getItem("lastReadPost") || "{}");
- if (localPost && localPost.timestamp && localPost.authorHandler) {
- lastReadPost = localPost;
- console.log("✅ Leseposition aus localStorage:", lastReadPost);
- } else {
- console.warn("⚠️ Keine Leseposition gefunden.");
- showPopup("noPositionFound", 5000);
- }
- }
- resolve();
- });
- });
- }
- async function initializeScript() {
- console.log("🔧 Lade Leseposition...");
- try {
- await loadNewestLastReadPost();
- console.log("✅ Initialisierung erfolgreich.");
- window.addEventListener("scroll", () => {
- if (!isScriptActivated) {
- isScriptActivated = true;
- console.log("🛠️ DEBUG: Skript durch Scrollen aktiviert.");
- observeForNewPosts();
- }
- if (isAutoScrolling || isSearching) {
- console.log("⏹️ Scroll-Ereignis ignoriert.");
- return;
- }
- markTopVisiblePost(true);
- }, { passive: true });
- } catch (err) {
- console.error("❌ Fehler bei der Initialisierung:", err);
- showPopup("scriptError", 5000);
- }
- }
- function initializeWhenDOMReady() {
- if (!window.location.href.includes("/home")) {
- console.log("🚫 Skript deaktiviert: Nicht auf der Home-Seite.");
- return;
- }
- console.log("🚀 Initialisiere Skript...");
- const observer = new MutationObserver((mutations, obs) => {
- if (document.body) {
- obs.disconnect();
- initializeScript().then(() => {
- createButtons();
- startPeriodicSave();
- }).catch(err => {
- console.error("❌ Fehler bei der Initialisierung:", err);
- showPopup("scriptError", 5000);
- });
- }
- });
- observer.observe(document.documentElement, { childList: true, subtree: true });
- }
- window.addEventListener("load", initializeWhenDOMReady);
- function loadLastReadPostFromFile() {
- try {
- const input = document.createElement("input");
- input.type = "file";
- input.accept = ".json";
- input.style.display = "none";
- document.body.appendChild(input);
- input.addEventListener("change", (event) => {
- const file = event.target.files[0];
- if (!file) {
- console.warn("⚠️ Keine Datei ausgewählt.");
- showPopup("fileSelectError", 5000);
- document.body.removeChild(input);
- return;
- }
- const reader = new FileReader();
- reader.onload = (e) => {
- try {
- const data = JSON.parse(e.target.result);
- if (!data || typeof data !== "object" || !data.timestamp || !data.authorHandler) {
- console.warn("⚠️ Ungültige oder unvollständige Leseposition in der Datei:", data);
- showPopup("invalidPosition", 5000);
- document.body.removeChild(input);
- return;
- }
- lastReadPost = data;
- saveLastReadPost(data);
- console.log("✅ Leseposition aus Datei geladen:", lastReadPost);
- showPopup("fileLoadSuccess", 3000);
- // Skript aktivieren und Suche starten
- if (!isScriptActivated) {
- isScriptActivated = true;
- console.log("🛠️ DEBUG: Skript durch Import aktiviert.");
- observeForNewPosts();
- }
- startRefinedSearchForLastReadPost();
- } catch (err) {
- console.error("❌ Fehler beim Parsen der Datei:", err);
- showPopup("fileReadError", 5000);
- document.body.removeChild(input);
- }
- };
- reader.readAsText(file);
- });
- input.click();
- } catch (err) {
- console.error("❌ Fehler beim Öffnen des Datei-Dialogs:", err);
- showPopup("fileDialogError", 5000);
- }
- }
- function startPeriodicSave() {
- setInterval(() => {
- if (lastReadPost && isScriptActivated) {
- loadLastReadPost(existingPost => {
- if (!existingPost || new Date(lastReadPost.timestamp) > new Date(existingPost.timestamp) ||
- (lastReadPost.timestamp === existingPost.timestamp && lastReadPost.authorHandler !== existingPost.authorHandler)) {
- saveLastReadPost(lastReadPost);
- console.log("💾 Periodische Speicherung: Neue Leseposition gespeichert:", lastReadPost);
- } else {
- console.log("⏹️ Periodische Speicherung übersprungen: Leseposition nicht neuer oder identisch.");
- }
- });
- }
- }, 30000);
- }
- function markTopVisiblePost(save = true) {
- const topPost = getTopVisiblePost();
- if (!topPost) {
- console.log("❌ Kein sichtbarer Beitrag.");
- return;
- }
- const postTimestamp = getPostTimestamp(topPost);
- const postAuthorHandler = getPostAuthorHandler(topPost);
- if (postTimestamp && postAuthorHandler) {
- const newPost = { timestamp: postTimestamp, authorHandler: postAuthorHandler };
- console.log("🛠️ DEBUG: Versuche, Leseposition zu speichern:", newPost);
- if (lastHighlightedPost && lastHighlightedPost !== topPost) {
- lastHighlightedPost.style.boxShadow = "none";
- }
- topPost.style.boxShadow = "0 0 20px 10px rgba(246, 146, 25, 0.9)";
- lastHighlightedPost = topPost;
- currentPost = newPost;
- if (save && isScriptActivated) {
- loadLastReadPost(existingPost => {
- console.log("🛠️ DEBUG: markTopVisiblePost - newPost:", newPost, "existingPost:", existingPost);
- if (!existingPost ||
- new Date(postTimestamp) > new Date(existingPost.timestamp) ||
- (postTimestamp === existingPost.timestamp && postAuthorHandler !== existingPost.authorHandler)) {
- lastReadPost = newPost;
- console.log("💾 Neue Leseposition gesetzt:", lastReadPost);
- saveLastReadPost(lastReadPost);
- } else {
- console.log("⏹️ Interne Speicherung übersprungen: Leseposition nicht neuer oder identisch.");
- }
- });
- }
- }
- }
- function getTopVisiblePost() {
- const posts = Array.from(document.querySelectorAll("article"));
- return posts.find(post => {
- const rect = post.getBoundingClientRect();
- return rect.top >= 0 && rect.bottom > 0;
- });
- }
- function getPostTimestamp(post) {
- const timeElement = post.querySelector("time");
- if (!timeElement) {
- console.warn("⚠️ Zeitstempel-Element nicht gefunden für Beitrag:", post);
- return null;
- }
- return timeElement.getAttribute("datetime");
- }
- function getPostAuthorHandler(post) {
- const handlerElement = post.querySelector('[role="link"][href*="/"]');
- if (!handlerElement) {
- console.warn("⚠️ Autoren-Handle-Element nicht gefunden für Beitrag:", post);
- return null;
- }
- return handlerElement.getAttribute("href").slice(1);
- }
- function startRefinedSearchForLastReadPost() {
- if (!isScriptActivated) {
- console.log("⏹️ Suche abgebrochen: Skript nicht aktiviert.");
- showPopup("searchScrollPrompt", 5000);
- return;
- }
- loadLastReadPost(storedData => {
- if (!storedData) {
- const localData = JSON.parse(localStorage.getItem("lastReadPost") || "{}");
- if (localData && localData.timestamp && localData.authorHandler) {
- lastReadPost = localData;
- } else {
- console.log("❌ Keine Leseposition gefunden.");
- showPopup("searchNoPosition", 5000);
- return;
- }
- } else {
- lastReadPost = storedData;
- }
- if (!lastReadPost.timestamp || !lastReadPost.authorHandler) {
- console.log("❌ Ungültige Leseposition:", lastReadPost);
- showPopup("invalidPosition", 5000);
- return;
- }
- console.log("🔍 Starte Suche:", lastReadPost);
- const popup = createSearchPopup();
- let direction = 1;
- let scrollAmount = 2000;
- let previousScrollY = -1;
- let searchAttempts = 0;
- let stagnantScrollCount = 0;
- const maxAttempts = 50;
- function handleSpaceKey(event) {
- if (event.code === "Space") {
- console.log("⏹️ Suche gestoppt.");
- isSearching = false;
- popup.remove();
- window.removeEventListener("keydown", handleSpaceKey);
- }
- }
- window.addEventListener("keydown", handleSpaceKey);
- const search = () => {
- if (!isSearching || searchAttempts >= maxAttempts) {
- console.log("⏹️ Suche beendet: Max Versuche oder abgebrochen.");
- isSearching = false;
- popup.remove();
- window.removeEventListener("keydown", handleSpaceKey);
- return;
- }
- const visiblePosts = getVisiblePosts();
- const comparison = compareVisiblePostsToLastReadPost(visiblePosts);
- if (comparison === "match") {
- const matchedPost = findPostByData(lastReadPost);
- if (matchedPost) {
- console.log("🎯 Beitrag gefunden:", lastReadPost);
- isAutoScrolling = true;
- scrollToPostWithHighlight(matchedPost);
- isSearching = false;
- popup.remove();
- window.removeEventListener("keydown", handleSpaceKey);
- return;
- }
- } else if (comparison === "older") {
- direction = -1;
- } else if (comparison === "newer") {
- direction = 1;
- }
- if (window.scrollY === previousScrollY) {
- stagnantScrollCount++;
- if (stagnantScrollCount >= 3) {
- console.log("⏹️ Suche abgebrochen: Scroll-Position stagniert.");
- isSearching = false;
- popup.remove();
- window.removeEventListener("keydown", handleSpaceKey);
- return;
- }
- scrollAmount = Math.max(scrollAmount / 2, 500);
- direction = -direction;
- } else {
- stagnantScrollCount = 0;
- scrollAmount = Math.min(scrollAmount * 1.5, 3000);
- }
- previousScrollY = window.scrollY;
- searchAttempts++;
- requestAnimationFrame(() => {
- window.scrollBy({
- top: direction * scrollAmount,
- behavior: "smooth"
- });
- setTimeout(search, 1000);
- });
- };
- isSearching = true;
- search();
- });
- }
- function createSearchPopup() {
- const lang = getUserLanguage();
- const message = getTranslatedMessage('searchPopup', lang);
- const popup = document.createElement("div");
- popup.style.position = "fixed";
- popup.style.top = "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(246, 146, 25, 0.8)";
- popup.style.zIndex = "10000";
- popup.textContent = message;
- document.body.appendChild(popup);
- return popup;
- }
- function compareVisiblePostsToLastReadPost(posts, customPosition = lastReadPost) {
- const validPosts = posts.filter(post => post.timestamp && post.authorHandler);
- if (validPosts.length === 0) {
- console.log("⚠️ Keine sichtbaren Beiträge.");
- return null;
- }
- const lastReadTime = new Date(customPosition.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 === customPosition.timestamp && post.authorHandler === customPosition.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.");
- return;
- }
- isAutoScrolling = true;
- if (lastHighlightedPost && lastHighlightedPost !== post) {
- lastHighlightedPost.style.boxShadow = "none";
- }
- post.style.boxShadow = "0 0 20px 10px rgba(246, 146, 25, 0.9)";
- lastHighlightedPost = post;
- post.scrollIntoView({ behavior: "smooth", block: "center" });
- setTimeout(() => {
- isAutoScrolling = false;
- console.log("✅ Beitrag zentriert.");
- }, 1000);
- }
- function getVisiblePosts() {
- const posts = Array.from(document.querySelectorAll("article"));
- return posts.map(post => ({
- element: post,
- timestamp: getPostTimestamp(post),
- authorHandler: getPostAuthorHandler(post),
- }));
- }
- function observeForNewPosts() {
- let isProcessingIndicator = false;
- const observer = new MutationObserver(() => {
- if (!isScriptActivated) {
- console.log("⏹️ Beobachtung abgebrochen: Skript nicht aktiviert.");
- observer.disconnect();
- return;
- }
- if (window.scrollY <= 1 && !isSearching && !isProcessingIndicator && lastReadPost) {
- const newPostsIndicator = getNewPostsIndicator();
- if (newPostsIndicator) {
- console.log("🆕 Neue Beiträge erkannt.");
- isProcessingIndicator = true;
- clickNewPostsIndicator(newPostsIndicator);
- setTimeout(() => {
- startRefinedSearchForLastReadPost();
- isProcessingIndicator = false;
- }, 2000);
- }
- }
- });
- observer.observe(document.body, {
- childList: true,
- subtree: true,
- });
- window.addEventListener("unload", () => observer.disconnect());
- }
- function getNewPostsIndicator() {
- const buttons = document.querySelectorAll('button[role="button"]');
- for (const button of buttons) {
- const span = button.querySelector('span');
- if (span) {
- const textContent = span.textContent || '';
- const postIndicatorPattern = /^\d+\s*(neue|new)?\s*(Post|Posts|Beitrag|Beiträge|Tweet|Tweets|Publicación|Publications|投稿|게시물|пост|постов|mensagem|mensagens|مشاركة|مشاركات)\b/i;
- if (postIndicatorPattern.test(textContent)) {
- if (!button.dataset.processed) {
- console.log(`🆕 Indikator gefunden: ${textContent}`);
- button.dataset.processed = 'true';
- return button;
- }
- }
- }
- }
- console.log("ℹ️ Kein Beitragsindikator gefunden.");
- return null;
- }
- function clickNewPostsIndicator(indicator) {
- if (!indicator) {
- console.log("⚠️ Kein Indikator gefunden.");
- return;
- }
- console.log("✅ Klicke auf Indikator...");
- try {
- indicator.click();
- console.log("✅ Indikator geklickt.");
- } catch (err) {
- console.error("❌ Fehler beim Klicken:", err);
- }
- }
- 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 createButtons() {
- const observer = new MutationObserver(() => {
- if (document.body) {
- observer.disconnect();
- try {
- const buttonContainer = document.createElement("div");
- buttonContainer.style.position = "fixed";
- buttonContainer.style.top = "10px";
- buttonContainer.style.left = "10px";
- buttonContainer.style.zIndex = "10000";
- buttonContainer.style.display = "flex";
- buttonContainer.style.flexDirection = "column";
- buttonContainer.style.alignItems = "flex-start";
- buttonContainer.style.visibility = "visible";
- const buttonsConfig = [
- {
- icon: "🔍",
- title: "Start manual search",
- onClick: () => {
- console.log("🔍 Manuelle Suche gestartet.");
- if (!isScriptActivated) {
- isScriptActivated = true;
- console.log("🛠️ DEBUG: Skript durch Lupen-Klick aktiviert.");
- observeForNewPosts();
- }
- startRefinedSearchForLastReadPost();
- },
- },
- {
- icon: "📂",
- title: "Load last read position from file",
- onClick: () => {
- console.log("📂 Lade Leseposition aus Datei...");
- loadLastReadPostFromFile();
- },
- },
- {
- icon: "💾",
- title: "Download current read position",
- onClick: () => {
- console.log("💾 Starte manuellen Download der Leseposition...");
- downloadLastReadPost();
- },
- },
- ];
- buttonsConfig.forEach(({ icon, title, onClick }) => {
- const button = createButton(icon, title, onClick);
- buttonContainer.appendChild(button);
- });
- document.body.appendChild(buttonContainer);
- console.log("🛠️ DEBUG: Button-Container erstellt:", buttonContainer);
- } catch (err) {
- console.error("❌ Fehler beim Erstellen der Buttons:", err);
- showPopup("buttonsError", 5000);
- }
- }
- });
- observer.observe(document.documentElement, { childList: true, subtree: true });
- }
- function createButton(icon, title, onClick) {
- const button = document.createElement("div");
- button.style.width = "27px";
- button.style.height = "27px";
- button.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 = "14px";
- button.style.boxShadow = "0 0 8px rgba(255, 255, 255, 0.5)";
- button.style.transition = "transform 0.2s, box-shadow 0.3s";
- button.style.zIndex = "10001";
- button.style.marginBottom = "8px";
- button.textContent = icon;
- button.title = title;
- button.addEventListener("click", () => {
- button.style.boxShadow = "0 0 15px rgba(255, 255, 255, 0.8)";
- button.style.transform = "scale(0.9)";
- setTimeout(() => {
- button.style.boxShadow = "0 0 8px rgba(255, 255, 255, 0.5)";
- button.style.transform = "scale(1)";
- }, 300);
- onClick();
- });
- return button;
- }
- function showPopup(messageKey, duration = 3000, params = {}) {
- const lang = getUserLanguage();
- const message = getTranslatedMessage(messageKey, lang, params);
- const popup = document.createElement("div");
- popup.style.position = "fixed";
- popup.style.top = "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(246, 146, 25, 0.8)";
- popup.style.zIndex = "10000";
- popup.style.maxWidth = "500px";
- popup.style.whiteSpace = "pre-wrap";
- popup.textContent = message;
- document.body.appendChild(popup);
- setTimeout(() => {
- popup.remove();
- }, duration);
- }
- })();