Add "not visited in x days" as an option

This commit is contained in:
Bastian Gruber 2026-01-06 12:03:57 -04:00
parent 537cc1f724
commit 5cf6f68d61
Signed by: gruberb
GPG key ID: 426AF1CBA0530691
7 changed files with 58 additions and 9 deletions

View file

@ -11,9 +11,10 @@ A Firefox extension that surfaces your forgotten bookmarks — the ones you save
## Features ## Features
- **Discover forgotten bookmarks** — Shows bookmarks you've rarely or never visited - **Discover forgotten bookmarks** — Shows bookmarks you've rarely or never visited
- **Stale bookmark detection** — Find bookmarks you visited a few times long ago but haven't opened in months or years
- **Click to open** — Just click any bookmark to visit it - **Click to open** — Just click any bookmark to visit it
- **Refresh behavior** — Choose to see new bookmarks every time, once a day, or only when you manually refresh - **Refresh behavior** — Choose to see new bookmarks every time, once a day, or only when you manually refresh
- **Configurable criteria** — Set maximum visit count and minimum age for "forgotten" status - **Configurable criteria** — Set maximum visit count, minimum age, and "not visited in X days" threshold
- **Exclude folders** — Keep certain bookmark folders out of suggestions - **Exclude folders** — Keep certain bookmark folders out of suggestions
## Screenshots ## Screenshots

View file

