Twitter/X Timeline Sync

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.

სკრიპტის ინსტალაცია?
ავტორის შემოთავაზებული სკრიპტი

შეიძლება მოგეწონოს YouTube Video Hider with 🚫 Icon (Transparent).

სკრიპტის ინსტალაცია
  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-04-15
  18. // @author Copiis
  19. // @license MIT
  20. // @match https://x.com/home
  21. // @grant GM_setValue
  22. // @grant GM_getValue
  23. // @grant GM_download
  24. // ==/UserScript==
  25. // If you find this script useful and would like to support my work, consider making a small donation!
  26. // Bitcoin (BTC): bc1quc5mkudlwwkktzhvzw5u2nruxyepef957p68r7
  27. // PayPal: https://www.paypal.com/paypalme/Coopiis?country.x=DE&locale.x=de_DE
  28.  
  29. (function () {
  30. let lastReadPost = null;
  31. let isAutoScrolling = false;
  32. let isSearching = false;
  33. let isTabFocused = true;
  34. let downloadTriggered = false;
  35. let lastDownloadedPost = null;
  36.  
  37. window.onload = async () => {
  38. if (!window.location.href.includes("/home")) {
  39. console.log("🚫 Skript deaktiviert: Nicht auf der Home-Seite.");
  40. return;
  41. }
  42. console.log("🚀 Seite vollständig geladen. Initialisiere Skript...");
  43. await loadNewestLastReadPost();
  44. await initializeScript();
  45. createButtons();
  46. };
  47.  
  48. window.addEventListener("blur", async () => {
  49. isTabFocused = false;
  50. console.log("🌐 Tab nicht mehr fokussiert.");
  51. if (lastReadPost && !downloadTriggered) {
  52. // Prüfe, ob sich die aktuelle Leseposition von der zuletzt heruntergeladenen unterscheidet
  53. if (
  54. !lastDownloadedPost ||
  55. lastDownloadedPost.timestamp !== lastReadPost.timestamp ||
  56. lastDownloadedPost.authorHandler !== lastReadPost.authorHandler
  57. ) {
  58. downloadTriggered = true;
  59. console.log("📥 Speichere und lade aktuelle Leseposition herunter...");
  60. await saveLastReadPostToFile(); // Speichert lokal
  61. await downloadLastReadPost(); // Löst den Download aus
  62. lastDownloadedPost = { ...lastReadPost }; // Aktualisiere die zuletzt heruntergeladene Position
  63. downloadTriggered = false;
  64. } else {
  65. console.log("⏹️ Leseposition ist identisch mit der zuletzt heruntergeladenen. Download übersprungen.");
  66. }
  67. }
  68. });
  69.  
  70. window.addEventListener("focus", () => {
  71. isTabFocused = true;
  72. downloadTriggered = false;
  73. console.log("🟢 Tab wieder fokussiert.");
  74. });
  75.  
  76. async function initializeScript() {
  77. console.log("🔧 Lade Leseposition...");
  78. await loadLastReadPostFromFile();
  79. observeForNewPosts();
  80.  
  81. window.addEventListener("scroll", () => {
  82. if (isAutoScrolling || isSearching) {
  83. console.log("⏹️ Scroll-Ereignis ignoriert (automatischer Modus aktiv).");
  84. return;
  85. }
  86. markTopVisiblePost(true);
  87. }, { passive: true }); // Passiver Listener
  88. }
  89.  
  90. async function downloadLastReadPost() {
  91. if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler) {
  92. console.warn("⚠️ Keine gültige Leseposition zum Herunterladen.");
  93. return;
  94. }
  95.  
  96. // Prüfe, ob die Leseposition bereits heruntergeladen wurde
  97. if (
  98. lastDownloadedPost &&
  99. lastDownloadedPost.timestamp === lastReadPost.timestamp &&
  100. lastDownloadedPost.authorHandler === lastReadPost.authorHandler
  101. ) {
  102. console.log("⏹️ Leseposition ist identisch mit der zuletzt heruntergeladenen. Download übersprungen.");
  103. return;
  104. }
  105.  
  106. try {
  107. const data = JSON.stringify(lastReadPost, null, 2);
  108. const sanitizedHandler = lastReadPost.authorHandler.replace(/[^a-zA-Z0-9-_]/g, "");
  109. const timestamp = new Date(lastReadPost.timestamp).toISOString().replace(/[:.-]/g, "_");
  110. const fileName = `${timestamp}_${sanitizedHandler}.json`;
  111.  
  112. GM_download({
  113. url: `data:application/json;charset=utf-8,${encodeURIComponent(data)}`,
  114. name: fileName,
  115. onload: () => {
  116. console.log(`✅ Leseposition erfolgreich heruntergeladen: ${fileName}`);
  117. // Aktualisiere lastDownloadedPost nach erfolgreichem Download
  118. lastDownloadedPost = { ...lastReadPost };
  119. },
  120. onerror: (err) => console.error("❌ Fehler beim Herunterladen der Leseposition:", err),
  121. });
  122. } catch (error) {
  123. console.error("❌ Fehler beim Herunterladen der Leseposition:", error);
  124. }
  125. }
  126.  
  127. async function loadNewestLastReadPost() {
  128. try {
  129. const localData = GM_getValue("lastReadPost", null);
  130. if (localData) {
  131. lastReadPost = JSON.parse(localData);
  132. console.log("✅ Lokale Leseposition beim Start geladen:", lastReadPost);
  133. } else {
  134. console.warn("⚠️ Keine gespeicherte Leseposition gefunden.");
  135. }
  136. } catch (err) {
  137. console.error("❌ Fehler beim Laden der neuesten Leseposition:", err);
  138. }
  139. }
  140.  
  141. async function loadLastReadPostFromFile() {
  142. try {
  143. const data = GM_getValue("lastReadPost", null);
  144. if (data) {
  145. lastReadPost = JSON.parse(data);
  146. console.log("✅ Leseposition erfolgreich geladen:", lastReadPost);
  147. } else {
  148. console.warn("⚠️ Keine gespeicherte Leseposition gefunden.");
  149. }
  150. } catch (err) {
  151. console.error("❌ Fehler beim Laden der Leseposition:", err);
  152. }
  153. }
  154.  
  155. function markTopVisiblePost(save = true) {
  156. const topPost = getTopVisiblePost();
  157. if (!topPost) {
  158. console.log("❌ Kein oberster sichtbarer Beitrag gefunden.");
  159. return;
  160. }
  161.  
  162. const postTimestamp = getPostTimestamp(topPost);
  163. const authorHandler = getPostAuthorHandler(topPost);
  164.  
  165. if (postTimestamp && authorHandler) {
  166. const newPost = { timestamp: postTimestamp, authorHandler };
  167. const existingData = GM_getValue("lastReadPost", null);
  168. const existingPost = existingData ? JSON.parse(existingData) : null;
  169.  
  170. if (save && (!existingPost || new Date(postTimestamp) > new Date(existingPost.timestamp))) {
  171. lastReadPost = newPost;
  172. console.log("💾 Neue Leseposition erkannt und aktualisiert:", lastReadPost);
  173. if (isTabFocused) {
  174. saveLastReadPostToFile(); // Speichert nur lokal, kein Download
  175. }
  176. }
  177. }
  178. }
  179.  
  180. function getTopVisiblePost() {
  181. const posts = Array.from(document.querySelectorAll("article"));
  182. return posts.find(post => {
  183. const rect = post.getBoundingClientRect();
  184. return rect.top >= 0 && rect.bottom > 0;
  185. });
  186. }
  187.  
  188. function getPostTimestamp(post) {
  189. const timeElement = post.querySelector("time");
  190. return timeElement ? timeElement.getAttribute("datetime") : null;
  191. }
  192.  
  193. function getPostAuthorHandler(post) {
  194. const handlerElement = post.querySelector('[role="link"][href*="/"]');
  195. return handlerElement ? handlerElement.getAttribute("href").slice(1) : null;
  196. }
  197.  
  198. function startRefinedSearchForLastReadPost() {
  199. const storedData = GM_getValue("lastReadPost", null);
  200. if (!storedData) {
  201. console.log("❌ Keine gespeicherte Leseposition gefunden.");
  202. showPopup("❌ Keine gespeicherte Leseposition vorhanden.");
  203. return;
  204. }
  205.  
  206. try {
  207. lastReadPost = JSON.parse(storedData);
  208. if (!lastReadPost.timestamp || !lastReadPost.authorHandler) {
  209. console.log("❌ Gespeicherte Leseposition ist ungültig:", lastReadPost);
  210. showPopup("❌ Ungültige gespeicherte Leseposition.");
  211. return;
  212. }
  213. } catch (err) {
  214. console.error("❌ Fehler beim Parsen der gespeicherten Leseposition:", err);
  215. showPopup("❌ Fehler bei der gespeicherten Leseposition.");
  216. return;
  217. }
  218.  
  219. console.log("🔍 Starte verfeinerte Suche mit gespeicherter Position:", lastReadPost);
  220. const popup = createSearchPopup();
  221.  
  222. let direction = 1;
  223. let scrollAmount = 2000;
  224. let previousScrollY = -1;
  225. let searchAttempts = 0;
  226. const maxAttempts = 50; // Begrenze die maximale Anzahl an Suchversuchen
  227.  
  228. function handleSpaceKey(event) {
  229. if (event.code === "Space") {
  230. console.log("⏹️ Suche manuell gestoppt.");
  231. isSearching = false;
  232. popup.remove();
  233. window.removeEventListener("keydown", handleSpaceKey);
  234. }
  235. }
  236.  
  237. window.addEventListener("keydown", handleSpaceKey);
  238.  
  239. const search = () => {
  240. if (!isSearching || searchAttempts >= maxAttempts) {
  241. console.log("⏹️ Suche beendet: Maximale Versuche erreicht oder abgebrochen.");
  242. isSearching = false;
  243. popup.remove();
  244. window.removeEventListener("keydown", handleSpaceKey);
  245. return;
  246. }
  247.  
  248. const visiblePosts = getVisiblePosts();
  249. const comparison = compareVisiblePostsToLastReadPost(visiblePosts);
  250.  
  251. if (comparison === "match") {
  252. const matchedPost = findPostByData(lastReadPost);
  253. if (matchedPost) {
  254. console.log("🎯 Beitrag gefunden:", lastReadPost);
  255. isAutoScrolling = true;
  256. scrollToPostWithHighlight(matchedPost);
  257. isSearching = false;
  258. popup.remove();
  259. window.removeEventListener("keydown", handleSpaceKey);
  260. return;
  261. }
  262. } else if (comparison === "older") {
  263. direction = -1;
  264. } else if (comparison === "newer") {
  265. direction = 1;
  266. }
  267.  
  268. if (window.scrollY === previousScrollY) {
  269. scrollAmount = Math.max(scrollAmount / 2, 500);
  270. direction = -direction;
  271. } else {
  272. scrollAmount = Math.min(scrollAmount * 1.5, 3000);
  273. }
  274.  
  275. previousScrollY = window.scrollY;
  276. searchAttempts++;
  277.  
  278. // Verwende requestAnimationFrame mit längerer Verzögerung
  279. requestAnimationFrame(() => {
  280. window.scrollBy({
  281. top: direction * scrollAmount,
  282. behavior: "smooth"
  283. });
  284. setTimeout(search, 1000); // Erhöhte Wartezeit
  285. });
  286. };
  287.  
  288. isSearching = true;
  289. search();
  290. }
  291.  
  292. function startRefinedSearchForLastReadPostWithPosition(position) {
  293. if (!position || !position.timestamp || !position.authorHandler) {
  294. console.log("❌ Ungültige Leseposition für Suche:", position);
  295. showPopup("❌ Ungültige Leseposition.");
  296. return;
  297. }
  298.  
  299. console.log("🔍 Starte verfeinerte Suche mit temporärer Position:", position);
  300. const popup = createSearchPopup();
  301.  
  302. let direction = 1;
  303. let scrollAmount = 2000;
  304. let previousScrollY = -1;
  305.  
  306. function handleSpaceKey(event) {
  307. if (event.code === "Space") {
  308. console.log("⏹️ Suche manuell gestoppt.");
  309. isSearching = false;
  310. popup.remove();
  311. window.removeEventListener("keydown", handleSpaceKey);
  312. }
  313. }
  314.  
  315. window.addEventListener("keydown", handleSpaceKey);
  316.  
  317. const search = () => {
  318. if (!isSearching) {
  319. popup.remove();
  320. return;
  321. }
  322.  
  323. const visiblePosts = getVisiblePosts();
  324. const comparison = compareVisiblePostsToLastReadPost(visiblePosts, position);
  325.  
  326. if (comparison === "match") {
  327. const matchedPost = findPostByData(position);
  328. if (matchedPost) {
  329. console.log("🎯 Beitrag gefunden:", position);
  330. isAutoScrolling = true;
  331. scrollToPostWithHighlight(matchedPost);
  332. isSearching = false;
  333. popup.remove();
  334. window.removeEventListener("keydown", handleSpaceKey);
  335. return;
  336. }
  337. } else if (comparison === "older") {
  338. direction = -1;
  339. } else if (comparison === "newer") {
  340. direction = 1;
  341. }
  342.  
  343. if (window.scrollY === previousScrollY) {
  344. scrollAmount = Math.max(scrollAmount / 2, 500);
  345. direction = -direction;
  346. } else {
  347. scrollAmount = Math.min(scrollAmount * 1.5, 3000);
  348. }
  349.  
  350. previousScrollY = window.scrollY;
  351.  
  352. window.scrollBy(0, direction * scrollAmount);
  353.  
  354. setTimeout(search, 300);
  355. };
  356.  
  357. isSearching = true;
  358. search();
  359. }
  360.  
  361. function createSearchPopup() {
  362. const popup = document.createElement("div");
  363. popup.style.position = "fixed";
  364. popup.style.bottom = "20px";
  365. popup.style.left = "50%";
  366. popup.style.transform = "translateX(-50%)";
  367. popup.style.backgroundColor = "rgba(0, 0, 0, 0.9)";
  368. popup.style.color = "#ffffff";
  369. popup.style.padding = "10px 20px";
  370. popup.style.borderRadius = "8px";
  371. popup.style.fontSize = "14px";
  372. popup.style.boxShadow = "0 0 10px rgba(255, 255, 255, 0.8)";
  373. popup.style.zIndex = "10000";
  374. popup.textContent = "🔍 Searching... Press SPACE to cancel.";
  375. document.body.appendChild(popup);
  376. return popup;
  377. }
  378.  
  379. function compareVisiblePostsToLastReadPost(posts, customPosition = lastReadPost) {
  380. const validPosts = posts.filter(post => post.timestamp && post.authorHandler);
  381.  
  382. if (validPosts.length === 0) {
  383. console.log("⚠️ Keine sichtbaren Beiträge gefunden.");
  384. return null;
  385. }
  386.  
  387. const lastReadTime = new Date(customPosition.timestamp);
  388.  
  389. const allOlder = validPosts.every(post => new Date(post.timestamp) < lastReadTime);
  390. const allNewer = validPosts.every(post => new Date(post.timestamp) > lastReadTime);
  391.  
  392. if (validPosts.some(post => post.timestamp === customPosition.timestamp && post.authorHandler === customPosition.authorHandler)) {
  393. return "match";
  394. } else if (allOlder) {
  395. return "older";
  396. } else if (allNewer) {
  397. return "newer";
  398. } else {
  399. return "mixed";
  400. }
  401. }
  402.  
  403. function scrollToPostWithHighlight(post) {
  404. if (!post) {
  405. console.log("❌ Kein Beitrag zum Scrollen gefunden.");
  406. return;
  407. }
  408.  
  409. isAutoScrolling = true;
  410.  
  411. // Entferne bestehende Stile und setze durchgehendes Leuchten
  412. post.style.outline = "none";
  413. post.style.boxShadow = "0 0 20px 10px rgba(255, 215, 0, 0.9)"; // Durchgehendes Leuchten
  414. post.style.animation = "none"; // Entferne die Animation
  415.  
  416. // Entferne das @keyframes glow, falls es existiert
  417. const existingStyle = document.querySelector('#glowStyle');
  418. if (existingStyle) {
  419. existingStyle.remove();
  420. }
  421.  
  422. post.scrollIntoView({ behavior: "smooth", block: "center" });
  423.  
  424. // Entferne das Highlight bei der ersten Scrollaktion
  425. const removeHighlightOnScroll = () => {
  426. if (!isAutoScrolling) {
  427. post.style.boxShadow = "none";
  428. console.log("✅ Highlight entfernt nach manuellem Scroll.");
  429. window.removeEventListener("scroll", removeHighlightOnScroll);
  430. }
  431. };
  432.  
  433. setTimeout(() => {
  434. isAutoScrolling = false;
  435. window.addEventListener("scroll", removeHighlightOnScroll);
  436. console.log("✅ Beitrag zentriert, warte auf manuellen Scroll.");
  437. }, 1000);
  438. }
  439.  
  440. function getVisiblePosts() {
  441. const posts = Array.from(document.querySelectorAll("article"));
  442. return posts.map(post => ({
  443. element: post,
  444. timestamp: getPostTimestamp(post),
  445. authorHandler: getPostAuthorHandler(post),
  446. }));
  447. }
  448.  
  449. async function saveLastReadPostToFile() {
  450. try {
  451. if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler) {
  452. console.warn("⚠️ Keine gültige Leseposition vorhanden. Speichern übersprungen.");
  453. return;
  454. }
  455.  
  456. const existingData = GM_getValue("lastReadPost", null);
  457. if (existingData) {
  458. const existingPost = JSON.parse(existingData);
  459. if (
  460. existingPost.timestamp === lastReadPost.timestamp &&
  461. existingPost.authorHandler === lastReadPost.authorHandler
  462. ) {
  463. console.log("⏹️ Lesestelle ist identisch mit der gespeicherten. Speichern übersprungen.");
  464. return;
  465. }
  466. }
  467.  
  468. GM_setValue("lastReadPost", JSON.stringify(lastReadPost));
  469. console.log("💾 Leseposition erfolgreich lokal gespeichert:", lastReadPost);
  470. } catch (err) {
  471. console.error("❌ Fehler beim Speichern der Leseposition:", err);
  472. }
  473. }
  474.  
  475. async function deleteOldReadingPositions(handler) {
  476. console.log(`🗑️ Ältere Lesestellen für den Handler "${handler}" werden simuliert entfernt.`);
  477. }
  478.  
  479. function observeForNewPosts() {
  480. let isProcessingIndicator = false; // Flag, um Mehrfachverarbeitung zu verhindern
  481.  
  482. const observer = new MutationObserver(() => {
  483. if (window.scrollY <= 1 && !isSearching && !isProcessingIndicator && lastReadPost) {
  484. const newPostsIndicator = getNewPostsIndicator();
  485. if (newPostsIndicator) {
  486. console.log("🆕 Neue Beiträge erkannt. Starte automatische Suche...");
  487. isProcessingIndicator = true;
  488. clickNewPostsIndicator(newPostsIndicator);
  489. setTimeout(() => {
  490. startRefinedSearchForLastReadPost();
  491. isProcessingIndicator = false;
  492. }, 2000); // Erhöhter Timeout für stabileres Laden
  493. }
  494. }
  495. });
  496.  
  497. observer.observe(document.body, {
  498. childList: true,
  499. subtree: true,
  500. });
  501. }
  502.  
  503. function getNewPostsIndicator() {
  504. // Suche nach Buttons, die den Indikator enthalten könnten
  505. const buttons = document.querySelectorAll('button[role="button"]');
  506. for (const button of buttons) {
  507. // Suche nach dem span mit dem Textmuster innerhalb des Buttons
  508. const span = button.querySelector('span.r-poiln3');
  509. if (span) {
  510. const textContent = span.textContent || '';
  511. // Prüfe auf eine Zahl gefolgt von einer Variation von "Posts anzeigen" (international)
  512. const postIndicatorPattern = /^\d+\s*(neue|new)?\s*(Post|Posts|Beitrag|Beiträge|Tweet|Tweets|Publicación|Publications|投稿|게시물|пост|постов|mensagem|mensagens|مشاركة|مشاركات)\b/i;
  513. if (postIndicatorPattern.test(textContent)) {
  514. if (!button.dataset.processed) {
  515. console.log(`🆕 Neuer Beitrags-Indikator gefunden mit Text: "${textContent}"`);
  516. button.dataset.processed = 'true'; // Markiere als verarbeitet
  517. return button;
  518. }
  519. }
  520. }
  521. }
  522. console.log("ℹ️ Kein neuer Beitragsindikator gefunden.");
  523. return null;
  524. }
  525.  
  526. function clickNewPostsIndicator(indicator) {
  527. if (!indicator) {
  528. console.log("⚠️ Kein neuer Beitragsindikator gefunden.");
  529. return;
  530. }
  531.  
  532. console.log("✅ Klicke auf neuen Beitragsindikator...");
  533. try {
  534. indicator.click();
  535. console.log("✅ Neuer Beitragsindikator erfolgreich geklickt.");
  536. } catch (err) {
  537. console.error("❌ Fehler beim Klicken auf den Beitragsindikator:", err);
  538. }
  539. }
  540.  
  541. function findPostByData(data) {
  542. const posts = Array.from(document.querySelectorAll("article"));
  543. return posts.find(post => {
  544. const postTimestamp = getPostTimestamp(post);
  545. const authorHandler = getPostAuthorHandler(post);
  546. return postTimestamp === data.timestamp && authorHandler === data.authorHandler;
  547. });
  548. }
  549.  
  550. function createButtons() {
  551. const buttonContainer = document.createElement("div");
  552. buttonContainer.style.position = "fixed";
  553. buttonContainer.style.top = "50%";
  554. buttonContainer.style.left = "3px";
  555. buttonContainer.style.transform = "translateY(-50%)";
  556. buttonContainer.style.display = "flex";
  557. buttonContainer.style.flexDirection = "column";
  558. buttonContainer.style.gap = "3px";
  559. buttonContainer.style.zIndex = "10000";
  560.  
  561. const buttonsConfig = [
  562. {
  563. icon: "📂",
  564. title: "Load saved position",
  565. onClick: async () => {
  566. await importLastReadPost();
  567. },
  568. },
  569. {
  570. icon: "🔍",
  571. title: "Start manual search",
  572. onClick: () => {
  573. console.log("🔍 Manuelle Suche gestartet.");
  574. startRefinedSearchForLastReadPost();
  575. },
  576. },
  577. ];
  578.  
  579. buttonsConfig.forEach(({ icon, title, onClick }) => {
  580. const button = createButton(icon, title, onClick);
  581. buttonContainer.appendChild(button);
  582. });
  583.  
  584. document.body.appendChild(buttonContainer);
  585. }
  586.  
  587. function createButton(icon, title, onClick) {
  588. const button = document.createElement("div");
  589. button.style.width = "36px";
  590. button.style.height = "36px";
  591. button.style.backgroundColor = "rgba(0, 0, 0, 0.9)";
  592. button.style.color = "#ffffff";
  593. button.style.borderRadius = "50%";
  594. button.style.display = "flex";
  595. button.style.justifyContent = "center";
  596. button.style.alignItems = "center";
  597. button.style.cursor = "pointer";
  598. button.style.fontSize = "18px";
  599. button.style.boxShadow = "inset 0 0 10px rgba(255, 255, 255, 0.5)";
  600. button.style.transition = "transform 0.2s, box-shadow 0.3s";
  601. button.textContent = icon;
  602. button.title = title;
  603.  
  604. button.addEventListener("click", () => {
  605. button.style.boxShadow = "inset 0 0 20px rgba(255, 255, 255, 0.8)";
  606. button.style.transform = "scale(0.9)";
  607. setTimeout(() => {
  608. button.style.boxShadow = "inset 0 0 10px rgba(255, 255, 255, 0.5)";
  609. button.style.transform = "scale(1)";
  610. }, 300);
  611. onClick();
  612. });
  613.  
  614. button.addEventListener("mouseenter", () => {
  615. button.style.boxShadow = "inset 0 0 15px rgba(255, 255, 255, 0.7)";
  616. button.style.transform = "scale(1.1)";
  617. });
  618.  
  619. button.addEventListener("mouseleave", () => {
  620. button.style.boxShadow = "inset 0 0 10px rgba(255, 255, 255, 0.5)";
  621. button.style.transform = "scale(1)";
  622. });
  623.  
  624. return button;
  625. }
  626.  
  627. async function importLastReadPost() {
  628. const input = document.createElement("input");
  629. input.type = "file";
  630. input.accept = "application/json";
  631. input.style.display = "none";
  632.  
  633. input.addEventListener("change", async (event) => {
  634. const file = event.target.files[0];
  635. if (file) {
  636. const reader = new FileReader();
  637. reader.onload = async () => {
  638. try {
  639. const importedData = JSON.parse(reader.result);
  640. if (importedData.timestamp && importedData.authorHandler) {
  641. console.log("✅ Importierte Leseposition geladen (wird nicht intern gespeichert):", importedData);
  642. showPopup("✅ Position geladen. Suche startet...");
  643.  
  644. const tempPosition = importedData;
  645. const matchedPost = findPostByData(tempPosition);
  646. if (matchedPost) {
  647. scrollToPostWithHighlight(matchedPost);
  648. } else {
  649. startRefinedSearchForLastReadPostWithPosition(tempPosition);
  650. }
  651. } else {
  652. throw new Error("Ungültige Position");
  653. }
  654. } catch (error) {
  655. console.error("❌ Fehler beim Importieren der Leseposition:", error);
  656. showPopup("❌ Fehler: Ungültige Position.");
  657. }
  658. };
  659. reader.readAsText(file);
  660. }
  661. });
  662.  
  663. document.body.appendChild(input);
  664. input.click();
  665. document.body.removeChild(input);
  666. }
  667.  
  668. function showPopup(message) {
  669. const popup = document.createElement("div");
  670. popup.style.position = "fixed";
  671. popup.style.bottom = "20px";
  672. popup.style.right = "20px";
  673. popup.style.backgroundColor = "rgba(0, 0, 0, 0.9)";
  674. popup.style.color = "#ffffff";
  675. popup.style.padding = "10px 20px";
  676. popup.style.borderRadius = "8px";
  677. popup.style.fontSize = "14px";
  678. popup.style.boxShadow = "0 0 10px rgba(255, 255, 255, 0.8)";
  679. popup.style.zIndex = "10000";
  680. popup.textContent = message;
  681.  
  682. document.body.appendChild(popup);
  683.  
  684. setTimeout(() => {
  685. popup.remove();
  686. }, 3000);
  687. }
  688. })();