// Read it later — background service worker // Works under both the `chrome` namespace (Chrome/Edge) and Firefox's // chrome.* alias for WebExtensions. // Chrome loads only "service_worker" from the manifest (a Worker context, so // importScripts works); Firefox loads the "scripts" array instead, which // already includes sync.js ahead of this file in the same global scope. if (typeof importScripts === "function") { importScripts("sync.js"); } const STORAGE_KEY = "shelfItems"; const PREFS_KEY = "shelfPrefs"; const AUTO_DELETE_ALARM = "autoDeleteSweep"; function domainOf(url) { try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return ""; } } async function getItems() { const data = await chrome.storage.local.get(STORAGE_KEY); return data[STORAGE_KEY] || []; } async function setItems(items) { await chrome.storage.local.set({ [STORAGE_KEY]: items }); } async function getPrefs() { const data = await chrome.storage.local.get(PREFS_KEY); return data[PREFS_KEY] || {}; } // Runs on an hourly alarm so overdue items get cleaned up even when the // shelf page is never opened. Silent — no one's necessarily watching, so // unlike the newtab-page sweep there's no toast/undo here; the newtab page // runs its own courtesy-toast sweep whenever it's open. async function sweepAutoDelete() { const prefs = await getPrefs(); if (!prefs.autoDeleteEnabled) return; const items = await getItems(); const remaining = items.filter((item) => !isOverdueForDeletion(item, prefs)); if (remaining.length === items.length) return; const merged = typeof reconcileWithSync === "function" ? await reconcileWithSync(remaining) : remaining; await setItems(merged); } async function addItem({ url, title, favIconUrl, image }) { if (!url) return; let items = await getItems(); // De-dupe: if already saved, just bump it to the top and refresh metadata. const existingIndex = items.findIndex((i) => i.url === url); const now = Date.now(); const entry = { id: existingIndex >= 0 ? items[existingIndex].id : `${now}-${Math.random().toString(36).slice(2, 8)}`, url, title: title || url, domain: domainOf(url), favicon: favIconUrl || "", image: image || (existingIndex >= 0 ? items[existingIndex].image : ""), dateAdded: now, archived: false, updatedAt: now, surfaceCount: existingIndex >= 0 ? items[existingIndex].surfaceCount || 0 : 0, lastSurfacedAt: existingIndex >= 0 ? items[existingIndex].lastSurfacedAt || 0 : 0, }; if (existingIndex >= 0) items.splice(existingIndex, 1); items.unshift(entry); items = typeof reconcileWithSync === "function" ? await reconcileWithSync(items) : items; await setItems(items); return entry; } async function flashBadge(tabId, text) { try { await chrome.action.setBadgeText({ text, tabId }); await chrome.action.setBadgeBackgroundColor({ color: "#C47A4A", tabId }); setTimeout(() => { chrome.action.setBadgeText({ text: "", tabId }).catch(() => {}); }, 1400); } catch { // tabId may be gone; ignore. } } // Try to grab a screenshot of the *currently active* tab. This only works // for the page the user is actually looking at (browser security), so it's // used when saving the current page, not for arbitrary right-clicked links. async function captureScreenshot(windowId) { try { const dataUrl = await chrome.tabs.captureVisibleTab(windowId, { format: "jpeg", quality: 55, }); return dataUrl; } catch { return ""; } } // --- Toolbar icon click: save the current page --- chrome.action.onClicked.addListener(async (tab) => { if (!tab || !tab.url || tab.url.startsWith("chrome://") || tab.url.startsWith("about:")) { return; } const image = await captureScreenshot(tab.windowId); await addItem({ url: tab.url, title: tab.title, favIconUrl: tab.favIconUrl, image }); flashBadge(tab.id, "✓"); }); // --- Context menus: right-click a page or a link --- chrome.runtime.onInstalled.addListener(() => { chrome.contextMenus.create({ id: "save-page-later", title: "Save page for later", contexts: ["page"], }); chrome.contextMenus.create({ id: "save-link-later", title: "Save link for later", contexts: ["link"], }); }); chrome.contextMenus.onClicked.addListener(async (info, tab) => { if (info.menuItemId === "save-page-later") { if (!tab || !tab.url) return; const image = await captureScreenshot(tab.windowId); await addItem({ url: tab.url, title: tab.title, favIconUrl: tab.favIconUrl, image }); flashBadge(tab.id, "✓"); } if (info.menuItemId === "save-link-later") { if (!info.linkUrl) return; const linkTitle = info.linkText && info.linkText.trim() ? info.linkText.trim() : info.linkUrl; // No screenshot for links we haven't navigated to — favicon/title only. await addItem({ url: info.linkUrl, title: linkTitle, favIconUrl: "", image: "" }); if (tab && tab.id) flashBadge(tab.id, "✓"); } }); // --- Auto-delete: hourly sweep, independent of whether the shelf is open --- // chrome.alarms.create is idempotent (re-arming an existing alarm just // resets it), so it's safe to call unconditionally every time this script // runs rather than only from onInstalled. chrome.alarms.create(AUTO_DELETE_ALARM, { periodInMinutes: 60 }); sweepAutoDelete(); chrome.alarms.onAlarm.addListener((alarm) => { if (alarm.name === AUTO_DELETE_ALARM) sweepAutoDelete(); });