@ -12,6 +12,7 @@ const DEFAULT_SETTINGS = {
numBookmarks: 5, numBookmarks: 5,
maxVisitCount: 2, // Consider "forgotten" if visited 2 or fewer times maxVisitCount: 2, // Consider "forgotten" if visited 2 or fewer times
minAgeDays: 7, // Only show bookmarks older than 7 days minAgeDays: 7, // Only show bookmarks older than 7 days
notVisitedInDays: 0, // Consider "forgotten" if not visited in X days (0 = disabled)
refreshBehavior: "always", // "always", "daily", or "manual" refreshBehavior: "always", // "always", "daily", or "manual"
}; };
@ -96,13 +97,18 @@ async function getAllBookmarks(excludedFolderIds = []) {
return bookmarks; return bookmarks;
} }
// Get visit count for a URL // Get visit count and last visit time for a URL
async function getVisitCount(url) { async function getVisitInfo(url) {
try { try {
const visits = await browser.history.getVisits({ url }); const visits = await browser.history.getVisits({ url });
return visits.length; const lastVisitTime =
visits.length > 0 ? Math.max(...visits.map((v) => v.visitTime)) : null;
return {
visitCount: visits.length,
lastVisitTime: lastVisitTime,
};
} catch (e) { } catch (e) {
return 0; return { visitCount: 0, lastVisitTime: null };
} }
} }
@ -116,6 +122,7 @@ async function fetchFreshBookmarks() {
// Filter by age and get visit counts // Filter by age and get visit counts
const candidates = []; const candidates = [];
const notVisitedInMs = settings.notVisitedInDays * 24 * 60 * 60 * 1000;
for (const bookmark of bookmarks) { for (const bookmark of bookmarks) {
// Skip if too new // Skip if too new
@ -131,16 +138,29 @@ async function fetchFreshBookmarks() {
continue; continue;
} }
const visitCount = await getVisitCount(bookmark.url); const { visitCount, lastVisitTime } = await getVisitInfo(bookmark.url);
if (visitCount <= settings.maxVisitCount) { // Check if bookmark qualifies as "forgotten"
// Either: low visit count OR hasn't been visited in X days (if enabled)
const isLowVisitCount = visitCount <= settings.maxVisitCount;
const isStale =
settings.notVisitedInDays > 0 &&
(lastVisitTime === null || now - lastVisitTime > notVisitedInMs);
if (isLowVisitCount || isStale) {
// Check if dateAdded is valid (not corrupted/too old) // Check if dateAdded is valid (not corrupted/too old)
const hasValidDate = const hasValidDate =
bookmark.dateAdded && bookmark.dateAdded >= MIN_VALID_TIMESTAMP; bookmark.dateAdded && bookmark.dateAdded >= MIN_VALID_TIMESTAMP;
const daysSinceLastVisit = lastVisitTime
? Math.floor((now - lastVisitTime) / (24 * 60 * 60 * 1000))
: null;
candidates.push({ candidates.push({
...bookmark, ...bookmark,
visitCount, visitCount,
lastVisitTime,
daysSinceLastVisit,
daysSinceAdded: hasValidDate daysSinceAdded: hasValidDate
? Math.floor((now - bookmark.dateAdded) / (24 * 60 * 60 * 1000)) ? Math.floor((now - bookmark.dateAdded) / (24 * 60 * 60 * 1000))
: null, : null,

View file

@ -1,7 +1,7 @@
{ {
"manifest_version": 2, "manifest_version": 2,
"name": "Embermarks", "name": "Embermarks",
"version": "1.0.3", "version": "1.0.4",
"description": "Rediscover your forgotten bookmarks. Surface the ones you never visit.", "description": "Rediscover your forgotten bookmarks. Surface the ones you never visit.",
"author": "Bastian Gruber", "author": "Bastian Gruber",
"homepage_url": "https://github.com/gruberb/embermarks", "homepage_url": "https://github.com/gruberb/embermarks",

View file

@ -277,6 +277,23 @@
value="7" value="7"
/> />
</div> </div>
<div class="setting-row">
<div class="setting-label">
Not visited in (days)
<span
>Also show bookmarks not visited in this many days,
even if visited before (0 = disabled)</span
>
</div>
<input
type="number"
id="notVisitedInDays"
min="0"
max="3650"
value="0"
/>
</div>
</div> </div>
<div class="section"> <div class="section">

View file

@ -5,6 +5,7 @@ document.addEventListener("DOMContentLoaded", async () => {
const refreshBehaviorSelect = document.getElementById("refreshBehavior"); const refreshBehaviorSelect = document.getElementById("refreshBehavior");
const maxVisitCountInput = document.getElementById("maxVisitCount"); const maxVisitCountInput = document.getElementById("maxVisitCount");
const minAgeDaysInput = document.getElementById("minAgeDays"); const minAgeDaysInput = document.getElementById("minAgeDays");
const notVisitedInDaysInput = document.getElementById("notVisitedInDays");
const folderList = document.getElementById("folderList"); const folderList = document.getElementById("folderList");
const saveBtn = document.getElementById("saveBtn"); const saveBtn = document.getElementById("saveBtn");
const saveStatus = document.getElementById("saveStatus"); const saveStatus = document.getElementById("saveStatus");
@ -16,6 +17,7 @@ document.addEventListener("DOMContentLoaded", async () => {
refreshBehaviorSelect.value = settings.refreshBehavior || "always"; refreshBehaviorSelect.value = settings.refreshBehavior || "always";
maxVisitCountInput.value = settings.maxVisitCount; maxVisitCountInput.value = settings.maxVisitCount;
minAgeDaysInput.value = settings.minAgeDays; minAgeDaysInput.value = settings.minAgeDays;
notVisitedInDaysInput.value = settings.notVisitedInDays || 0;
// Load folder list // Load folder list
const folders = await browser.runtime.sendMessage({ const folders = await browser.runtime.sendMessage({
@ -60,6 +62,7 @@ document.addEventListener("DOMContentLoaded", async () => {
refreshBehavior: refreshBehaviorSelect.value, refreshBehavior: refreshBehaviorSelect.value,
maxVisitCount: parseInt(maxVisitCountInput.value, 10), maxVisitCount: parseInt(maxVisitCountInput.value, 10),
minAgeDays: parseInt(minAgeDaysInput.value, 10), minAgeDays: parseInt(minAgeDaysInput.value, 10),
notVisitedInDays: parseInt(notVisitedInDaysInput.value, 10),
excludedFolders, excludedFolders,
}; };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 131 KiB

View file

@ -55,7 +55,15 @@ function createBookmarkCard(bookmark) {
meta.className = "bookmark-meta"; meta.className = "bookmark-meta";
const visitText = const visitText =
bookmark.visitCount === 1 ? "1 visit" : `${bookmark.visitCount} visits`; bookmark.visitCount === 1 ? "1 visit" : `${bookmark.visitCount} visits`;
meta.textContent = `${visitText} · Added ${formatDaysAgo(bookmark.daysSinceAdded)}`;
let metaText = `${visitText} · Added ${formatDaysAgo(bookmark.daysSinceAdded)}`;
// Show last visited info if the bookmark has been visited
if (bookmark.daysSinceLastVisit !== null && bookmark.visitCount > 0) {
metaText += ` · Last visit ${formatDaysAgo(bookmark.daysSinceLastVisit)}`;
}
meta.textContent = metaText;
info.appendChild(title); info.appendChild(title);
info.appendChild(meta); info.appendChild(meta);