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:
+50
-4
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user