Twitter/X Timeline Sync

跟踪并同步您在 Twitter/X 上的最后阅读位置,提供手动和自动选项。完美解决在查看新帖子时不丢失当前位置的问题。

  1. // ==UserScript==
  2. // @name Twitter/X Timeline Sync
  3. // @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.
  4. // @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.
  5. // @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.
  6. // @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.
  7. // @description:zh-CN 跟踪并同步您在 Twitter/X 上的最后阅读位置,提供手动和自动选项。完美解决在查看新帖子时不丢失当前位置的问题。
  8. // @description:ru Отслеживает и синхронизирует вашу последнюю позицию чтения на Twitter/X с ручными и автоматическими опциями. Идеально подходит для просмотра новых постов без потери текущей позиции。
  9. // @description:ja Twitter/X での最後の読み取り位置を追跡して同期します。手動および自動オプションを提供します。新しい投稿を見逃さずに現在の位置を維持するのに最適です。
  10. // @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。
  11. // @description:hi Twitter/X पर आपकी अंतिम पठन स्थिति को ट्रैक और सिंक करता है, मैनुअल और स्वचालित विकल्पों के साथ। नई पोस्ट देखते समय अपनी वर्तमान स्थिति को खोए बिना इसे ट्रैक करें।
  12. // @description:ar يتتبع ويزامن آخر موضع قراءة لك على Twitter/X، مع خيارات يدوية وتلقائية. مثالي لتتبع المشاركات الجديدة دون فقدان موضعك الحالي。
  13. // @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。
  14. // @description:ko Twitter/X에서 마지막 읽기 위치를 추적하고 동기화합니다. 수동 및 자동 옵션 포함. 새로운 게시물을 확인하면서 현재 위치를 잃지 않도록 이상적입니다。
  15. // @icon https://x.com/favicon.ico
  16. // @namespace http://tampermonkey.net/
  17. // @version 2025.6.9
  18. // @author Copiis
  19. // @license MIT
  20. // @match https://x.com/home
  21. // @grant GM_setValue
  22. // @grant GM_getValue
  23. // ==/UserScript==
  24. // If you find this script useful and would like to support my work, consider making a small donation!
  25. // Bitcoin (BTC): bc1quc5mkudlwwkktzhvzw5u2nruxyepef957p68r7
  26. // PayPal: https://www.paypal.com/paypalme/Coopiis?country.x=DE&locale.x=de_DE
  27.  
  28. (function () {
  29. 'use strict';
  30.  
  31. // Übersetzungen für alle Popup-Nachrichten
  32. const translations = {
  33. en: {
  34. noValidPosition: "❌ No valid reading position to download.",
  35. alreadyDownloaded: "ℹ️ This reading position has already been downloaded.",
  36. downloadSuccess: "✅ Reading position downloaded as {fileName}.",
  37. downloadFailed: "❌ Download failed. Reading position copied to clipboard. Please paste it into a .json file manually.",
  38. downloadClipboardFailed: "❌ Download and clipboard copy failed. Please save manually.",
  39. noPositionFound: "ℹ️ Scroll to set a reading position.",
  40. scriptError: "❌ Error loading the script.",
  41. invalidPosition: "❌ Invalid reading position.",
  42. fileSelectError: "❌ Please select a JSON file.",
  43. fileReadError: "❌ Error reading the file.",
  44. fileDialogError: "❌ Error opening file dialog.",
  45. fileLoadSuccess: "✅ Reading position successfully loaded!",
  46. buttonsError: "❌ Error displaying buttons.",
  47. searchPopup: "🔍 Searching... Press SPACE to cancel.",
  48. searchNoPosition: "❌ No reading position available.",
  49. searchScrollPrompt: "ℹ️ Please scroll or click the magnifier."
  50. },
  51. de: {
  52. noValidPosition: "❌ Keine gültige Leseposition zum Downloaden.",
  53. alreadyDownloaded: "ℹ️ Diese Leseposition wurde bereits heruntergeladen.",
  54. downloadSuccess: "✅ Leseposition als {fileName} heruntergeladen.",
  55. downloadFailed: "❌ Download fehlgeschlagen. Leseposition wurde in die Zwischenablage kopiert. Bitte manuell in eine .json-Datei einfügen.",
  56. downloadClipboardFailed: "❌ Download und Kopieren fehlgeschlagen. Bitte manuell speichern.",
  57. noPositionFound: "ℹ️ Scrolle, um eine Leseposition zu setzen.",
  58. scriptError: "❌ Fehler beim Laden des Skripts.",
  59. invalidPosition: "❌ Ungültige Leseposition.",
  60. fileSelectError: "❌ Bitte wähle eine JSON-Datei aus.",
  61. fileReadError: "❌ Fehler beim Lesen der Datei.",
  62. fileDialogError: "❌ Fehler beim Öffnen des Datei-Dialogs.",
  63. fileLoadSuccess: "✅ Leseposition erfolgreich geladen!",
  64. buttonsError: "❌ Fehler beim Anzeigen der Buttons.",
  65. searchPopup: "🔍 Suche läuft... Drücke LEERTASTE zum Abbrechen.",
  66. searchNoPosition: "❌ Keine Leseposition vorhanden.",
  67. searchScrollPrompt: "ℹ️ Bitte scrollen oder Lupe klicken."
  68. },
  69. es: {
  70. noValidPosition: "❌ No hay posición de lectura válida para descargar.",
  71. alreadyDownloaded: "ℹ️ Esta posición de lectura ya ha sido descargada.",
  72. downloadSuccess: "✅ Posición de lectura descargada como {fileName}.",
  73. downloadFailed: "❌ Falló la descarga. La posición de lectura se copió al portapapeles. Pégala manualmente en un archivo .json.",
  74. downloadClipboardFailed: "❌ Falló la descarga y la copia al portapapeles. Por favor, guarda manualmente.",
  75. noPositionFound: "ℹ️ Desplázate para establecer una posición de lectura.",
  76. scriptError: "❌ Error al cargar el script.",
  77. invalidPosition: "❌ Posición de lectura no válida.",
  78. fileSelectError: "❌ Por favor, selecciona un archivo JSON.",
  79. fileReadError: "❌ Error al leer el archivo.",
  80. fileDialogError: "❌ Error al abrir el diálogo de archivo.",
  81. fileLoadSuccess: "✅ ¡Posición de lectura cargada con éxito!",
  82. buttonsError: "❌ Error al mostrar los botones.",
  83. searchPopup: "🔍 Buscando... Presiona ESPACIO para cancelar.",
  84. searchNoPosition: "❌ No hay posición de lectura disponible.",
  85. searchScrollPrompt: "ℹ️ Por favor, desplázate o haz clic en la lupa."
  86. },
  87. fr: {
  88. noValidPosition: "❌ Aucune position de lecture valide à télécharger.",
  89. alreadyDownloaded: "ℹ️ Cette position de lecture a déjà été téléchargée.",
  90. downloadSuccess: "✅ Position de lecture téléchargée sous {fileName}.",
  91. downloadFailed: "❌ Échec du téléchargement. Position de lecture copiée dans le presse-papiers. Veuillez la coller manuellement dans un fichier .json.",
  92. downloadClipboardFailed: "❌ Échec du téléchargement et de la copie dans le presse-papiers. Veuillez sauvegarder manuellement.",
  93. noPositionFound: "ℹ️ Faites défiler pour définir une position de lecture.",
  94. scriptError: "❌ Erreur lors du chargement du script.",
  95. invalidPosition: "❌ Position de lecture invalide.",
  96. fileSelectError: "❌ Veuillez sélectionner un fichier JSON.",
  97. fileReadError: "❌ Erreur lors de la lecture du fichier.",
  98. fileDialogError: "❌ Erreur lors de l'ouverture de la boîte de dialogue.",
  99. fileLoadSuccess: "✅ Position de lecture chargée avec succès !",
  100. buttonsError: "❌ Erreur lors de l'affichage des boutons.",
  101. searchPopup: "🔍 Recherche en cours... Appuyez sur ESPACE pour annuler.",
  102. searchNoPosition: "❌ Aucune position de lecture disponible.",
  103. searchScrollPrompt: "ℹ️ Veuillez faire défiler ou cliquer sur la loupe."
  104. },
  105. 'zh-CN': {
  106. noValidPosition: "❌ 没有有效的阅读位置可以下载。",
  107. alreadyDownloaded: "ℹ️ 此阅读位置已下载。",
  108. downloadSuccess: "✅ 阅读位置已下载为 {fileName}。",
  109. downloadFailed: "❌ 下载失败。阅读位置已复制到剪贴板。请手动粘贴到 .json 文件中。",
  110. downloadClipboardFailed: "❌ 下载和剪贴板复制失败。请手动保存。",
  111. noPositionFound: "ℹ️ 滚动以设置阅读位置。",
  112. scriptError: "❌ 加载脚本时出错。",
  113. invalidPosition: "❌ 无效的阅读位置。",
  114. fileSelectError: "❌ 请选择一个 JSON 文件。",
  115. fileReadError: "❌ 读取文件时出错。",
  116. fileDialogError: "❌ 打开文件对话框时出错。",
  117. fileLoadSuccess: "✅ 阅读位置加载成功!",
  118. buttonsError: "❌ 显示按钮时出错。",
  119. searchPopup: "🔍 正在搜索... 按空格键取消。",
  120. searchNoPosition: "❌ 没有可用的阅读位置。",
  121. searchScrollPrompt: "ℹ️ 请滚动或点击放大镜。"
  122. },
  123. ru: {
  124. noValidPosition: "❌ Нет действительной позиции чтения для загрузки.",
  125. alreadyDownloaded: "ℹ️ Эта позиция чтения уже была загружена.",
  126. downloadSuccess: "✅ Позиция чтения загружена как {fileName}.",
  127. downloadFailed: "❌ Не удалось выполнить загрузку. Позиция чтения скопирована в буфер обмена. Пожалуйста, вставьте вручную в файл .json.",
  128. downloadClipboardFailed: "❌ Не удалось выполнить загрузку и копирование в буфер обмена. Пожалуйста, сохраните вручную.",
  129. noPositionFound: "ℹ️ Прокрутите, чтобы установить позицию чтения.",
  130. scriptError: "❌ Ошибка при загрузке скрипта.",
  131. invalidPosition: "❌ Недействительная позиция чтения.",
  132. fileSelectError: "❌ Пожалуйста, выберите файл JSON.",
  133. fileReadError: "❌ Ошибка при чтении файла.",
  134. fileDialogError: "❌ Ошибка при открытии диалогового окна.",
  135. fileLoadSuccess: "✅ Позиция чтения успешно загружена!",
  136. buttonsError: "❌ Ошибка при отображении кнопок.",
  137. searchPopup: "🔍 Поиск... Нажмите ПРОБЕЛ для отмены.",
  138. searchNoPosition: "❌ Позиция чтения недоступна.",
  139. searchScrollPrompt: "ℹ️ Прокрутите или нажмите на лупу."
  140. },
  141. ja: {
  142. noValidPosition: "❌ ダウンロードする有効な読み取り位置がありません。",
  143. alreadyDownloaded: "ℹ️ この読み取り位置はすでにダウンロードされています。",
  144. downloadSuccess: "✅ 読み取り位置が{fileName}としてダウンロードされました。",
  145. downloadFailed: "❌ ダウンロードに失敗しました。読み取り位置がクリップボードにコピーされました。手動で.jsonファイルに貼り付けてください。",
  146. downloadClipboardFailed: "❌ ダウンロードおよびクリップボードへのコピーに失敗しました。手動で保存してください。",
  147. noPositionFound: "ℹ️ スクロールして読み取り位置を設定してください。",
  148. scriptError: "❌ スクリプトの読み込み中にエラーが発生しました。",
  149. invalidPosition: "❌ 無効な読み取り位置です。",
  150. fileSelectError: "❌ JSONファイルを選択してください。",
  151. fileReadError: "❌ ファイルの読み込み中にエラーが発生しました。",
  152. fileDialogError: "❌ ファイルダイアログのオープン中にエラーが発生しました。",
  153. fileLoadSuccess: "✅ 読み取り位置が正常にロードされました!",
  154. buttonsError: "❌ ボタンの表示中にエラーが発生しました。",
  155. searchPopup: "🔍 検索中... スペースキーを押してキャンセル。",
  156. searchNoPosition: "❌ 読み取り位置がありません。",
  157. searchScrollPrompt: "ℹ️ スクロールするか、虫眼鏡をクリックしてください。"
  158. },
  159. 'pt-BR': {
  160. noValidPosition: "❌ Nenhuma posição de leitura válida para download.",
  161. alreadyDownloaded: "ℹ️ Esta posição de leitura já foi baixada.",
  162. downloadSuccess: "✅ Posição de leitura baixada como {fileName}.",
  163. downloadFailed: "❌ Falha no download. Posição de leitura copiada para a área de transferência. Cole manualmente em um arquivo .json.",
  164. downloadClipboardFailed: "❌ Falha no download e na cópia para a área de transferência. Por favor, salve manualmente.",
  165. noPositionFound: "ℹ️ Role para definir uma posição de leitura.",
  166. scriptError: "❌ Erro ao carregar o script.",
  167. invalidPosition: "❌ Posição de leitura inválida.",
  168. fileSelectError: "❌ Por favor, selecione um arquivo JSON.",
  169. fileReadError: "❌ Erro ao ler o arquivo.",
  170. fileDialogError: "❌ Erro ao abrir o diálogo de arquivo.",
  171. fileLoadSuccess: "✅ Posição de leitura carregada com sucesso!",
  172. buttonsError: "❌ Erro ao exibir os botões.",
  173. searchPopup: "🔍 Pesquisando... Pressione ESPAÇO para cancelar.",
  174. searchNoPosition: "❌ Nenhuma posição de leitura disponível.",
  175. searchScrollPrompt: "ℹ️ Role ou clique na lupa."
  176. },
  177. hi: {
  178. noValidPosition: "❌ डाउनलोड करने के लिए कोई वैध पढ़ने की स्थिति नहीं है।",
  179. alreadyDownloaded: "ℹ️ यह पढ़ने की स्थिति पहले ही डाउनलोड की जा चुकी है।",
  180. downloadSuccess: "✅ पढ़ने की स्थिति {fileName} के रूप में डाउनलोड की गई।",
  181. downloadFailed: "❌ डाउनलोड विफल। पढ़ने की स्थिति क्लिपबोर्ड में कॉपी की गई है। कृपया इसे मैन्युअल रूप से .json फ़ाइल में पेस्ट करें।",
  182. downloadClipboardFailed: "❌ डाउनलोड और क्लिपबोर्ड कॉपी विफल। कृपया मैन्युअल रूप से सहेजें।",
  183. noPositionFound: "ℹ️ पढ़ने की स्थिति सेट करने के लिए स्क्रॉल करें।",
  184. scriptError: "❌ स्क्रिप्ट लोड करने में त्रुटि।",
  185. invalidPosition: "❌ अमान्य पढ़ने की स्थिति।",
  186. fileSelectError: "❌ कृपया एक JSON फ़ाइल चुनें।",
  187. fileReadError: "❌ फ़ाइल पढ़ने में त्रुटि।",
  188. fileDialogError: "❌ फ़ाइल डायलॉग खोलने में त्रुटि।",
  189. fileLoadSuccess: "✅ पढ़ने की स्थिति सफलतापूर्वक लोड की गई!",
  190. buttonsError: "❌ बटनों को प्रदर्शित करने में त्रुटि।",
  191. searchPopup: "🔍 खोज चल रही है... रद्द करने के लिए स्पेस दबाएं।",
  192. searchNoPosition: "❌ कोई पढ़ने की स्थिति उपलब्ध नहीं है।",
  193. searchScrollPrompt: "ℹ️ कृपया स्क्रॉल करें या मैग्नीफायर पर क्लिक करें।"
  194. },
  195. ar: {
  196. noValidPosition: "❌ لا توجد مواضع قراءة صالحة للتحميل.",
  197. alreadyDownloaded: "ℹ️ تم تحميل موضع القراءة هذا بالفعل.",
  198. downloadSuccess: "✅ تم تحميل موضع القراءة باسم {fileName}.",
  199. downloadFailed: "❌ فشل التحميل. تم نسخ موضع القراءة إلى الحافظة. يرجى لصقه يدويًا في ملف .json.",
  200. downloadClipboardFailed: "❌ فشل التحميل والنسخ إلى الحافظة. يرجى الحفظ يدويًا.",
  201. noPositionFound: "ℹ️ قم بالتمرير لتحديد موضع القراءة.",
  202. scriptError: "❌ خطأ أثناء تحميل السكربت.",
  203. invalidPosition: "❌ موضع قراءة غير صالح.",
  204. fileSelectError: "❌ يرجى اختيار ملف JSON.",
  205. fileReadError: "❌ خطأ أثناء قراءة الملف.",
  206. fileDialogError: "❌ خطأ أثناء فتح حوار الملف.",
  207. fileLoadSuccess: "✅ تم تحميل موضع القراءة بنجاح!",
  208. buttonsError: "❌ خطأ أثناء عرض الأزرار.",
  209. searchPopup: "🔍 جارٍ البحث... اضغط على مفتاح المسافة للإلغاء.",
  210. searchNoPosition: "❌ لا يوجد موضع قراءة متاح.",
  211. searchScrollPrompt: "ℹ️ يرجى التمرير أو النقر على العدسة المكبرة."
  212. },
  213. it: {
  214. noValidPosition: "❌ Nessuna posizione di lettura valida da scaricare.",
  215. alreadyDownloaded: "ℹ️ Questa posizione di lettura è già stata scaricata.",
  216. downloadSuccess: "✅ Posizione di lettura scaricata come {fileName}.",
  217. downloadFailed: "❌ Download fallito. Posizione di lettura copiata negli appunti. Incollala manualmente in un file .json.",
  218. downloadClipboardFailed: "❌ Download e copia negli appunti falliti. Salva manualmente.",
  219. noPositionFound: "ℹ️ Scorri per impostare una posizione di lettura.",
  220. scriptError: "❌ Errore durante il caricamento dello script.",
  221. invalidPosition: "❌ Posizione di lettura non valida.",
  222. fileSelectError: "❌ Seleziona un file JSON.",
  223. fileReadError: "❌ Errore durante la lettura del file.",
  224. fileDialogError: "❌ Errore durante l'apertura della finestra di dialogo.",
  225. fileLoadSuccess: "✅ Posizione di lettura caricata con successo!",
  226. buttonsError: "❌ Errore durante la visualizzazione dei pulsanti.",
  227. searchPopup: "🔍 Ricerca in corso... Premi SPAZIO per annullare.",
  228. searchNoPosition: "❌ Nessuna posizione di lettura disponibile.",
  229. searchScrollPrompt: "ℹ️ Scorri o fai clic sulla lente d'ingrandimento."
  230. },
  231. ko: {
  232. noValidPosition: "❌ 다운로드할 유효한 읽기 위치가 없습니다.",
  233. alreadyDownloaded: "ℹ️ 이 읽기 위치는 이미 다운로드되었습니다.",
  234. downloadSuccess: "✅ 읽기 위치가 {fileName}으로 다운로드되었습니다.",
  235. downloadFailed: "❌ 다운로드 실패. 읽기 위치가 클립보드에 복사되었습니다. .json 파일에 수동으로 붙여넣으세요.",
  236. downloadClipboardFailed: "❌ 다운로드 및 클립보드 복사 실패. 수동으로 저장하세요.",
  237. noPositionFound: "ℹ️ 읽기 위치를 설정하려면 스크롤하세요.",
  238. scriptError: "❌ 스크립트 로드 중 오류가 발생했습니다.",
  239. invalidPosition: "❌ 유효하지 않은 읽기 위치입니다.",
  240. fileSelectError: "❌ JSON 파일을 선택하세요.",
  241. fileReadError: "❌ 파일 읽기 중 오류가 발생했습니다.",
  242. fileDialogError: "❌ 파일 대화 상자를 여는 중 오류가 발생했습니다.",
  243. fileLoadSuccess: "✅ 읽기 위치가 성공적으로 로드되었습니다!",
  244. buttonsError: "❌ 버튼 표시 중 오류가 발생했습니다.",
  245. searchPopup: "🔍 검색 중... 취소하려면 스페이스바를 누르세요.",
  246. searchNoPosition: "❌ 사용 가능한 읽기 위치가 없습니다.",
  247. searchScrollPrompt: "ℹ️ 스크롤하거나 돋보기를 클릭하세요."
  248. }
  249. };
  250.  
  251. // Funktion zur Erkennung der Benutzersprache
  252. function getUserLanguage() {
  253. const lang = navigator.language || navigator.languages[0] || 'en';
  254. const langCode = lang.split('-')[0];
  255. return translations[lang] || translations[langCode] ? lang : 'en';
  256. }
  257.  
  258. // Funktion zum Abrufen der übersetzten Nachricht
  259. function getTranslatedMessage(key, lang, params = {}) {
  260. const translation = translations[lang] || translations['en'];
  261. let message = translation[key] || translations['en'][key] || key;
  262. Object.keys(params).forEach(param => {
  263. message = message.replace(`{${param}}`, params[param]);
  264. });
  265. return message;
  266. }
  267.  
  268. let lastReadPost = null;
  269. let isAutoScrolling = false;
  270. let isSearching = false;
  271. let isScriptActivated = false;
  272. let currentPost = null;
  273. let lastHighlightedPost = null;
  274. const downloadedPosts = new Set();
  275.  
  276. function loadLastReadPost(callback) {
  277. try {
  278. const storedPost = GM_getValue("lastReadPost", null);
  279. if (storedPost) {
  280. const parsedPost = JSON.parse(storedPost);
  281. console.log("🛠️ DEBUG: Geladene Leseposition:", parsedPost);
  282. callback(parsedPost);
  283. } else {
  284. console.log("⏹️ Keine gespeicherte Leseposition gefunden.");
  285. callback(null);
  286. }
  287. } catch (err) {
  288. console.error("❌ Fehler beim Laden der Leseposition:", err);
  289. callback(null);
  290. }
  291. }
  292.  
  293. function saveLastReadPost(post) {
  294. if (!post || !post.timestamp || !post.authorHandler) {
  295. console.log("❌ Ungültige Leseposition, Speicherung abgebrochen:", post);
  296. return;
  297. }
  298.  
  299. let attempts = 0;
  300. const maxAttempts = 3;
  301.  
  302. function trySave() {
  303. try {
  304. const postData = JSON.stringify(post);
  305. GM_setValue("lastReadPost", postData);
  306. console.log("💾 Leseposition erfolgreich mit GM_setValue gespeichert:", postData);
  307. localStorage.setItem("lastReadPost", postData);
  308. console.log("💾 Fallback in localStorage gespeichert:", localStorage.getItem("lastReadPost"));
  309. } catch (err) {
  310. attempts++;
  311. console.error(`❌ Fehler beim Speichern der Leseposition (Versuch ${attempts}/${maxAttempts}):`, err);
  312. if (attempts < maxAttempts) {
  313. console.log("🔄 Wiederhole Speicherversuch...");
  314. setTimeout(trySave, 1000);
  315. } else {
  316. console.error("❌ Maximale Speicherversuche erreicht. Fallback auf localStorage.");
  317. localStorage.setItem("lastReadPost", JSON.stringify(post));
  318. promptManualFallback(post);
  319. }
  320. }
  321. }
  322.  
  323. trySave();
  324. }
  325.  
  326. function promptManualFallback(data) {
  327. const content = JSON.stringify(data);
  328. const message = `📝 Neue Leseposition: ${content}\nBitte speichere dies manuell, da der Speichervorgang fehlschlug.`;
  329. showPopup(message, 10000);
  330. console.log("📝 Bitte manuell speichern:", content);
  331. }
  332.  
  333. function downloadLastReadPost() {
  334. try {
  335. if (!currentPost || !currentPost.timestamp || !currentPost.authorHandler) {
  336. console.warn("⚠️ Keine gültige Leseposition zum Speichern:", currentPost);
  337. showPopup("noValidPosition", 5000);
  338. return;
  339. }
  340.  
  341. const postKey = `${currentPost.timestamp}-${currentPost.authorHandler}`;
  342. if (downloadedPosts.has(postKey)) {
  343. console.log("⏹️ Leseposition bereits heruntergeladen:", postKey);
  344. showPopup("alreadyDownloaded", 5000);
  345. return;
  346. }
  347.  
  348. console.log("🛠️ DEBUG: Starte manuellen Download-Prozess für Leseposition:", currentPost);
  349.  
  350. const date = new Date(currentPost.timestamp);
  351. const year = date.getFullYear();
  352. const month = String(date.getMonth() + 1).padStart(2, "0");
  353. const day = String(date.getDate()).padStart(2, "0");
  354. const hour = String(date.getHours()).padStart(2, "0");
  355. const minute = String(date.getMinutes()).padStart(2, "0");
  356. const second = String(date.getSeconds()).padStart(2, "0");
  357. const fileName = `${year}${month}${day}_${hour}${minute}${second}-${currentPost.authorHandler}.json`;
  358.  
  359. console.log("📄 Generierter Dateiname:", fileName);
  360.  
  361. const fileContent = JSON.stringify(currentPost, null, 2);
  362. const blob = new Blob([fileContent], { type: "application/json" });
  363. const url = URL.createObjectURL(blob);
  364. const a = document.createElement("a");
  365. a.href = url;
  366. a.download = fileName;
  367. a.style.display = "none";
  368. document.body.appendChild(a);
  369. console.log("🔗 Download-Element erstellt:", a);
  370.  
  371. try {
  372. a.click();
  373. console.log(`💾 Leseposition als Datei gespeichert: ${fileName}`);
  374. showPopup("downloadSuccess", 5000, { fileName });
  375. downloadedPosts.add(postKey);
  376. } catch (clickErr) {
  377. console.error("❌ Fehler beim Auslösen des Downloads:", clickErr);
  378. if (!navigator.clipboard) {
  379. console.error("❌ Clipboard-API nicht verfügbar.");
  380. showPopup("downloadClipboardFailed", 10000);
  381. promptManualFallback(currentPost);
  382. return;
  383. }
  384. navigator.clipboard.writeText(fileContent).then(() => {
  385. console.log("📋 Leseposition in Zwischenablage kopiert.");
  386. showPopup("downloadFailed", 10000, { fileName });
  387. downloadedPosts.add(postKey);
  388. }).catch(clipErr => {
  389. console.error("❌ Fehler beim Kopieren in die Zwischenablage:", clipErr);
  390. showPopup("downloadClipboardFailed", 10000);
  391. promptManualFallback(currentPost);
  392. });
  393. }
  394.  
  395. setTimeout(() => {
  396. try {
  397. document.body.removeChild(a);
  398. URL.revokeObjectURL(url);
  399. console.log("🧹 Download-Element entfernt und URL freigegeben.");
  400. } catch (cleanupErr) {
  401. console.error("❌ Fehler beim Aufräumen:", cleanupErr);
  402. }
  403. }, 3000);
  404. } catch (err) {
  405. console.error("❌ Fehler beim Speichern der Datei:", err);
  406. showPopup("downloadClipboardFailed", 5000);
  407. promptManualFallback(currentPost);
  408. }
  409. }
  410.  
  411. function loadNewestLastReadPost() {
  412. return new Promise(resolve => {
  413. loadLastReadPost(storedPost => {
  414. if (storedPost && storedPost.timestamp && storedPost.authorHandler) {
  415. lastReadPost = storedPost;
  416. console.log("✅ Leseposition geladen:", lastReadPost);
  417. } else {
  418. const localPost = JSON.parse(localStorage.getItem("lastReadPost") || "{}");
  419. if (localPost && localPost.timestamp && localPost.authorHandler) {
  420. lastReadPost = localPost;
  421. console.log("✅ Leseposition aus localStorage:", lastReadPost);
  422. } else {
  423. console.warn("⚠️ Keine Leseposition gefunden.");
  424. showPopup("noPositionFound", 5000);
  425. }
  426. }
  427. resolve();
  428. });
  429. });
  430. }
  431.  
  432. async function initializeScript() {
  433. console.log("🔧 Lade Leseposition...");
  434. try {
  435. await loadNewestLastReadPost();
  436. console.log("✅ Initialisierung erfolgreich.");
  437.  
  438. window.addEventListener("scroll", () => {
  439. if (!isScriptActivated) {
  440. isScriptActivated = true;
  441. console.log("🛠️ DEBUG: Skript durch Scrollen aktiviert.");
  442. observeForNewPosts();
  443. }
  444.  
  445. if (isAutoScrolling || isSearching) {
  446. console.log("⏹️ Scroll-Ereignis ignoriert.");
  447. return;
  448. }
  449. markTopVisiblePost(true);
  450. }, { passive: true });
  451. } catch (err) {
  452. console.error("❌ Fehler bei der Initialisierung:", err);
  453. showPopup("scriptError", 5000);
  454. }
  455. }
  456.  
  457. function initializeWhenDOMReady() {
  458. if (!window.location.href.includes("/home")) {
  459. console.log("🚫 Skript deaktiviert: Nicht auf der Home-Seite.");
  460. return;
  461. }
  462. console.log("🚀 Initialisiere Skript...");
  463.  
  464. const observer = new MutationObserver((mutations, obs) => {
  465. if (document.body) {
  466. obs.disconnect();
  467. initializeScript().then(() => {
  468. createButtons();
  469. startPeriodicSave();
  470. }).catch(err => {
  471. console.error("❌ Fehler bei der Initialisierung:", err);
  472. showPopup("scriptError", 5000);
  473. });
  474. }
  475. });
  476. observer.observe(document.documentElement, { childList: true, subtree: true });
  477. }
  478.  
  479. window.addEventListener("load", initializeWhenDOMReady);
  480.  
  481. function loadLastReadPostFromFile() {
  482. try {
  483. const input = document.createElement("input");
  484. input.type = "file";
  485. input.accept = ".json";
  486. input.style.display = "none";
  487. document.body.appendChild(input);
  488.  
  489. input.addEventListener("change", (event) => {
  490. const file = event.target.files[0];
  491. if (!file) {
  492. console.warn("⚠️ Keine Datei ausgewählt.");
  493. showPopup("fileSelectError", 5000);
  494. document.body.removeChild(input);
  495. return;
  496. }
  497.  
  498. const reader = new FileReader();
  499. reader.onload = (e) => {
  500. try {
  501. const data = JSON.parse(e.target.result);
  502. if (!data || typeof data !== "object" || !data.timestamp || !data.authorHandler) {
  503. console.warn("⚠️ Ungültige oder unvollständige Leseposition in der Datei:", data);
  504. showPopup("invalidPosition", 5000);
  505. document.body.removeChild(input);
  506. return;
  507. }
  508. lastReadPost = data;
  509. saveLastReadPost(data);
  510. console.log("✅ Leseposition aus Datei geladen:", lastReadPost);
  511. showPopup("fileLoadSuccess", 3000);
  512. // Skript aktivieren und Suche starten
  513. if (!isScriptActivated) {
  514. isScriptActivated = true;
  515. console.log("🛠️ DEBUG: Skript durch Import aktiviert.");
  516. observeForNewPosts();
  517. }
  518. startRefinedSearchForLastReadPost();
  519. } catch (err) {
  520. console.error("❌ Fehler beim Parsen der Datei:", err);
  521. showPopup("fileReadError", 5000);
  522. document.body.removeChild(input);
  523. }
  524. };
  525. reader.readAsText(file);
  526. });
  527.  
  528. input.click();
  529. } catch (err) {
  530. console.error("❌ Fehler beim Öffnen des Datei-Dialogs:", err);
  531. showPopup("fileDialogError", 5000);
  532. }
  533. }
  534.  
  535. function startPeriodicSave() {
  536. setInterval(() => {
  537. if (lastReadPost && isScriptActivated) {
  538. loadLastReadPost(existingPost => {
  539. if (!existingPost || new Date(lastReadPost.timestamp) > new Date(existingPost.timestamp) ||
  540. (lastReadPost.timestamp === existingPost.timestamp && lastReadPost.authorHandler !== existingPost.authorHandler)) {
  541. saveLastReadPost(lastReadPost);
  542. console.log("💾 Periodische Speicherung: Neue Leseposition gespeichert:", lastReadPost);
  543. } else {
  544. console.log("⏹️ Periodische Speicherung übersprungen: Leseposition nicht neuer oder identisch.");
  545. }
  546. });
  547. }
  548. }, 30000);
  549. }
  550.  
  551. function markTopVisiblePost(save = true) {
  552. const topPost = getTopVisiblePost();
  553. if (!topPost) {
  554. console.log("❌ Kein sichtbarer Beitrag.");
  555. return;
  556. }
  557.  
  558. const postTimestamp = getPostTimestamp(topPost);
  559. const postAuthorHandler = getPostAuthorHandler(topPost);
  560.  
  561. if (postTimestamp && postAuthorHandler) {
  562. const newPost = { timestamp: postTimestamp, authorHandler: postAuthorHandler };
  563. console.log("🛠️ DEBUG: Versuche, Leseposition zu speichern:", newPost);
  564.  
  565. if (lastHighlightedPost && lastHighlightedPost !== topPost) {
  566. lastHighlightedPost.style.boxShadow = "none";
  567. }
  568. topPost.style.boxShadow = "0 0 20px 10px rgba(246, 146, 25, 0.9)";
  569. lastHighlightedPost = topPost;
  570. currentPost = newPost;
  571.  
  572. if (save && isScriptActivated) {
  573. loadLastReadPost(existingPost => {
  574. console.log("🛠️ DEBUG: markTopVisiblePost - newPost:", newPost, "existingPost:", existingPost);
  575. if (!existingPost ||
  576. new Date(postTimestamp) > new Date(existingPost.timestamp) ||
  577. (postTimestamp === existingPost.timestamp && postAuthorHandler !== existingPost.authorHandler)) {
  578. lastReadPost = newPost;
  579. console.log("💾 Neue Leseposition gesetzt:", lastReadPost);
  580. saveLastReadPost(lastReadPost);
  581. } else {
  582. console.log("⏹️ Interne Speicherung übersprungen: Leseposition nicht neuer oder identisch.");
  583. }
  584. });
  585. }
  586. }
  587. }
  588.  
  589. function getTopVisiblePost() {
  590. const posts = Array.from(document.querySelectorAll("article"));
  591. return posts.find(post => {
  592. const rect = post.getBoundingClientRect();
  593. return rect.top >= 0 && rect.bottom > 0;
  594. });
  595. }
  596.  
  597. function getPostTimestamp(post) {
  598. const timeElement = post.querySelector("time");
  599. if (!timeElement) {
  600. console.warn("⚠️ Zeitstempel-Element nicht gefunden für Beitrag:", post);
  601. return null;
  602. }
  603. return timeElement.getAttribute("datetime");
  604. }
  605.  
  606. function getPostAuthorHandler(post) {
  607. const handlerElement = post.querySelector('[role="link"][href*="/"]');
  608. if (!handlerElement) {
  609. console.warn("⚠️ Autoren-Handle-Element nicht gefunden für Beitrag:", post);
  610. return null;
  611. }
  612. return handlerElement.getAttribute("href").slice(1);
  613. }
  614.  
  615. function startRefinedSearchForLastReadPost() {
  616. if (!isScriptActivated) {
  617. console.log("⏹️ Suche abgebrochen: Skript nicht aktiviert.");
  618. showPopup("searchScrollPrompt", 5000);
  619. return;
  620. }
  621.  
  622. loadLastReadPost(storedData => {
  623. if (!storedData) {
  624. const localData = JSON.parse(localStorage.getItem("lastReadPost") || "{}");
  625. if (localData && localData.timestamp && localData.authorHandler) {
  626. lastReadPost = localData;
  627. } else {
  628. console.log("❌ Keine Leseposition gefunden.");
  629. showPopup("searchNoPosition", 5000);
  630. return;
  631. }
  632. } else {
  633. lastReadPost = storedData;
  634. }
  635.  
  636. if (!lastReadPost.timestamp || !lastReadPost.authorHandler) {
  637. console.log("❌ Ungültige Leseposition:", lastReadPost);
  638. showPopup("invalidPosition", 5000);
  639. return;
  640. }
  641.  
  642. console.log("🔍 Starte Suche:", lastReadPost);
  643. const popup = createSearchPopup();
  644.  
  645. let direction = 1;
  646. let scrollAmount = 2000;
  647. let previousScrollY = -1;
  648. let searchAttempts = 0;
  649. let stagnantScrollCount = 0;
  650. const maxAttempts = 50;
  651.  
  652. function handleSpaceKey(event) {
  653. if (event.code === "Space") {
  654. console.log("⏹️ Suche gestoppt.");
  655. isSearching = false;
  656. popup.remove();
  657. window.removeEventListener("keydown", handleSpaceKey);
  658. }
  659. }
  660.  
  661. window.addEventListener("keydown", handleSpaceKey);
  662.  
  663. const search = () => {
  664. if (!isSearching || searchAttempts >= maxAttempts) {
  665. console.log("⏹️ Suche beendet: Max Versuche oder abgebrochen.");
  666. isSearching = false;
  667. popup.remove();
  668. window.removeEventListener("keydown", handleSpaceKey);
  669. return;
  670. }
  671.  
  672. const visiblePosts = getVisiblePosts();
  673. const comparison = compareVisiblePostsToLastReadPost(visiblePosts);
  674.  
  675. if (comparison === "match") {
  676. const matchedPost = findPostByData(lastReadPost);
  677. if (matchedPost) {
  678. console.log("🎯 Beitrag gefunden:", lastReadPost);
  679. isAutoScrolling = true;
  680. scrollToPostWithHighlight(matchedPost);
  681. isSearching = false;
  682. popup.remove();
  683. window.removeEventListener("keydown", handleSpaceKey);
  684. return;
  685. }
  686. } else if (comparison === "older") {
  687. direction = -1;
  688. } else if (comparison === "newer") {
  689. direction = 1;
  690. }
  691.  
  692. if (window.scrollY === previousScrollY) {
  693. stagnantScrollCount++;
  694. if (stagnantScrollCount >= 3) {
  695. console.log("⏹️ Suche abgebrochen: Scroll-Position stagniert.");
  696. isSearching = false;
  697. popup.remove();
  698. window.removeEventListener("keydown", handleSpaceKey);
  699. return;
  700. }
  701. scrollAmount = Math.max(scrollAmount / 2, 500);
  702. direction = -direction;
  703. } else {
  704. stagnantScrollCount = 0;
  705. scrollAmount = Math.min(scrollAmount * 1.5, 3000);
  706. }
  707.  
  708. previousScrollY = window.scrollY;
  709. searchAttempts++;
  710.  
  711. requestAnimationFrame(() => {
  712. window.scrollBy({
  713. top: direction * scrollAmount,
  714. behavior: "smooth"
  715. });
  716. setTimeout(search, 1000);
  717. });
  718. };
  719.  
  720. isSearching = true;
  721. search();
  722. });
  723. }
  724.  
  725. function createSearchPopup() {
  726. const lang = getUserLanguage();
  727. const message = getTranslatedMessage('searchPopup', lang);
  728. const popup = document.createElement("div");
  729. popup.style.position = "fixed";
  730. popup.style.top = "20px";
  731. popup.style.left = "50%";
  732. popup.style.transform = "translateX(-50%)";
  733. popup.style.backgroundColor = "rgba(0, 0, 0, 0.9)";
  734. popup.style.color = "#ffffff";
  735. popup.style.padding = "10px 20px";
  736. popup.style.borderRadius = "8px";
  737. popup.style.fontSize = "14px";
  738. popup.style.boxShadow = "0 0 10px rgba(246, 146, 25, 0.8)";
  739. popup.style.zIndex = "10000";
  740. popup.textContent = message;
  741. document.body.appendChild(popup);
  742. return popup;
  743. }
  744.  
  745. function compareVisiblePostsToLastReadPost(posts, customPosition = lastReadPost) {
  746. const validPosts = posts.filter(post => post.timestamp && post.authorHandler);
  747.  
  748. if (validPosts.length === 0) {
  749. console.log("⚠️ Keine sichtbaren Beiträge.");
  750. return null;
  751. }
  752.  
  753. const lastReadTime = new Date(customPosition.timestamp);
  754.  
  755. const allOlder = validPosts.every(post => new Date(post.timestamp) < lastReadTime);
  756. const allNewer = validPosts.every(post => new Date(post.timestamp) > lastReadTime);
  757.  
  758. if (validPosts.some(post => post.timestamp === customPosition.timestamp && post.authorHandler === customPosition.authorHandler)) {
  759. return "match";
  760. } else if (allOlder) {
  761. return "older";
  762. } else if (allNewer) {
  763. return "newer";
  764. } else {
  765. return "mixed";
  766. }
  767. }
  768.  
  769. function scrollToPostWithHighlight(post) {
  770. if (!post) {
  771. console.log("❌ Kein Beitrag zum Scrollen.");
  772. return;
  773. }
  774.  
  775. isAutoScrolling = true;
  776.  
  777. if (lastHighlightedPost && lastHighlightedPost !== post) {
  778. lastHighlightedPost.style.boxShadow = "none";
  779. }
  780. post.style.boxShadow = "0 0 20px 10px rgba(246, 146, 25, 0.9)";
  781. lastHighlightedPost = post;
  782.  
  783. post.scrollIntoView({ behavior: "smooth", block: "center" });
  784.  
  785. setTimeout(() => {
  786. isAutoScrolling = false;
  787. console.log("✅ Beitrag zentriert.");
  788. }, 1000);
  789. }
  790.  
  791. function getVisiblePosts() {
  792. const posts = Array.from(document.querySelectorAll("article"));
  793. return posts.map(post => ({
  794. element: post,
  795. timestamp: getPostTimestamp(post),
  796. authorHandler: getPostAuthorHandler(post),
  797. }));
  798. }
  799.  
  800. function observeForNewPosts() {
  801. let isProcessingIndicator = false;
  802. const observer = new MutationObserver(() => {
  803. if (!isScriptActivated) {
  804. console.log("⏹️ Beobachtung abgebrochen: Skript nicht aktiviert.");
  805. observer.disconnect();
  806. return;
  807. }
  808.  
  809. if (window.scrollY <= 1 && !isSearching && !isProcessingIndicator && lastReadPost) {
  810. const newPostsIndicator = getNewPostsIndicator();
  811. if (newPostsIndicator) {
  812. console.log("🆕 Neue Beiträge erkannt.");
  813. isProcessingIndicator = true;
  814. clickNewPostsIndicator(newPostsIndicator);
  815. setTimeout(() => {
  816. startRefinedSearchForLastReadPost();
  817. isProcessingIndicator = false;
  818. }, 2000);
  819. }
  820. }
  821. });
  822.  
  823. observer.observe(document.body, {
  824. childList: true,
  825. subtree: true,
  826. });
  827. window.addEventListener("unload", () => observer.disconnect());
  828. }
  829.  
  830. function getNewPostsIndicator() {
  831. const buttons = document.querySelectorAll('button[role="button"]');
  832. for (const button of buttons) {
  833. const span = button.querySelector('span');
  834. if (span) {
  835. const textContent = span.textContent || '';
  836. const postIndicatorPattern = /^\d+\s*(neue|new)?\s*(Post|Posts|Beitrag|Beiträge|Tweet|Tweets|Publicación|Publications|投稿|게시물|пост|постов|mensagem|mensagens|مشاركة|مشاركات)\b/i;
  837. if (postIndicatorPattern.test(textContent)) {
  838. if (!button.dataset.processed) {
  839. console.log(`🆕 Indikator gefunden: ${textContent}`);
  840. button.dataset.processed = 'true';
  841. return button;
  842. }
  843. }
  844. }
  845. }
  846. console.log("ℹ️ Kein Beitragsindikator gefunden.");
  847. return null;
  848. }
  849.  
  850. function clickNewPostsIndicator(indicator) {
  851. if (!indicator) {
  852. console.log("⚠️ Kein Indikator gefunden.");
  853. return;
  854. }
  855.  
  856. console.log("✅ Klicke auf Indikator...");
  857. try {
  858. indicator.click();
  859. console.log("✅ Indikator geklickt.");
  860. } catch (err) {
  861. console.error("❌ Fehler beim Klicken:", err);
  862. }
  863. }
  864.  
  865. function findPostByData(data) {
  866. const posts = Array.from(document.querySelectorAll("article"));
  867. return posts.find(post => {
  868. const postTimestamp = getPostTimestamp(post);
  869. const authorHandler = getPostAuthorHandler(post);
  870. return postTimestamp === data.timestamp && authorHandler === data.authorHandler;
  871. });
  872. }
  873.  
  874. function createButtons() {
  875. const observer = new MutationObserver(() => {
  876. if (document.body) {
  877. observer.disconnect();
  878. try {
  879. const buttonContainer = document.createElement("div");
  880. buttonContainer.style.position = "fixed";
  881. buttonContainer.style.top = "10px";
  882. buttonContainer.style.left = "10px";
  883. buttonContainer.style.zIndex = "10000";
  884. buttonContainer.style.display = "flex";
  885. buttonContainer.style.flexDirection = "column";
  886. buttonContainer.style.alignItems = "flex-start";
  887. buttonContainer.style.visibility = "visible";
  888.  
  889. const buttonsConfig = [
  890. {
  891. icon: "🔍",
  892. title: "Start manual search",
  893. onClick: () => {
  894. console.log("🔍 Manuelle Suche gestartet.");
  895. if (!isScriptActivated) {
  896. isScriptActivated = true;
  897. console.log("🛠️ DEBUG: Skript durch Lupen-Klick aktiviert.");
  898. observeForNewPosts();
  899. }
  900. startRefinedSearchForLastReadPost();
  901. },
  902. },
  903. {
  904. icon: "📂",
  905. title: "Load last read position from file",
  906. onClick: () => {
  907. console.log("📂 Lade Leseposition aus Datei...");
  908. loadLastReadPostFromFile();
  909. },
  910. },
  911. {
  912. icon: "💾",
  913. title: "Download current read position",
  914. onClick: () => {
  915. console.log("💾 Starte manuellen Download der Leseposition...");
  916. downloadLastReadPost();
  917. },
  918. },
  919. ];
  920.  
  921. buttonsConfig.forEach(({ icon, title, onClick }) => {
  922. const button = createButton(icon, title, onClick);
  923. buttonContainer.appendChild(button);
  924. });
  925.  
  926. document.body.appendChild(buttonContainer);
  927. console.log("🛠️ DEBUG: Button-Container erstellt:", buttonContainer);
  928. } catch (err) {
  929. console.error("❌ Fehler beim Erstellen der Buttons:", err);
  930. showPopup("buttonsError", 5000);
  931. }
  932. }
  933. });
  934. observer.observe(document.documentElement, { childList: true, subtree: true });
  935. }
  936.  
  937. function createButton(icon, title, onClick) {
  938. const button = document.createElement("div");
  939. button.style.width = "27px";
  940. button.style.height = "27px";
  941. button.style.backgroundColor = "rgba(0, 0, 0, 0.9)";
  942. button.style.color = "#ffffff";
  943. button.style.borderRadius = "50%";
  944. button.style.display = "flex";
  945. button.style.justifyContent = "center";
  946. button.style.alignItems = "center";
  947. button.style.cursor = "pointer";
  948. button.style.fontSize = "14px";
  949. button.style.boxShadow = "0 0 8px rgba(255, 255, 255, 0.5)";
  950. button.style.transition = "transform 0.2s, box-shadow 0.3s";
  951. button.style.zIndex = "10001";
  952. button.style.marginBottom = "8px";
  953. button.textContent = icon;
  954. button.title = title;
  955.  
  956. button.addEventListener("click", () => {
  957. button.style.boxShadow = "0 0 15px rgba(255, 255, 255, 0.8)";
  958. button.style.transform = "scale(0.9)";
  959. setTimeout(() => {
  960. button.style.boxShadow = "0 0 8px rgba(255, 255, 255, 0.5)";
  961. button.style.transform = "scale(1)";
  962. }, 300);
  963. onClick();
  964. });
  965.  
  966. return button;
  967. }
  968.  
  969. function showPopup(messageKey, duration = 3000, params = {}) {
  970. const lang = getUserLanguage();
  971. const message = getTranslatedMessage(messageKey, lang, params);
  972. const popup = document.createElement("div");
  973. popup.style.position = "fixed";
  974. popup.style.top = "20px";
  975. popup.style.left = "50%";
  976. popup.style.transform = "translateX(-50%)";
  977. popup.style.backgroundColor = "rgba(0, 0, 0, 0.9)";
  978. popup.style.color = "#ffffff";
  979. popup.style.padding = "10px 20px";
  980. popup.style.borderRadius = "8px";
  981. popup.style.fontSize = "14px";
  982. popup.style.boxShadow = "0 0 10px rgba(246, 146, 25, 0.8)";
  983. popup.style.zIndex = "10000";
  984. popup.style.maxWidth = "500px";
  985. popup.style.whiteSpace = "pre-wrap";
  986. popup.textContent = message;
  987.  
  988. document.body.appendChild(popup);
  989.  
  990. setTimeout(() => {
  991. popup.remove();
  992. }, duration);
  993. }
  994. })();