Server (Express + better-sqlite3 + FTS5, mirroring keep's stack): /api/capture (upsert-on-url, per PROPOSAL.md's dedup decision), /api/articles (list/search/mark-read/delete), plus a minimal server-rendered reading list and reader view at / and /read/:id (token passed as ?t= there since browser navigation can't set an Authorization header). Bearer-token auth generated via `npm run token` — no login flow, matching keep's one-time identity registration pattern. Extension (Manifest V3, sideloaded, no store distribution): toolbar click or context menu "Send to stash" runs @mozilla/readability (vendored into extension/lib/) against the current page, falling back to raw rendered HTML when extraction comes back too thin. Options page for one-time server URL + token entry. Verified end-to-end with the real extension loaded in a live Chromium instance (not just unit-tested extraction logic): capture against a real page, dedup-on-recapture, mark-read, FTS5 search, 401 on missing auth, and both server-rendered views all confirmed working. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
52 lines
1.6 KiB
JavaScript
52 lines
1.6 KiB
JavaScript
chrome.runtime.onInstalled.addListener(() => {
|
|
chrome.contextMenus.create({
|
|
id: 'send-to-stash',
|
|
title: 'Send to stash',
|
|
contexts: ['page'],
|
|
});
|
|
});
|
|
|
|
chrome.contextMenus.onClicked.addListener((info, tab) => {
|
|
if (info.menuItemId === 'send-to-stash' && tab?.id) capture(tab.id);
|
|
});
|
|
|
|
chrome.action.onClicked.addListener((tab) => {
|
|
if (tab?.id) capture(tab.id);
|
|
});
|
|
|
|
async function capture(tabId) {
|
|
try {
|
|
const [{ result }] = await chrome.scripting.executeScript({
|
|
target: { tabId },
|
|
files: ['lib/Readability.js', 'extract.js'],
|
|
});
|
|
if (!result) throw new Error('extraction produced nothing');
|
|
|
|
const { serverUrl, token } = await chrome.storage.local.get(['serverUrl', 'token']);
|
|
if (!serverUrl || !token) {
|
|
notify('stash: not configured', 'Set the server URL and token in the extension options.');
|
|
chrome.runtime.openOptionsPage();
|
|
return;
|
|
}
|
|
|
|
const res = await fetch(`${serverUrl.replace(/\/$/, '')}/api/capture`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
body: JSON.stringify(result),
|
|
});
|
|
if (!res.ok) throw new Error(`server responded ${res.status}`);
|
|
notify('Sent to stash', result.title);
|
|
} catch (e) {
|
|
notify('stash capture failed', String(e.message ?? e));
|
|
}
|
|
}
|
|
|
|
function notify(title, message) {
|
|
chrome.notifications.create({
|
|
type: 'basic',
|
|
iconUrl: 'data:image/svg+xml;base64,' + btoa('<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48"><rect width="48" height="48" rx="8" fill="#0a5"/></svg>'),
|
|
title,
|
|
message,
|
|
});
|
|
}
|