X Timeline Sync

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

目前为 2025-02-26 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name 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-02-26.2
  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.  
  26. (function() {
  27. 'use strict';
  28.  
  29. let lastReadPost = null;
  30. let isAutoScrolling = false;
  31. let isSearching = false;
  32. let isTabFocused = true;
  33. let downloadTriggered = false;
  34. let isPostLoading = false;
  35. let hasScrolledAfterLoad = false;
  36. let saveToDownloadFolder = GM_getValue("saveToDownloadFolder", true);
  37.  
  38. const translations = {
  39. en: {
  40. scriptDisabled: "🚫 Script disabled: Not on the home page.",
  41. pageLoaded: "🚀 Page fully loaded. Initializing script...",
  42. tabBlur: "🌐 Tab lost focus.",
  43. downloadStart: "📥 Starting download of last read position...",
  44. alreadyDownloaded: "🗂️ Position already downloaded.",
  45. tabFocused: "🟢 Tab refocused.",
  46. saveSuccess: "✅ Last read position saved:",
  47. saveFail: "⚠️ No valid position to save.",
  48. noPostFound: "❌ No top visible post found.",
  49. highlightSuccess: "✅ Post highlighted successfully.",
  50. searchStart: "🔍 Refined search started...",
  51. searchCancel: "⏹️ Search manually canceled.",
  52. contentLoadWait: "⌛ Waiting for content to load...",
  53. toggleSaveOn: "💾 Save to download folder enabled",
  54. toggleSaveOff: "🚫 Save to download folder disabled"
  55. },
  56. de: {
  57. scriptDisabled: "🚫 Skript deaktiviert: Nicht auf der Home-Seite.",
  58. pageLoaded: "🚀 Seite vollständig geladen. Initialisiere Skript...",
  59. tabBlur: "🌐 Tab hat den Fokus verloren.",
  60. downloadStart: "📥 Starte Download der letzten Leseposition...",
  61. alreadyDownloaded: "🗂️ Leseposition bereits im Download-Ordner vorhanden.",
  62. tabFocused: "🟢 Tab wieder fokussiert.",
  63. saveSuccess: "✅ Leseposition erfolgreich gespeichert:",
  64. saveFail: "⚠️ Keine gültige Leseposition zum Speichern.",
  65. noPostFound: "❌ Kein oberster sichtbarer Beitrag gefunden.",
  66. highlightSuccess: "✅ Beitrag erfolgreich hervorgehoben.",
  67. searchStart: "🔍 Verfeinerte Suche gestartet...",
  68. searchCancel: "⏹️ Suche manuell abgebrochen.",
  69. contentLoadWait: "⌛ Warte darauf, dass der Inhalt geladen wird...",
  70. toggleSaveOn: "💾 Speichern im Download-Ordner aktiviert",
  71. toggleSaveOff: "🚫 Speichern im Download-Ordner deaktiviert"
  72. }
  73. };
  74.  
  75. const userLang = navigator.language.split('-')[0];
  76. const t = (key) => translations[userLang]?.[key] || translations.en[key];
  77.  
  78. function debounce(func, wait) {
  79. let timeout;
  80. return function executedFunction(...args) {
  81. const later = () => {
  82. clearTimeout(timeout);
  83. func(...args);
  84. };
  85. clearTimeout(timeout);
  86. timeout = setTimeout(later, wait);
  87. };
  88. }
  89.  
  90. const observer = new IntersectionObserver(
  91. entries => {
  92. entries.forEach(entry => {
  93. if (entry.isIntersecting) {
  94. const postData = {
  95. timestamp: getPostTimestamp(entry.target),
  96. authorHandler: getPostAuthorHandler(entry.target)
  97. };
  98. }
  99. });
  100. },
  101. { threshold: 0.1 }
  102. );
  103.  
  104. function observeVisiblePosts() {
  105. const articles = document.querySelectorAll('article');
  106. const viewportHeight = window.innerHeight;
  107. const buffer = viewportHeight * 2;
  108.  
  109. for (let article of articles) {
  110. const rect = article.getBoundingClientRect();
  111. if (rect.top < buffer && rect.bottom > -buffer) {
  112. observer.observe(article);
  113. } else {
  114. observer.unobserve(article);
  115. }
  116. }
  117. }
  118.  
  119. function loadNewestLastReadPost() {
  120. const data = GM_getValue("lastReadPost", null);
  121. if (data) {
  122. lastReadPost = JSON.parse(data);
  123. console.log(t("saveSuccess"), lastReadPost);
  124. } else {
  125. console.warn(t("saveFail"));
  126. }
  127. }
  128.  
  129. function loadLastReadPostFromFile() {
  130. loadNewestLastReadPost();
  131. }
  132.  
  133. function saveLastReadPostToFile() {
  134. if (lastReadPost && lastReadPost.timestamp && lastReadPost.authorHandler) {
  135. GM_setValue("lastReadPost", JSON.stringify(lastReadPost));
  136. console.log(t("saveSuccess"), lastReadPost);
  137. } else {
  138. console.warn(t("saveFail"));
  139. }
  140. }
  141.  
  142. function downloadLastReadPost() {
  143. if (!saveToDownloadFolder) {
  144. console.log("Saving to download folder is disabled.");
  145. return;
  146. }
  147. if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler) {
  148. console.warn(t("saveFail"));
  149. return;
  150. }
  151. try {
  152. const data = JSON.stringify(lastReadPost, null, 2);
  153. const sanitizedHandler = lastReadPost.authorHandler.replace(/[^a-zA-Z0-9-_]/g, "");
  154. const timestamp = new Date(lastReadPost.timestamp).toISOString().replace(/[:.-]/g, "_");
  155. const fileName = `${sanitizedHandler}_${timestamp}.json`;
  156.  
  157. GM_download({
  158. url: `data:application/json;charset=utf-8,${encodeURIComponent(data)}`,
  159. name: fileName,
  160. onload: () => console.log(`${t("saveSuccess")} ${fileName}`),
  161. onerror: (err) => console.error("❌ Error downloading:", err),
  162. });
  163. } catch (error) {
  164. console.error("❌ Download error:", error);
  165. }
  166. }
  167.  
  168. function markTopVisiblePost(save = true) {
  169. const topPost = getTopVisiblePost();
  170. if (!topPost) {
  171. console.log(t("noPostFound"));
  172. return;
  173. }
  174.  
  175. const postTimestamp = getPostTimestamp(topPost);
  176. const authorHandler = getPostAuthorHandler(topPost);
  177.  
  178. if (postTimestamp && authorHandler) {
  179. if (save && (!lastReadPost || new Date(postTimestamp) > new Date(lastReadPost.timestamp))) {
  180. lastReadPost = { timestamp: postTimestamp, authorHandler };
  181. saveLastReadPostToFile();
  182. }
  183. }
  184. }
  185.  
  186. function getTopVisiblePost() {
  187. return Array.from(document.querySelectorAll("article")).find(post => {
  188. const rect = post.getBoundingClientRect();
  189. return rect.top >= 0 && rect.bottom > 0;
  190. });
  191. }
  192.  
  193. function getPostTimestamp(post) {
  194. const timeElement = post.querySelector("time");
  195. return timeElement ? timeElement.getAttribute("datetime") : null;
  196. }
  197.  
  198. function getPostAuthorHandler(post) {
  199. const handlerElement = post.querySelector('[role="link"][href*="/"]');
  200. return handlerElement ? handlerElement.getAttribute("href").slice(1) : null;
  201. }
  202.  
  203. function startRefinedSearchForLastReadPost() {
  204. if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler || isPostLoading) return;
  205.  
  206. console.log(t("searchStart"));
  207. const popup = createSearchPopup();
  208. let direction = 1;
  209. let scrollAmount = 200;
  210. let scrollSpeed = 1000;
  211. let scrollInterval = 200;
  212. let lastComparison = null;
  213. let initialAdjusted = false;
  214. let lastScrollDirection = null;
  215. let lastScrollY = 0;
  216. let jumpMultiplier = 1;
  217.  
  218. function handleSpaceKey(event) {
  219. if (event.code === "Space") {
  220. console.log(t("searchCancel"));
  221. isSearching = false;
  222. popup.remove();
  223. window.removeEventListener("keydown", handleSpaceKey);
  224. }
  225. }
  226.  
  227. window.addEventListener("keydown", handleSpaceKey);
  228.  
  229. function adjustScrollParameters(lastReadTime) {
  230. const visiblePosts = getVisiblePosts();
  231. if (visiblePosts.length === 0) return { amount: 200, speed: 1000 };
  232.  
  233. const nearestPost = visiblePosts.reduce((closest, post) => {
  234. const postTime = new Date(post.timestamp);
  235. const diffCurrent = Math.abs(postTime - lastReadTime);
  236. const diffClosest = Math.abs(new Date(closest.timestamp) - lastReadTime);
  237. return diffCurrent < diffClosest ? post : closest;
  238. });
  239.  
  240. const nearestTime = new Date(nearestPost.timestamp);
  241. const timeDifference = Math.abs(lastReadTime - nearestTime) / (1000 * 60);
  242.  
  243. let newScrollAmount = 200;
  244. let newScrollSpeed = 1000;
  245.  
  246. if (timeDifference < 5) {
  247. newScrollAmount = 50;
  248. newScrollSpeed = 500;
  249. jumpMultiplier = 1;
  250. } else if (timeDifference < 30) {
  251. newScrollAmount = 100;
  252. newScrollSpeed = 1000;
  253. jumpMultiplier = 1;
  254. } else if (timeDifference < 60) {
  255. newScrollAmount = 200;
  256. newScrollSpeed = 1500;
  257. jumpMultiplier = 1.5;
  258. } else if (timeDifference < 1440) {
  259. newScrollAmount = 500;
  260. newScrollSpeed = 2000;
  261. jumpMultiplier = 2;
  262. } else {
  263. newScrollAmount = 1000;
  264. newScrollSpeed = 3000;
  265. jumpMultiplier = 3;
  266. }
  267.  
  268. newScrollAmount = Math.max(50, Math.min(newScrollAmount * jumpMultiplier, window.innerHeight * 2));
  269. return { amount: newScrollAmount, speed: newScrollSpeed };
  270. }
  271.  
  272. async function search() {
  273. if (!isSearching) {
  274. popup.remove();
  275. return;
  276. }
  277.  
  278. const visiblePosts = getVisiblePosts();
  279. if (visiblePosts.length === 0 && !isPostLoading) {
  280. setTimeout(search, scrollInterval);
  281. return;
  282. }
  283.  
  284. if (!initialAdjusted) {
  285. const comparison = compareVisiblePostsToLastReadPost(visiblePosts);
  286. adjustInitialScroll(comparison);
  287. initialAdjusted = true;
  288. }
  289.  
  290. const comparison = compareVisiblePostsToLastReadPost(visiblePosts);
  291. const lastReadTime = new Date(lastReadPost.timestamp);
  292. let nearestVisiblePostTime = null;
  293.  
  294. if (visiblePosts.length > 0) {
  295. nearestVisiblePostTime = new Date(visiblePosts[0].timestamp);
  296. }
  297.  
  298. const { amount, speed } = adjustScrollParameters(lastReadTime);
  299. scrollAmount = amount;
  300. scrollSpeed = speed;
  301. scrollInterval = Math.max(30, 1000 / (scrollSpeed / scrollAmount));
  302.  
  303. const distanceToBottom = document.documentElement.scrollHeight - (window.scrollY + window.innerHeight);
  304. if (distanceToBottom < window.innerHeight) {
  305. scrollAmount = Math.max(50, scrollAmount / 2);
  306. scrollSpeed = Math.max(500, scrollSpeed / 2);
  307. scrollInterval = Math.max(30, 1000 / (scrollSpeed / scrollAmount));
  308. }
  309.  
  310. if (comparison === "match") {
  311. const matchedPost = findPostByData(lastReadPost);
  312. if (matchedPost) {
  313. scrollToPostWithHighlight(matchedPost);
  314. isSearching = false;
  315. popup.remove();
  316. window.removeEventListener("keydown", handleSpaceKey);
  317. setTimeout(() => {
  318. isAutoScrolling = false;
  319. }, 1000);
  320. return;
  321. }
  322. } else if (comparison === "older") {
  323. direction = -1;
  324. if (lastComparison === "newer") jumpMultiplier *= 0.5;
  325. } else if (comparison === "newer") {
  326. direction = 1;
  327. if (lastComparison === "older") jumpMultiplier *= 0.5;
  328. } else if (comparison === "mixed") {
  329. scrollAmount = Math.max(50, scrollAmount / 2);
  330. scrollSpeed = Math.max(500, scrollSpeed / 2);
  331. scrollInterval = Math.max(30, 1000 / (scrollSpeed / scrollAmount));
  332. }
  333.  
  334. if (window.scrollY === 0 && direction === -1) {
  335. direction = 1;
  336. jumpMultiplier *= 2;
  337. } else if (distanceToBottom < 50 && direction === 1) {
  338. direction = -1;
  339. jumpMultiplier *= 2;
  340. }
  341.  
  342. lastComparison = comparison;
  343. lastScrollDirection = direction;
  344. lastScrollY = window.scrollY;
  345.  
  346. console.log(`Scroll-Richtung: ${direction}, Betrag: ${scrollAmount}px, Geschwindigkeit: ${scrollSpeed}px/s, Intervall: ${scrollInterval}ms, Position: ${window.scrollY}, Zeitdifferenz: ${nearestVisiblePostTime ? Math.abs(lastReadTime - nearestVisiblePostTime) : 'N/A'}`);
  347.  
  348. requestAnimationFrame(() => {
  349. window.scrollBy(0, direction * scrollAmount);
  350. setTimeout(search, scrollInterval);
  351. });
  352. }
  353.  
  354. isSearching = true;
  355. search();
  356. }
  357.  
  358. function adjustInitialScroll(comparison) {
  359. const initialScrollAmount = 2000;
  360. if (comparison === "older") {
  361. for (let i = 0; i < 3; i++) {
  362. window.scrollBy(0, -initialScrollAmount / 3);
  363. }
  364. } else if (comparison === "newer") {
  365. for (let i = 0; i < 3; i++) {
  366. window.scrollBy(0, initialScrollAmount / 3);
  367. }
  368. }
  369. }
  370.  
  371. function createSearchPopup() {
  372. const popup = document.createElement("div");
  373. popup.style.cssText = `position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.9); color: #fff; padding: 10px 20px; border-radius: 8px; font-size: 14px; box-shadow: 0 0 10px rgba(255, 255, 255, 0.8); z-index: 10000;`;
  374. popup.textContent = "🔍 Refined search in progress... Press SPACE to cancel.";
  375. document.body.appendChild(popup);
  376. return popup;
  377. }
  378.  
  379. function compareVisiblePostsToLastReadPost(posts) {
  380. const validPosts = posts.filter(post => post.timestamp && post.authorHandler);
  381. if (validPosts.length === 0) return null;
  382.  
  383. const lastReadTime = new Date(lastReadPost.timestamp);
  384.  
  385. if (validPosts.some(post => post.timestamp === lastReadPost.timestamp && post.authorHandler === lastReadPost.authorHandler)) {
  386. return "match";
  387. } else if (validPosts.every(post => new Date(post.timestamp) < lastReadTime)) {
  388. return "older";
  389. } else if (validPosts.every(post => new Date(post.timestamp) > lastReadTime)) {
  390. return "newer";
  391. } else {
  392. return "mixed";
  393. }
  394. }
  395.  
  396. function scrollToPostWithHighlight(post) {
  397. if (!post) return;
  398. isAutoScrolling = true;
  399.  
  400. post.style.cssText = `outline: none; box-shadow: 0 0 30px 10px rgba(255, 223, 0, 1); background-color: rgba(255, 223, 0, 0.3); border-radius: 12px; transform: scale(1.1); transition: all 0.3s ease;`;
  401.  
  402. const postRect = post.getBoundingClientRect();
  403. const viewportHeight = window.innerHeight;
  404. const scrollY = window.scrollY;
  405. const scrollTo = scrollY + postRect.top - viewportHeight / 2 + postRect.height / 2;
  406.  
  407. window.scrollTo({
  408. top: scrollTo,
  409. behavior: 'smooth'
  410. });
  411.  
  412. setTimeout(() => {
  413. let scrollHandler = function() {
  414. post.style.cssText = "";
  415. window.removeEventListener('scroll', scrollHandler);
  416. console.log(t("highlightSuccess"));
  417. };
  418. window.addEventListener('scroll', scrollHandler);
  419. }, 500);
  420. }
  421.  
  422. function getVisiblePosts() {
  423. return Array.from(document.querySelectorAll("article")).map(post => ({
  424. element: post,
  425. timestamp: getPostTimestamp(post),
  426. authorHandler: getPostAuthorHandler(post)
  427. })).filter(post => post.timestamp && post.authorHandler);
  428. }
  429.  
  430. function findPostByData(data) {
  431. return Array.from(document.querySelectorAll("article")).find(post => {
  432. const postTimestamp = getPostTimestamp(post);
  433. const authorHandler = getPostAuthorHandler(post);
  434. return postTimestamp === data.timestamp && authorHandler === data.authorHandler;
  435. });
  436. }
  437.  
  438. function createButtons() {
  439. const container = document.createElement("div");
  440. container.style.cssText = `position: fixed; top: 50%; left: 3px; transform: translateY(-50%); display: flex; flex-direction: column; gap: 3px; z-index: 10000;`;
  441.  
  442. let toggleButton;
  443.  
  444. const buttons = [
  445. { icon: "📂", title: "Load saved reading position", onClick: importLastReadPost },
  446. { icon: "🔍", title: "Start manual search", onClick: startRefinedSearchForLastReadPost },
  447. {
  448. icon: saveToDownloadFolder ? "💾" : "🚫",
  449. title: "Toggle save to download folder",
  450. onClick: function() {
  451. saveToDownloadFolder = !saveToDownloadFolder;
  452. GM_setValue("saveToDownloadFolder", saveToDownloadFolder);
  453. toggleButton.style.background = saveToDownloadFolder ? "rgba(0, 255, 0, 0.9)" : "rgba(255, 0, 0, 0.9)";
  454. toggleButton.textContent = saveToDownloadFolder ? "💾" : "🚫";
  455. console.log(saveToDownloadFolder ? t("toggleSaveOn") : t("toggleSaveOff"));
  456. }
  457. }
  458. ];
  459.  
  460. buttons.forEach(({ icon, title, onClick }) => {
  461. const button = document.createElement("div");
  462. button.style.cssText = `width: 36px; height: 36px; background: ${icon === "💾" || icon === "🚫" ? (saveToDownloadFolder ? "rgba(0, 255, 0, 0.9)" : "rgba(255, 0, 0, 0.9)") : "rgba(0, 0, 0, 0.9)"}; color: #fff; border-radius: 50%; display: flex; justify-content: center; align-items: center; cursor: pointer; font-size: 18px; box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.5); transition: all 0.2s ease;`;
  463. button.title = title;
  464. button.textContent = icon;
  465.  
  466. button.addEventListener("click", function() {
  467. button.style.boxShadow = "inset 0 0 20px rgba(255, 255, 255, 0.8)";
  468. button.style.transform = "scale(0.9)";
  469. setTimeout(() => {
  470. button.style.boxShadow = "inset 0 0 10px rgba(255, 255, 255, 0.5)";
  471. button.style.transform = "scale(1)";
  472. onClick();
  473. }, 300);
  474. });
  475.  
  476. ["mouseenter", "mouseleave"].forEach(event =>
  477. button.addEventListener(event, () => button.style.transform = event === "mouseenter" ? "scale(1.1)" : "scale(1)")
  478. );
  479.  
  480. if (icon === "💾" || icon === "🚫") {
  481. toggleButton = button;
  482. }
  483.  
  484. container.appendChild(button);
  485. });
  486.  
  487. document.body.appendChild(container);
  488. }
  489.  
  490. function importLastReadPost() {
  491. const input = document.createElement("input");
  492. input.type = "file";
  493. input.accept = "application/json";
  494. input.style.display = "none";
  495.  
  496. input.addEventListener("change", (event) => {
  497. const file = event.target.files[0];
  498. if (file) {
  499. const reader = new FileReader();
  500. reader.onload = () => {
  501. try {
  502. const importedData = JSON.parse(reader.result);
  503. if (importedData.timestamp && importedData.authorHandler) {
  504. lastReadPost = importedData;
  505. saveLastReadPostToFile();
  506. startRefinedSearchForLastReadPost();
  507. } else {
  508. throw new Error("Invalid reading position");
  509. }
  510. } catch (error) {
  511. console.error("❌ Error importing reading position:", error);
  512. }
  513. };
  514. reader.readAsText(file);
  515. }
  516. });
  517.  
  518. document.body.appendChild(input);
  519. input.click();
  520. document.body.removeChild(input);
  521. }
  522.  
  523. function observeForNewPosts() {
  524. const targetNode = document.querySelector('div[aria-label="Timeline: Your Home Timeline"]') || document.body;
  525.  
  526. const mutationObserver = new MutationObserver(mutations => {
  527. for (const mutation of mutations) {
  528. if (mutation.type === 'childList') {
  529. checkForNewPosts();
  530. }
  531. }
  532. });
  533.  
  534. mutationObserver.observe(targetNode, { childList: true, subtree: true });
  535. }
  536.  
  537. function getNewPostsIndicator() {
  538. const indicator = document.querySelector('div[aria-label*="undefined"]') ||
  539. document.querySelector('div[aria-label*="new"]');
  540. console.log(`[Debug] Neuer Beitragsindikator gefunden: ${indicator ? 'Ja' : 'Nein'}`);
  541. return indicator;
  542. }
  543.  
  544. function clickNewPostsIndicator(indicator, preservedScrollY) {
  545. if (indicator && indicator.offsetParent !== null) {
  546. console.log("Versuche, auf den neuen Beitrag-Indikator zu klicken.");
  547. indicator.click();
  548. console.log("Klick auf den neuen Beitrag-Indikator war erfolgreich.");
  549.  
  550. const timelineNode = document.querySelector('div[aria-label="Timeline: Your Home Timeline"]') || document.body;
  551. let mutationTimeout;
  552.  
  553. const observer = new MutationObserver(() => {
  554. if (window.scrollY !== preservedScrollY) {
  555. window.scrollTo(0, preservedScrollY);
  556. console.log(`[Debug] Scroll-Position korrigiert auf: ${preservedScrollY}`);
  557. }
  558.  
  559. clearTimeout(mutationTimeout);
  560. mutationTimeout = setTimeout(() => {
  561. observer.disconnect();
  562. console.log(t("newPostsLoaded"));
  563. }, 3000);
  564. });
  565.  
  566. observer.observe(timelineNode, { childList: true, subtree: true });
  567.  
  568. setTimeout(() => {
  569. window.scrollTo(0, preservedScrollY);
  570. }, 100);
  571. } else {
  572. console.log("Kein klickbarer Indikator gefunden.");
  573. }
  574. }
  575.  
  576. async function checkForNewPosts() {
  577. if (window.scrollY <= 10) {
  578. const newPostsIndicator = getNewPostsIndicator();
  579. if (newPostsIndicator) {
  580. const preservedScrollY = window.scrollY;
  581. console.log("🆕 Neue Beiträge in der Nähe des oberen Randes erkannt. Klicke auf Indikator...");
  582. clickNewPostsIndicator(newPostsIndicator, preservedScrollY);
  583. hasScrolledAfterLoad = false;
  584.  
  585. try {
  586. console.log(t("contentLoadWait"));
  587. await waitForTimelineLoad(5000);
  588. console.log("Inhalt geladen, starte verfeinerte Suche...");
  589. startRefinedSearchForLastReadPost();
  590. } catch (error) {
  591. console.error("❌ Fehler beim Warten auf das Laden der Timeline:", error.message);
  592. console.log("[Debug] Starte Suche trotz Fehler.");
  593. startRefinedSearchForLastReadPost();
  594. }
  595. } else {
  596. console.log("[Debug] Kein neuer Beitragsindikator gefunden.");
  597. }
  598. } else {
  599. console.log("[Debug] Scroll-Position nicht oben, keine neuen Beiträge geprüft.");
  600. }
  601. }
  602.  
  603. async function waitForTimelineLoad(maxWaitTime = 3000) {
  604. const startTime = Date.now();
  605. return new Promise((resolve) => {
  606. const checkInterval = setInterval(() => {
  607. const loadingSpinner = document.querySelector('div[role="progressbar"]') ||
  608. document.querySelector('div.css-175oi2r.r-1pi2tsx.r-1wtj0ep.r-ymttw5.r-1f1sjgu');
  609. const timeElapsed = Date.now() - startTime;
  610.  
  611. if (!loadingSpinner) {
  612. console.log("[Debug] Ladeindikator verschwunden, resolve.");
  613. clearInterval(checkInterval);
  614. resolve(true);
  615. } else if (timeElapsed > maxWaitTime) {
  616. console.log("[Debug] Timeout erreicht, starte Suche trotz Ladeindikator.");
  617. clearInterval(checkInterval);
  618. resolve(true);
  619. }
  620. }, 200);
  621. });
  622. }
  623.  
  624. async function initializeScript() {
  625. console.log(t("pageLoaded"));
  626. try {
  627. await loadLastReadPostFromFile();
  628. observeForNewPosts();
  629. observeVisiblePosts();
  630.  
  631. window.addEventListener("scroll", debounce(() => {
  632. observeVisiblePosts();
  633. if (!isAutoScrolling && !isSearching) {
  634. if (hasScrolledAfterLoad) {
  635. markTopVisiblePost(true);
  636. } else {
  637. hasScrolledAfterLoad = true;
  638. }
  639. }
  640. }, 500));
  641. } catch (error) {
  642. console.error("❌ Fehler bei der Initialisierung des Skripts:", error);
  643. }
  644. }
  645.  
  646. window.onload = async () => {
  647. if (!window.location.href.includes("/home")) {
  648. console.log(t("scriptDisabled"));
  649. return;
  650. }
  651. console.log(t("pageLoaded"));
  652. try {
  653. await loadNewestLastReadPost();
  654. await initializeScript();
  655. createButtons();
  656. } catch (error) {
  657. console.error("❌ Fehler beim Seitenladen:", error);
  658. }
  659. };
  660.  
  661. window.addEventListener("blur", async () => {
  662. console.log(t("tabBlur"));
  663. if (lastReadPost && !downloadTriggered) {
  664. downloadTriggered = true;
  665. if (!(await isFileAlreadyDownloaded())) {
  666. console.log(t("downloadStart"));
  667. await downloadLastReadPost();
  668. await markDownloadAsComplete();
  669. } else {
  670. console.log(t("alreadyDownloaded"));
  671. }
  672. downloadTriggered = false;
  673. }
  674. });
  675.  
  676. window.addEventListener("focus", () => {
  677. isTabFocused = true;
  678. downloadTriggered = false;
  679. console.log(t("tabFocused"));
  680. });
  681.  
  682. async function isFileAlreadyDownloaded() {
  683. const localFiles = await GM_getValue("downloadedPosts", []);
  684. const fileSignature = `${lastReadPost.authorHandler}_${lastReadPost.timestamp}`;
  685. return localFiles.includes(fileSignature);
  686. }
  687.  
  688. async function markDownloadAsComplete() {
  689. const localFiles = await GM_getValue("downloadedPosts", []);
  690. const fileSignature = `${lastReadPost.authorHandler}_${lastReadPost.timestamp}`;
  691. if (!localFiles.includes(fileSignature)) {
  692. localFiles.push(fileSignature);
  693. GM_setValue("downloadedPosts", localFiles);
  694. }
  695. }
  696. })();