diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..849ddff --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000..f845b97 --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,53 @@ +# Privacy Policy for Read it later + +Last updated: 2026-07-14 + +Read it later is a browser extension for saving pages and links to read later. This policy explains what data it handles, where that data lives, and what it never does with it. + +## No account, no server + +Read it later has no backend server, no account system, and no analytics or tracking of any kind. The developer never receives, sees, or has access to any data this extension stores. Everything described below either stays on your device or moves through your own browser vendor's sync infrastructure (Google's or Mozilla's), never through infrastructure operated by the developer. + +## What the extension stores + +When you save a page or link, the extension stores: + +- The page's URL, title, and domain +- A favicon, or (only when saving the page you're currently viewing) a screenshot of that page, used as a thumbnail +- Any tags you choose to add +- The date you saved it, whether it's archived, and related display state +- Your own settings: grid or list view, sort order, theme, and the reminder, auto-delete, and "Worth a second look" preferences described in the extension's README + +This data is stored using your browser's local extension storage (`chrome.storage.local`), on your device only. + +## Optional sync across your devices + +If your browser is signed into your own account with its built-in sync enabled, the extension also mirrors a lightweight copy of each saved item (URL, title, domain, favicon, dates, archived state, tags, and some internal counters used by the "Worth a second look" feature) through your browser vendor's own sync storage (`chrome.storage.sync`), so the same list appears on your other signed-in devices. Screenshot thumbnails are never included in this sync; devices without a given screenshot show a favicon instead. + +This sync is handled entirely by your browser vendor under their own privacy policy and security practices. The developer does not operate, and has no access to, this sync data at any point. + +## What this extension does not do + +- It does not collect or transmit personally identifiable information, health information, financial or payment information, passwords or other authentication credentials, personal communications, or location data. +- It does not monitor your browsing activity, clicks, mouse movement, or keystrokes. +- It does not sell, rent, or share any data with third parties. +- It does not use your data for advertising, profiling, or any purpose other than showing you the pages and links you chose to save. +- It does not download or execute any remote code. All code ships inside the extension package. + +## Permissions + +The extension requests only the browser permissions it needs to function: + +- **storage**: to save your items and preferences, as described above +- **contextMenus**: to add the "Save page for later" and "Save link for later" right-click menu items +- **activeTab**: to read the current tab's URL, title, and favicon, and capture a screenshot, only when you click the toolbar icon or a save menu item +- **unlimitedStorage**: so screenshot thumbnails don't run into the browser's default storage limit +- **alarms**: to run the optional, off-by-default auto-delete check on an hourly schedule + +## Changes to this policy + +If this policy changes, the updated version will be included with the extension and dated above. + +## Contact + +Questions about this policy can be sent to me@stefwill.net. diff --git a/README.md b/README.md index 50648c9..6c5add7 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Read it later A new-tab page that shows everything you've saved to read later, as a -sortable grid or list. No account, no server — everything lives in your +sortable grid or list. No account, no server: everything lives in your browser's local extension storage. ## How to save something -- **Click the toolbar icon** — saves the page you're currently on. +- **Click the toolbar icon**: saves the page you're currently on. - **Right-click a page** → "Save page for later" - **Right-click a link** → "Save link for later" (doesn't need to be open) @@ -15,9 +15,9 @@ A ✓ briefly appears on the icon to confirm the save. ## The shelf (new tab page) - Toggle between **grid** (cards with thumbnails) and **list** (compact, - bookshelf-spine style) — top right. + bookshelf-spine style), top right. - **Sort** by newest, oldest, title, or site. -- **Search** filters by title, domain, or URL as you type. +- **Search** filters by title, domain, URL, or tag as you type. - **Archive** tucks an item away without deleting it; "Show archived" reveals archived items again. - **Delete** removes an item, with a 5-second **Undo**. @@ -26,56 +26,68 @@ Thumbnails are captured automatically when you save the *current* page (a screenshot of what's on screen). Links saved via right-click show a favicon instead, since the browser hasn't loaded that page yet. +## Tags + +In grid view, click **+ tag** on any card to add one or more free-form +labels (comma-separated, e.g. `css, research`); click the **×** on a chip to +remove it. Tags are grid-only: list view stays the compact bookshelf row +it's meant to be. + +Click a tag chip (on a card, or in the 🏷 toolbar button's dropdown, which +lists every tag currently in use) to instantly filter the shelf to it. This +just fills in the search box, so typing a tag name works too, and it +combines with everything search already matches (title, domain, URL). + ## Reminders and auto-delete The gear icon opens a settings panel (also home to the light/dark/system theme picker) with two optional timers, both off by default: -- **Remind me after** — items older than this get pinned to the top of the +- **Remind me after**: items older than this get pinned to the top of the shelf and highlighted, so they don't get buried. -- **Delete unarchived items after** — silently removes items you never +- **Delete unarchived items after**: silently removes items you never came back to. Archiving an item takes it out of consideration entirely. -The delete period must be longer than the reminder period — enforced both -in the settings panel (an inline error if you try to set it shorter) and as -a hard rule in the deletion logic itself, so you're always warned before -anything is removed. Auto-delete runs on an hourly background alarm (so it -still works even if you never open the new tab page), and again whenever -the shelf page is open; when the shelf page does the deleting, it shows a -courtesy toast with an **Undo**. +The delete period must be longer than the reminder period. This is +enforced both in the settings panel (an inline error if you try to set it +shorter) and as a hard rule in the deletion logic itself, so you're always +warned before anything is removed. Auto-delete runs on an hourly background +alarm (so it still works even if you never open the new tab page), and +again whenever the shelf page is open; when the shelf page does the +deleting, it shows a courtesy toast with an **Undo**. ## Worth a second look Above the shelf, a small strip proactively surfaces 2–3 old, forgotten, -unarchived items each time you open a new tab — on by default, toggleable +unarchived items each time you open a new tab, on by default and toggleable in the settings panel. Unlike a notification, it doesn't nag: the more times an item gets shown without you opening/archiving/deleting it, the less often it's picked again (backing off from every few days out to every couple of months), so nothing gets shown forever, but nothing gets shown *so* rarely that it's effectively forgotten again either. Archiving or deleting an item removes it from the strip immediately with no replacement -until the next refresh — no whack-a-mole. +until the next refresh, avoiding whack-a-mole. "Old enough to matter" uses the same period as the reminder setting above when reminders are on, or a 14-day default when they're off. It's fine (and expected) for the same item to show up both pinned/highlighted in the main -shelf *and* in this strip at once — they're not trying to be non-overlapping, +shelf *and* in this strip at once. They're not trying to be non-overlapping, just two different nudges. -## Install — Chrome / Edge (unpacked) +## Install on Chrome / Edge (unpacked) 1. Go to `chrome://extensions` (or `edge://extensions`). 2. Turn on **Developer mode** (top right). 3. Click **Load unpacked** and select this folder. -4. Open a new tab — you should see the Read it later page. +4. Open a new tab. You should see the Read it later page. -## Install — Firefox (temporary, for testing) +## Install on Firefox (temporary, for testing) 1. Go to `about:debugging#/runtime/this-firefox`. 2. Click **Load Temporary Add-on…** 3. Select the `manifest.json` file inside this folder. 4. Open a new tab. Firefox will ask permission to let the extension - override the new tab page the first time — allow it. + override the new tab page the first time. Allow it. > Temporary add-ons in Firefox are removed when you close the browser. > For a permanent install, the extension needs to be signed by Mozilla @@ -87,22 +99,22 @@ just two different nudges. Read it later keeps full items (including screenshot thumbnails) in `chrome.storage.local`, and also mirrors a lightweight copy of each item -(url, title, domain, favicon, dates, archived state, spotlight backoff -counters — no images) to +(url, title, domain, favicon, dates, archived state, tags, spotlight +backoff counters, never images) to `chrome.storage.sync`, one entry per item so each stays well under the 8KB-per-item sync limit. On another device signed into the same browser account, opening the shelf pulls those in automatically; thumbnails just aren't part of what syncs, so items saved elsewhere show a favicon instead until re-saved on that device. Archiving and deleting also sync. This is -all best-effort — offline, not signed in, or over the (~100KB / 512-item) +all best-effort: offline, not signed in, or over the (~100KB / 512-item) sync quota just means that device keeps working from local storage alone. Note: `chrome.storage.sync` needs a stable extension ID to work, which -only applies to a permanently installed, signed add-on — not a Firefox +only applies to a permanently installed, signed add-on, not a Firefox "temporary add-on" loaded via `about:debugging` (see install notes above), which won't persist sync identity across restarts. ## Notes / things you might want to change -- Icons in `icons/` are placeholders — swap them for your own artwork +- Icons in `icons/` are placeholders. Swap them for your own artwork any time; sizes 16/32/48/128 are already wired up in `manifest.json`. diff --git a/background.js b/background.js index 5d0028b..77dd155 100644 --- a/background.js +++ b/background.js @@ -1,4 +1,4 @@ -// Read it later — background service worker +// Read it later: background service worker // Works under both the `chrome` namespace (Chrome/Edge) and Firefox's // chrome.* alias for WebExtensions. @@ -36,9 +36,9 @@ async function getPrefs() { } // 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. +// shelf page is never opened. Silent, since 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; @@ -70,6 +70,7 @@ async function addItem({ url, title, favIconUrl, image }) { updatedAt: now, surfaceCount: existingIndex >= 0 ? items[existingIndex].surfaceCount || 0 : 0, lastSurfacedAt: existingIndex >= 0 ? items[existingIndex].lastSurfacedAt || 0 : 0, + tags: existingIndex >= 0 ? items[existingIndex].tags || [] : [], }; if (existingIndex >= 0) items.splice(existingIndex, 1); @@ -141,7 +142,7 @@ chrome.contextMenus.onClicked.addListener(async (info, tab) => { 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. + // 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, "✓"); } diff --git a/description.md b/description.md new file mode 100644 index 0000000..a4a8a0c --- /dev/null +++ b/description.md @@ -0,0 +1,55 @@ +# Read it later + +## Elevator pitch (1 line) + +A new-tab page that remembers what you saved, actively helps you get back to it, and never turns into a graveyard of forgotten links. + +## Short description + +Read it later is a browser extension for saving pages and links to read later, then actually reading them. Click the toolbar icon or right-click any page or link to save it; every new tab shows your saved items as a sortable, searchable grid or a compact list. Beyond basic saving, it proactively fights the "saved and forgotten" problem: a reminder highlights stale items, a "Worth a second look" strip resurfaces old ones without nagging, and an optional auto-delete clears out things you truly never got back to. Everything lives in the browser's own storage, with no account and no external server, and it optionally syncs across your devices through the browser's built-in sync. + +## Detailed description + +**Saving** + +- Click the toolbar icon to save the page you're currently on. A screenshot of the page is captured automatically as a thumbnail. +- Right-click any page and choose "Save page for later," or right-click a link and choose "Save link for later" (this works even for links you haven't opened; they show a favicon instead of a screenshot since the browser never loaded them). +- Saving the same URL twice just bumps it back to the top and refreshes its metadata, instead of creating a duplicate. + +**The shelf (new tab page)** + +- Two views: a **grid** of cards with thumbnails, and a compact **list** styled like spines on a bookshelf. +- Sort by newest, oldest, title, or site. +- Search filters by title, domain, URL, or tag as you type. +- Archive tucks an item away without deleting it; "Show archived" brings archived items back into view. +- Delete removes an item, with a 5 second Undo. + +**Tags** + +- In grid view, add one or more free-form tags to any card (comma-separated entry supports adding several at once), and remove them individually. +- Click any tag, on a card or in the toolbar's tag dropdown (which lists every tag currently in use), to instantly filter the shelf to it. This works by filling in the search box, so typing a tag name filters the same way. + +**Reminders and auto-delete** + +- An optional "Remind me after" timer highlights and pins items older than a chosen period, so they surface when you're browsing the full shelf. +- An optional "Delete unarchived items after" timer quietly removes items you never returned to. Archiving an item takes it out of consideration permanently. +- The delete period is required to be longer than the reminder period, both as a settings-panel validation and as a hard rule in the deletion logic itself, so nothing is ever deleted without having been flagged first. Auto-delete runs on an hourly background alarm (so it works even if the new tab page is never opened) and again whenever the shelf is open, where it shows a courtesy toast with an Undo. + +**Worth a second look** + +- A small strip above the shelf proactively surfaces 2 to 3 old, unarchived items each time a new tab opens, aiming squarely at the core failure mode of every read-later tool: saving something and never coming back to it. +- It's designed to never become a nag. The more times an item is shown without being opened, archived, or deleted, the less often it's picked again, backing off from every few days out to every couple of months. Nothing is shown forever, but nothing goes silent forever either. +- Archiving or deleting a spotlighted item removes it immediately, with no replacement sliding in until the next refresh. + +**Appearance** + +- Light, dark, or system theme, chosen in the settings panel. + +**Sync and storage** + +- No account and no external server. Full items, including screenshot thumbnails, live in the browser's local extension storage on each device. +- A lightweight copy of each item (URL, title, domain, favicon, dates, archived state, tags, and the "Worth a second look" backoff counters, never the thumbnails) can also sync across devices signed into the same browser account, using the browser's own sync storage. This is best effort: if a device is offline, not signed in, or over the sync quota, it simply keeps working from local storage alone. + +**Platforms** + +- Works in Chrome, Edge, and Firefox. diff --git a/manifest.json b/manifest.json index 2266b83..7ebd6d5 100644 --- a/manifest.json +++ b/manifest.json @@ -27,8 +27,14 @@ }, "browser_specific_settings": { "gecko": { - "id": "shelf-read-later@example.local", - "strict_min_version": "109.0" + "id": "{b5bcdd65-958d-4b77-a5d6-ba2684c37a94}", + "strict_min_version": "140.0", + "data_collection_permissions": { + "required": ["bookmarksInfo"] + } + }, + "gecko_android": { + "strict_min_version": "142.0" } } } diff --git a/newtab.css b/newtab.css index ff4aa40..83e1efd 100644 --- a/newtab.css +++ b/newtab.css @@ -259,6 +259,103 @@ select#sortSelect { line-height: 1.4; } +/* --- Tags filter popover --- */ +.tags-wrap { + position: relative; + display: inline-block; +} +.tags-panel { + display: block; + position: absolute; + top: calc(100% + 6px); + right: 0; + left: auto; + z-index: 20; + width: 220px; + max-height: 260px; + overflow-y: auto; + background: var(--surface); + border: 1px solid var(--hairline); + border-radius: 10px; + box-shadow: var(--shadow); + padding: 14px; +} +.tags-panel[hidden] { + display: none; +} +.tags-list { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +/* --- Tag chips (shared by cards and the tags-filter popover) --- */ +.tag-chip { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 11px; + padding: 3px 8px; + border-radius: 999px; + background: var(--accent-soft); + color: var(--clay-deep); + border: none; + font-family: inherit; + cursor: pointer; +} +.tag-chip-label { + cursor: pointer; +} +.tag-chip:hover .tag-chip-label, +.tag-chip-filter:hover { + text-decoration: underline; +} +.tag-remove { + background: none; + border: none; + color: var(--clay-deep); + cursor: pointer; + font-size: 12px; + line-height: 1; + padding: 0; + opacity: 0.7; +} +.tag-remove:hover { + opacity: 1; +} + +.card-tags { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + margin-top: 2px; +} +.tag-add-btn { + font-size: 11px; + padding: 3px 8px; + border-radius: 999px; + background: transparent; + border: 1px dashed var(--hairline-strong); + color: var(--muted); + cursor: pointer; + font-family: inherit; +} +.tag-add-btn:hover { + color: var(--ink); + border-color: var(--clay); +} +.tag-input { + font-size: 11px; + padding: 3px 8px; + border-radius: 999px; + border: 1px solid var(--hairline); + background: var(--paper); + color: var(--ink); + font-family: inherit; + width: 120px; +} + /* --- Shelf: grid view (library index cards) --- */ .shelf[data-view="grid"] { display: grid; diff --git a/newtab.html b/newtab.html index 1891846..ce97f11 100644 --- a/newtab.html +++ b/newtab.html @@ -16,7 +16,7 @@