// ==UserScript==
// @name X Timeline Sync
// @description Tracks and syncs your last reading position on Twitter/X, with manual and automatic options. Ideal for keeping track of new posts without losing your place.
// @description:de Verfolgt und synchronisiert Ihre letzte Leseposition auf Twitter/X, mit manuellen und automatischen Optionen. Perfekt, um neue Beiträge im Blick zu behalten, ohne die aktuelle Position zu verlieren.
// @description:es Rastrea y sincroniza tu última posición de lectura en Twitter/X, con opciones manuales y automáticas. Ideal para mantener el seguimiento de las publicaciones nuevas sin perder tu posición.
// @description:fr Suit et synchronise votre dernière position de lecture sur Twitter/X, avec des options manuelles et automatiques. Idéal pour suivre les nouveaux posts sans perdre votre place actuelle.
// @description:zh-CN 跟踪并同步您在 Twitter/X 上的最后阅读位置,提供手动和自动选项。完美解决在查看新帖子时不丢失当前位置的问题。
// @description:ru Отслеживает и синхронизирует вашу последнюю позицию чтения на Twitter/X с ручными и автоматическими опциями. Идеально подходит для просмотра новых постов без потери текущей позиции.
// @description:ja Twitter/X での最後の読み取り位置を追跡して同期します。手動および自動オプションを提供します。新しい投稿を見逃さずに現在の位置を維持するのに最適です。
// @description:pt-BR Rastrea e sincroniza sua última posição de leitura no Twitter/X, com opções manuais e automáticas. Perfeito para acompanhar novos posts sem perder sua posição atual.
// @description:hi Twitter/X पर आपकी अंतिम पठन स्थिति को ट्रैक और सिंक करता है, मैनुअल और स्वचालित विकल्पों के साथ। नई पोस्ट देखते समय अपनी वर्तमान स्थिति को खोए बिना इसे ट्रैक करें।
// @description:ar يتتبع ويزامن آخر موضع قراءة لك على Twitter/X، مع خيارات يدوية وتلقائية. مثالي لتتبع المشاركات الجديدة دون فقدان موضعك الحالي.
// @description:it Traccia e sincronizza la tua ultima posizione di lettura su Twitter/X, con opzioni manuali e automatiche. Ideale per tenere traccia dei nuovi post senza perdere la posizione attuale.
// @description:ko Twitter/X에서 마지막 읽기 위치를 추적하고 동기화합니다. 수동 및 자동 옵션 포함. 새로운 게시물을 확인하면서 현재 위치를 잃지 않도록 이상적입니다.
// @icon https://x.com/favicon.ico
// @namespace http://tampermonkey.net/
// @version 2025-02-26.2
// @author Copiis
// @license MIT
// @match https://x.com/home
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_download
// ==/UserScript==
(function() {
'use strict';
let lastReadPost = null;
let isAutoScrolling = false;
let isSearching = false;
let isTabFocused = true;
let downloadTriggered = false;
let isPostLoading = false;
let hasScrolledAfterLoad = false;
let saveToDownloadFolder = GM_getValue("saveToDownloadFolder", true);
const translations = {
en: {
scriptDisabled: "🚫 Script disabled: Not on the home page.",
pageLoaded: "🚀 Page fully loaded. Initializing script...",
tabBlur: "🌐 Tab lost focus.",
downloadStart: "📥 Starting download of last read position...",
alreadyDownloaded: "🗂️ Position already downloaded.",
tabFocused: "🟢 Tab refocused.",
saveSuccess: "✅ Last read position saved:",
saveFail: "⚠️ No valid position to save.",
noPostFound: "❌ No top visible post found.",
highlightSuccess: "✅ Post highlighted successfully.",
searchStart: "🔍 Refined search started...",
searchCancel: "⏹️ Search manually canceled.",
contentLoadWait: "⌛ Waiting for content to load...",
toggleSaveOn: "💾 Save to download folder enabled",
toggleSaveOff: "🚫 Save to download folder disabled"
},
de: {
scriptDisabled: "🚫 Skript deaktiviert: Nicht auf der Home-Seite.",
pageLoaded: "🚀 Seite vollständig geladen. Initialisiere Skript...",
tabBlur: "🌐 Tab hat den Fokus verloren.",
downloadStart: "📥 Starte Download der letzten Leseposition...",
alreadyDownloaded: "🗂️ Leseposition bereits im Download-Ordner vorhanden.",
tabFocused: "🟢 Tab wieder fokussiert.",
saveSuccess: "✅ Leseposition erfolgreich gespeichert:",
saveFail: "⚠️ Keine gültige Leseposition zum Speichern.",
noPostFound: "❌ Kein oberster sichtbarer Beitrag gefunden.",
highlightSuccess: "✅ Beitrag erfolgreich hervorgehoben.",
searchStart: "🔍 Verfeinerte Suche gestartet...",
searchCancel: "⏹️ Suche manuell abgebrochen.",
contentLoadWait: "⌛ Warte darauf, dass der Inhalt geladen wird...",
toggleSaveOn: "💾 Speichern im Download-Ordner aktiviert",
toggleSaveOff: "🚫 Speichern im Download-Ordner deaktiviert"
}
};
const userLang = navigator.language.split('-')[0];
const t = (key) => translations[userLang]?.[key] || translations.en[key];
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
const observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const postData = {
timestamp: getPostTimestamp(entry.target),
authorHandler: getPostAuthorHandler(entry.target)
};
}
});
},
{ threshold: 0.1 }
);
function observeVisiblePosts() {
const articles = document.querySelectorAll('article');
const viewportHeight = window.innerHeight;
const buffer = viewportHeight * 2;
for (let article of articles) {
const rect = article.getBoundingClientRect();
if (rect.top < buffer && rect.bottom > -buffer) {
observer.observe(article);
} else {
observer.unobserve(article);
}
}
}
function loadNewestLastReadPost() {
const data = GM_getValue("lastReadPost", null);
if (data) {
lastReadPost = JSON.parse(data);
console.log(t("saveSuccess"), lastReadPost);
} else {
console.warn(t("saveFail"));
}
}
function loadLastReadPostFromFile() {
loadNewestLastReadPost();
}
function saveLastReadPostToFile() {
if (lastReadPost && lastReadPost.timestamp && lastReadPost.authorHandler) {
GM_setValue("lastReadPost", JSON.stringify(lastReadPost));
console.log(t("saveSuccess"), lastReadPost);
} else {
console.warn(t("saveFail"));
}
}
function downloadLastReadPost() {
if (!saveToDownloadFolder) {
console.log("Saving to download folder is disabled.");
return;
}
if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler) {
console.warn(t("saveFail"));
return;
}
try {
const data = JSON.stringify(lastReadPost, null, 2);
const sanitizedHandler = lastReadPost.authorHandler.replace(/[^a-zA-Z0-9-_]/g, "");
const timestamp = new Date(lastReadPost.timestamp).toISOString().replace(/[:.-]/g, "_");
const fileName = `${sanitizedHandler}_${timestamp}.json`;
GM_download({
url: `data:application/json;charset=utf-8,${encodeURIComponent(data)}`,
name: fileName,
onload: () => console.log(`${t("saveSuccess")} ${fileName}`),
onerror: (err) => console.error("❌ Error downloading:", err),
});
} catch (error) {
console.error("❌ Download error:", error);
}
}
function markTopVisiblePost(save = true) {
const topPost = getTopVisiblePost();
if (!topPost) {
console.log(t("noPostFound"));
return;
}
const postTimestamp = getPostTimestamp(topPost);
const authorHandler = getPostAuthorHandler(topPost);
if (postTimestamp && authorHandler) {
if (save && (!lastReadPost || new Date(postTimestamp) > new Date(lastReadPost.timestamp))) {
lastReadPost = { timestamp: postTimestamp, authorHandler };
saveLastReadPostToFile();
}
}
}
function getTopVisiblePost() {
return Array.from(document.querySelectorAll("article")).find(post => {
const rect = post.getBoundingClientRect();
return rect.top >= 0 && rect.bottom > 0;
});
}
function getPostTimestamp(post) {
const timeElement = post.querySelector("time");
return timeElement ? timeElement.getAttribute("datetime") : null;
}
function getPostAuthorHandler(post) {
const handlerElement = post.querySelector('[role="link"][href*="/"]');
return handlerElement ? handlerElement.getAttribute("href").slice(1) : null;
}
function startRefinedSearchForLastReadPost() {
if (!lastReadPost || !lastReadPost.timestamp || !lastReadPost.authorHandler || isPostLoading) return;
console.log(t("searchStart"));
const popup = createSearchPopup();
let direction = 1;
let scrollAmount = 200;
let scrollSpeed = 1000;
let scrollInterval = 200;
let lastComparison = null;
let initialAdjusted = false;
let lastScrollDirection = null;
let lastScrollY = 0;
let jumpMultiplier = 1;
function handleSpaceKey(event) {
if (event.code === "Space") {
console.log(t("searchCancel"));
isSearching = false;
popup.remove();
window.removeEventListener("keydown", handleSpaceKey);
}
}
window.addEventListener("keydown", handleSpaceKey);
function adjustScrollParameters(lastReadTime) {
const visiblePosts = getVisiblePosts();
if (visiblePosts.length === 0) return { amount: 200, speed: 1000 };
const nearestPost = visiblePosts.reduce((closest, post) => {
const postTime = new Date(post.timestamp);
const diffCurrent = Math.abs(postTime - lastReadTime);
const diffClosest = Math.abs(new Date(closest.timestamp) - lastReadTime);
return diffCurrent < diffClosest ? post : closest;
});
const nearestTime = new Date(nearestPost.timestamp);
const timeDifference = Math.abs(lastReadTime - nearestTime) / (1000 * 60);
let newScrollAmount = 200;
let newScrollSpeed = 1000;
if (timeDifference < 5) {
newScrollAmount = 50;
newScrollSpeed = 500;
jumpMultiplier = 1;
} else if (timeDifference < 30) {
newScrollAmount = 100;
newScrollSpeed = 1000;
jumpMultiplier = 1;
} else if (timeDifference < 60) {
newScrollAmount = 200;
newScrollSpeed = 1500;
jumpMultiplier = 1.5;
} else if (timeDifference < 1440) {
newScrollAmount = 500;
newScrollSpeed = 2000;
jumpMultiplier = 2;
} else {
newScrollAmount = 1000;
newScrollSpeed = 3000;
jumpMultiplier = 3;
}
newScrollAmount = Math.max(50, Math.min(newScrollAmount * jumpMultiplier, window.innerHeight * 2));
return { amount: newScrollAmount, speed: newScrollSpeed };
}
async function search() {
if (!isSearching) {
popup.remove();
return;
}
const visiblePosts = getVisiblePosts();
if (visiblePosts.length === 0 && !isPostLoading) {
setTimeout(search, scrollInterval);
return;
}
if (!initialAdjusted) {
const comparison = compareVisiblePostsToLastReadPost(visiblePosts);
adjustInitialScroll(comparison);
initialAdjusted = true;
}
const comparison = compareVisiblePostsToLastReadPost(visiblePosts);
const lastReadTime = new Date(lastReadPost.timestamp);
let nearestVisiblePostTime = null;
if (visiblePosts.length > 0) {
nearestVisiblePostTime = new Date(visiblePosts[0].timestamp);
}
const { amount, speed } = adjustScrollParameters(lastReadTime);
scrollAmount = amount;
scrollSpeed = speed;
scrollInterval = Math.max(30, 1000 / (scrollSpeed / scrollAmount));
const distanceToBottom = document.documentElement.scrollHeight - (window.scrollY + window.innerHeight);
if (distanceToBottom < window.innerHeight) {
scrollAmount = Math.max(50, scrollAmount / 2);
scrollSpeed = Math.max(500, scrollSpeed / 2);
scrollInterval = Math.max(30, 1000 / (scrollSpeed / scrollAmount));
}
if (comparison === "match") {
const matchedPost = findPostByData(lastReadPost);
if (matchedPost) {
scrollToPostWithHighlight(matchedPost);
isSearching = false;
popup.remove();
window.removeEventListener("keydown", handleSpaceKey);
setTimeout(() => {
isAutoScrolling = false;
}, 1000);
return;
}
} else if (comparison === "older") {
direction = -1;
if (lastComparison === "newer") jumpMultiplier *= 0.5;
} else if (comparison === "newer") {
direction = 1;
if (lastComparison === "older") jumpMultiplier *= 0.5;
} else if (comparison === "mixed") {
scrollAmount = Math.max(50, scrollAmount / 2);
scrollSpeed = Math.max(500, scrollSpeed / 2);
scrollInterval = Math.max(30, 1000 / (scrollSpeed / scrollAmount));
}
if (window.scrollY === 0 && direction === -1) {
direction = 1;
jumpMultiplier *= 2;
} else if (distanceToBottom < 50 && direction === 1) {
direction = -1;
jumpMultiplier *= 2;
}
lastComparison = comparison;
lastScrollDirection = direction;
lastScrollY = window.scrollY;
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'}`);
requestAnimationFrame(() => {
window.scrollBy(0, direction * scrollAmount);
setTimeout(search, scrollInterval);
});
}
isSearching = true;
search();
}
function adjustInitialScroll(comparison) {
const initialScrollAmount = 2000;
if (comparison === "older") {
for (let i = 0; i < 3; i++) {
window.scrollBy(0, -initialScrollAmount / 3);
}
} else if (comparison === "newer") {
for (let i = 0; i < 3; i++) {
window.scrollBy(0, initialScrollAmount / 3);
}
}
}
function createSearchPopup() {
const popup = document.createElement("div");
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;`;
popup.textContent = "🔍 Refined search in progress... Press SPACE to cancel.";
document.body.appendChild(popup);
return popup;
}
function compareVisiblePostsToLastReadPost(posts) {
const validPosts = posts.filter(post => post.timestamp && post.authorHandler);
if (validPosts.length === 0) return null;
const lastReadTime = new Date(lastReadPost.timestamp);
if (validPosts.some(post => post.timestamp === lastReadPost.timestamp && post.authorHandler === lastReadPost.authorHandler)) {
return "match";
} else if (validPosts.every(post => new Date(post.timestamp) < lastReadTime)) {
return "older";
} else if (validPosts.every(post => new Date(post.timestamp) > lastReadTime)) {
return "newer";
} else {
return "mixed";
}
}
function scrollToPostWithHighlight(post) {
if (!post) return;
isAutoScrolling = true;
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;`;
const postRect = post.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const scrollY = window.scrollY;
const scrollTo = scrollY + postRect.top - viewportHeight / 2 + postRect.height / 2;
window.scrollTo({
top: scrollTo,
behavior: 'smooth'
});
setTimeout(() => {
let scrollHandler = function() {
post.style.cssText = "";
window.removeEventListener('scroll', scrollHandler);
console.log(t("highlightSuccess"));
};
window.addEventListener('scroll', scrollHandler);
}, 500);
}
function getVisiblePosts() {
return Array.from(document.querySelectorAll("article")).map(post => ({
element: post,
timestamp: getPostTimestamp(post),
authorHandler: getPostAuthorHandler(post)
})).filter(post => post.timestamp && post.authorHandler);
}
function findPostByData(data) {
return Array.from(document.querySelectorAll("article")).find(post => {
const postTimestamp = getPostTimestamp(post);
const authorHandler = getPostAuthorHandler(post);
return postTimestamp === data.timestamp && authorHandler === data.authorHandler;
});
}
function createButtons() {
const container = document.createElement("div");
container.style.cssText = `position: fixed; top: 50%; left: 3px; transform: translateY(-50%); display: flex; flex-direction: column; gap: 3px; z-index: 10000;`;
let toggleButton;
const buttons = [
{ icon: "📂", title: "Load saved reading position", onClick: importLastReadPost },
{ icon: "🔍", title: "Start manual search", onClick: startRefinedSearchForLastReadPost },
{
icon: saveToDownloadFolder ? "💾" : "🚫",
title: "Toggle save to download folder",
onClick: function() {
saveToDownloadFolder = !saveToDownloadFolder;
GM_setValue("saveToDownloadFolder", saveToDownloadFolder);
toggleButton.style.background = saveToDownloadFolder ? "rgba(0, 255, 0, 0.9)" : "rgba(255, 0, 0, 0.9)";
toggleButton.textContent = saveToDownloadFolder ? "💾" : "🚫";
console.log(saveToDownloadFolder ? t("toggleSaveOn") : t("toggleSaveOff"));
}
}
];
buttons.forEach(({ icon, title, onClick }) => {
const button = document.createElement("div");
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;`;
button.title = title;
button.textContent = icon;
button.addEventListener("click", function() {
button.style.boxShadow = "inset 0 0 20px rgba(255, 255, 255, 0.8)";
button.style.transform = "scale(0.9)";
setTimeout(() => {
button.style.boxShadow = "inset 0 0 10px rgba(255, 255, 255, 0.5)";
button.style.transform = "scale(1)";
onClick();
}, 300);
});
["mouseenter", "mouseleave"].forEach(event =>
button.addEventListener(event, () => button.style.transform = event === "mouseenter" ? "scale(1.1)" : "scale(1)")
);
if (icon === "💾" || icon === "🚫") {
toggleButton = button;
}
container.appendChild(button);
});
document.body.appendChild(container);
}
function importLastReadPost() {
const input = document.createElement("input");
input.type = "file";
input.accept = "application/json";
input.style.display = "none";
input.addEventListener("change", (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = () => {
try {
const importedData = JSON.parse(reader.result);
if (importedData.timestamp && importedData.authorHandler) {
lastReadPost = importedData;
saveLastReadPostToFile();
startRefinedSearchForLastReadPost();
} else {
throw new Error("Invalid reading position");
}
} catch (error) {
console.error("❌ Error importing reading position:", error);
}
};
reader.readAsText(file);
}
});
document.body.appendChild(input);
input.click();
document.body.removeChild(input);
}
function observeForNewPosts() {
const targetNode = document.querySelector('div[aria-label="Timeline: Your Home Timeline"]') || document.body;
const mutationObserver = new MutationObserver(mutations => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
checkForNewPosts();
}
}
});
mutationObserver.observe(targetNode, { childList: true, subtree: true });
}
function getNewPostsIndicator() {
const indicator = document.querySelector('div[aria-label*="undefined"]') ||
document.querySelector('div[aria-label*="new"]');
console.log(`[Debug] Neuer Beitragsindikator gefunden: ${indicator ? 'Ja' : 'Nein'}`);
return indicator;
}
function clickNewPostsIndicator(indicator, preservedScrollY) {
if (indicator && indicator.offsetParent !== null) {
console.log("Versuche, auf den neuen Beitrag-Indikator zu klicken.");
indicator.click();
console.log("Klick auf den neuen Beitrag-Indikator war erfolgreich.");
const timelineNode = document.querySelector('div[aria-label="Timeline: Your Home Timeline"]') || document.body;
let mutationTimeout;
const observer = new MutationObserver(() => {
if (window.scrollY !== preservedScrollY) {
window.scrollTo(0, preservedScrollY);
console.log(`[Debug] Scroll-Position korrigiert auf: ${preservedScrollY}`);
}
clearTimeout(mutationTimeout);
mutationTimeout = setTimeout(() => {
observer.disconnect();
console.log(t("newPostsLoaded"));
}, 3000);
});
observer.observe(timelineNode, { childList: true, subtree: true });
setTimeout(() => {
window.scrollTo(0, preservedScrollY);
}, 100);
} else {
console.log("Kein klickbarer Indikator gefunden.");
}
}
async function checkForNewPosts() {
if (window.scrollY <= 10) {
const newPostsIndicator = getNewPostsIndicator();
if (newPostsIndicator) {
const preservedScrollY = window.scrollY;
console.log("🆕 Neue Beiträge in der Nähe des oberen Randes erkannt. Klicke auf Indikator...");
clickNewPostsIndicator(newPostsIndicator, preservedScrollY);
hasScrolledAfterLoad = false;
try {
console.log(t("contentLoadWait"));
await waitForTimelineLoad(5000);
console.log("Inhalt geladen, starte verfeinerte Suche...");
startRefinedSearchForLastReadPost();
} catch (error) {
console.error("❌ Fehler beim Warten auf das Laden der Timeline:", error.message);
console.log("[Debug] Starte Suche trotz Fehler.");
startRefinedSearchForLastReadPost();
}
} else {
console.log("[Debug] Kein neuer Beitragsindikator gefunden.");
}
} else {
console.log("[Debug] Scroll-Position nicht oben, keine neuen Beiträge geprüft.");
}
}
async function waitForTimelineLoad(maxWaitTime = 3000) {
const startTime = Date.now();
return new Promise((resolve) => {
const checkInterval = setInterval(() => {
const loadingSpinner = document.querySelector('div[role="progressbar"]') ||
document.querySelector('div.css-175oi2r.r-1pi2tsx.r-1wtj0ep.r-ymttw5.r-1f1sjgu');
const timeElapsed = Date.now() - startTime;
if (!loadingSpinner) {
console.log("[Debug] Ladeindikator verschwunden, resolve.");
clearInterval(checkInterval);
resolve(true);
} else if (timeElapsed > maxWaitTime) {
console.log("[Debug] Timeout erreicht, starte Suche trotz Ladeindikator.");
clearInterval(checkInterval);
resolve(true);
}
}, 200);
});
}
async function initializeScript() {
console.log(t("pageLoaded"));
try {
await loadLastReadPostFromFile();
observeForNewPosts();
observeVisiblePosts();
window.addEventListener("scroll", debounce(() => {
observeVisiblePosts();
if (!isAutoScrolling && !isSearching) {
if (hasScrolledAfterLoad) {
markTopVisiblePost(true);
} else {
hasScrolledAfterLoad = true;
}
}
}, 500));
} catch (error) {
console.error("❌ Fehler bei der Initialisierung des Skripts:", error);
}
}
window.onload = async () => {
if (!window.location.href.includes("/home")) {
console.log(t("scriptDisabled"));
return;
}
console.log(t("pageLoaded"));
try {
await loadNewestLastReadPost();
await initializeScript();
createButtons();
} catch (error) {
console.error("❌ Fehler beim Seitenladen:", error);
}
};
window.addEventListener("blur", async () => {
console.log(t("tabBlur"));
if (lastReadPost && !downloadTriggered) {
downloadTriggered = true;
if (!(await isFileAlreadyDownloaded())) {
console.log(t("downloadStart"));
await downloadLastReadPost();
await markDownloadAsComplete();
} else {
console.log(t("alreadyDownloaded"));
}
downloadTriggered = false;
}
});
window.addEventListener("focus", () => {
isTabFocused = true;
downloadTriggered = false;
console.log(t("tabFocused"));
});
async function isFileAlreadyDownloaded() {
const localFiles = await GM_getValue("downloadedPosts", []);
const fileSignature = `${lastReadPost.authorHandler}_${lastReadPost.timestamp}`;
return localFiles.includes(fileSignature);
}
async function markDownloadAsComplete() {
const localFiles = await GM_getValue("downloadedPosts", []);
const fileSignature = `${lastReadPost.authorHandler}_${lastReadPost.timestamp}`;
if (!localFiles.includes(fileSignature)) {
localFiles.push(fileSignature);
GM_setValue("downloadedPosts", localFiles);
}
}
})();