您需要先安装一个扩展,例如 篡改猴、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.7 // @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); 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); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址