Initial commit: Shelf read-later extension

This commit is contained in:
2026-07-13 11:11:24 +10:00
commit d627e6bb44
10 changed files with 876 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
// Shelf — background service worker
// Works under both the `chrome` namespace (Chrome/Edge) and Firefox's
// chrome.* alias for WebExtensions.
const STORAGE_KEY = "shelfItems";
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 addItem({ url, title, favIconUrl, image }) {
if (!url) return;
const 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 entry = {
id: existingIndex >= 0 ? items[existingIndex].id : `${Date.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(),
archived: false,
};
if (existingIndex >= 0) items.splice(existingIndex, 1);
items.unshift(entry);
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, "✓");
}
});