feat: add reminders, auto-delete, cross-device sync, dark mode, and rediscovery spotlight

- Rename extension from Shelf to "Read it later"
- Add reminder settings: highlight and pin overdue unarchived items
- Add auto-delete with a hard safety rule requiring its period to exceed the reminder period
- Add cross-device sync via chrome.storage.sync (lightweight per-item metadata, no thumbnails)
- Add light/dark/system theme support
- Fix list-view Archive/Delete button alignment (card-perf wasn't filling the row)
- Add "Worth a second look" spotlight: proactively resurfaces old, forgotten items with automatic backoff so repeatedly-ignored items are shown less often, never permanently
This commit is contained in:
2026-07-13 20:49:05 +10:00
parent d627e6bb44
commit c5bd7e38bf
7 changed files with 847 additions and 32 deletions
+50 -4
View File
@@ -1,8 +1,17 @@
// Shelf — background service worker
// 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 {
@@ -21,25 +30,51 @@ 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;
const items = await getItems();
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 : `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
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: Date.now(),
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;
}
@@ -111,3 +146,14 @@ chrome.contextMenus.onClicked.addListener(async (info, tab) => {
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();
});