v1.0.3: Add refresh behavior setting (every time, daily, manual)
This commit is contained in:
parent
b679d436ca
commit
d278388afd
7 changed files with 148 additions and 50 deletions
13
README.md
13
README.md
|
|
@ -4,20 +4,17 @@ A Firefox extension that surfaces your forgotten bookmarks — the ones you save
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### From Firefox Add-ons (recommended)
|
### From Firefox Add-ons
|
||||||
|
|
||||||
[Install from AMO](https://addons.mozilla.org/firefox/addon/embermarks/)
|
[Install from AMO](https://addons.mozilla.org/en-US/firefox/addon/embermarks1/)
|
||||||
|
|
||||||
### Manual Install
|
|
||||||
|
|
||||||
Download the latest `.xpi` from [Releases](https://github.com/gruberb/embermarks/releases) and drag it into Firefox.
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Discover forgotten bookmarks** — Shows bookmarks you've rarely or never visited
|
- **Discover forgotten bookmarks** — Shows bookmarks you've rarely or never visited
|
||||||
|
- **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
|
||||||
- **Configurable criteria** — Set maximum visit count and minimum age for "forgotten" status
|
- **Configurable criteria** — Set maximum visit count and minimum age for "forgotten" status
|
||||||
- **Exclude folders** — Keep certain bookmark folders out of suggestions
|
- **Exclude folders** — Keep certain bookmark folders out of suggestions
|
||||||
- **Quick actions** — Visit, skip, or delete bookmarks directly from the popup
|
|
||||||
|
|
||||||
## Building from Source
|
## Building from Source
|
||||||
|
|
||||||
|
|
@ -37,4 +34,4 @@ npx web-ext run
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Mozilla Public License Version 2.0
|
Mozilla Public License Version 2.0
|
||||||
|
|
@ -4,13 +4,21 @@
|
||||||
// Minimum valid timestamp (Jan 1, 2010) - older dates are likely corrupted imports
|
// Minimum valid timestamp (Jan 1, 2010) - older dates are likely corrupted imports
|
||||||
const MIN_VALID_TIMESTAMP = 1262304000000;
|
const MIN_VALID_TIMESTAMP = 1262304000000;
|
||||||
|
|
||||||
|
// Cache duration for "daily" refresh mode (24 hours in ms)
|
||||||
|
const DAILY_CACHE_DURATION = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
const DEFAULT_SETTINGS = {
|
const DEFAULT_SETTINGS = {
|
||||||
excludedFolders: [],
|
excludedFolders: [],
|
||||||
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
|
||||||
|
refreshBehavior: "always", // "always", "daily", or "manual"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// In-memory cache for bookmarks
|
||||||
|
let cachedBookmarks = null;
|
||||||
|
let cacheTimestamp = null;
|
||||||
|
|
||||||
// Get user settings
|
// Get user settings
|
||||||
async function getSettings() {
|
async function getSettings() {
|
||||||
const result = await browser.storage.local.get("settings");
|
const result = await browser.storage.local.get("settings");
|
||||||
|
|
@ -24,6 +32,33 @@ async function saveSettings(settings) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get cached bookmarks if valid
|
||||||
|
async function getCachedBookmarks() {
|
||||||
|
const result = await browser.storage.local.get([
|
||||||
|
"cachedBookmarks",
|
||||||
|
"cacheTimestamp",
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
bookmarks: result.cachedBookmarks || null,
|
||||||
|
timestamp: result.cacheTimestamp || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save bookmarks to cache
|
||||||
|
async function setCachedBookmarks(bookmarks) {
|
||||||
|
await browser.storage.local.set({
|
||||||
|
cachedBookmarks: bookmarks,
|
||||||
|
cacheTimestamp: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the cache
|
||||||
|
async function clearCache() {
|
||||||
|
await browser.storage.local.remove(["cachedBookmarks", "cacheTimestamp"]);
|
||||||
|
cachedBookmarks = null;
|
||||||
|
cacheTimestamp = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Get all bookmarks recursively, respecting excluded folders
|
// Get all bookmarks recursively, respecting excluded folders
|
||||||
async function getAllBookmarks(excludedFolderIds = []) {
|
async function getAllBookmarks(excludedFolderIds = []) {
|
||||||
const tree = await browser.bookmarks.getTree();
|
const tree = await browser.bookmarks.getTree();
|
||||||
|
|
@ -71,8 +106,8 @@ async function getVisitCount(url) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get forgotten bookmarks (low visit count, old enough)
|
// Fetch fresh forgotten bookmarks
|
||||||
async function getForgottenBookmarks() {
|
async function fetchFreshBookmarks() {
|
||||||
const settings = await getSettings();
|
const settings = await getSettings();
|
||||||
const bookmarks = await getAllBookmarks(settings.excludedFolders);
|
const bookmarks = await getAllBookmarks(settings.excludedFolders);
|
||||||
|
|
||||||
|
|
@ -118,6 +153,41 @@ async function getForgottenBookmarks() {
|
||||||
return shuffled.slice(0, settings.numBookmarks);
|
return shuffled.slice(0, settings.numBookmarks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get forgotten bookmarks (with caching logic)
|
||||||
|
async function getForgottenBookmarks(forceRefresh = false) {
|
||||||
|
const settings = await getSettings();
|
||||||
|
const { bookmarks: cached, timestamp } = await getCachedBookmarks();
|
||||||
|
|
||||||
|
// Determine if we should use cache
|
||||||
|
if (!forceRefresh && cached && timestamp) {
|
||||||
|
const now = Date.now();
|
||||||
|
const cacheAge = now - timestamp;
|
||||||
|
|
||||||
|
if (settings.refreshBehavior === "manual") {
|
||||||
|
// Always use cache in manual mode (until explicit refresh)
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
settings.refreshBehavior === "daily" &&
|
||||||
|
cacheAge < DAILY_CACHE_DURATION
|
||||||
|
) {
|
||||||
|
// Use cache if less than 24 hours old
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch fresh bookmarks
|
||||||
|
const freshBookmarks = await fetchFreshBookmarks();
|
||||||
|
|
||||||
|
// Cache the results (for daily and manual modes)
|
||||||
|
if (settings.refreshBehavior !== "always") {
|
||||||
|
await setCachedBookmarks(freshBookmarks);
|
||||||
|
}
|
||||||
|
|
||||||
|
return freshBookmarks;
|
||||||
|
}
|
||||||
|
|
||||||
// Get all bookmark folders for settings UI
|
// Get all bookmark folders for settings UI
|
||||||
async function getBookmarkFolders() {
|
async function getBookmarkFolders() {
|
||||||
const tree = await browser.bookmarks.getTree();
|
const tree = await browser.bookmarks.getTree();
|
||||||
|
|
@ -140,11 +210,14 @@ async function getBookmarkFolders() {
|
||||||
return folders;
|
return folders;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle messages from popup/newtab/options
|
// Handle messages from popup/options
|
||||||
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
switch (message.action) {
|
switch (message.action) {
|
||||||
case "getForgottenBookmarks":
|
case "getForgottenBookmarks":
|
||||||
return getForgottenBookmarks();
|
return getForgottenBookmarks(message.forceRefresh || false);
|
||||||
|
|
||||||
|
case "clearCache":
|
||||||
|
return clearCache();
|
||||||
|
|
||||||
case "getBookmarkFolders":
|
case "getBookmarkFolders":
|
||||||
return getBookmarkFolders();
|
return getBookmarkFolders();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "Embermarks",
|
"name": "Embermarks",
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,15 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
input[type="checkbox"] {
|
input[type="checkbox"] {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
|
@ -219,6 +228,18 @@
|
||||||
value="5"
|
value="5"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-label">
|
||||||
|
Refresh behavior
|
||||||
|
<span>When to show new bookmarks</span>
|
||||||
|
</div>
|
||||||
|
<select id="refreshBehavior">
|
||||||
|
<option value="always">Every time</option>
|
||||||
|
<option value="daily">Once a day</option>
|
||||||
|
<option value="manual">Manual only</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
const numBookmarksInput = document.getElementById("numBookmarks");
|
const numBookmarksInput = document.getElementById("numBookmarks");
|
||||||
|
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 folderList = document.getElementById("folderList");
|
const folderList = document.getElementById("folderList");
|
||||||
|
|
@ -12,6 +13,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||||
const settings = await browser.runtime.sendMessage({ action: "getSettings" });
|
const settings = await browser.runtime.sendMessage({ action: "getSettings" });
|
||||||
|
|
||||||
numBookmarksInput.value = settings.numBookmarks;
|
numBookmarksInput.value = settings.numBookmarks;
|
||||||
|
refreshBehaviorSelect.value = settings.refreshBehavior || "always";
|
||||||
maxVisitCountInput.value = settings.maxVisitCount;
|
maxVisitCountInput.value = settings.maxVisitCount;
|
||||||
minAgeDaysInput.value = settings.minAgeDays;
|
minAgeDaysInput.value = settings.minAgeDays;
|
||||||
|
|
||||||
|
|
@ -55,6 +57,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||||
|
|
||||||
const newSettings = {
|
const newSettings = {
|
||||||
numBookmarks: parseInt(numBookmarksInput.value, 10),
|
numBookmarks: parseInt(numBookmarksInput.value, 10),
|
||||||
|
refreshBehavior: refreshBehaviorSelect.value,
|
||||||
maxVisitCount: parseInt(maxVisitCountInput.value, 10),
|
maxVisitCount: parseInt(maxVisitCountInput.value, 10),
|
||||||
minAgeDays: parseInt(minAgeDaysInput.value, 10),
|
minAgeDays: parseInt(minAgeDaysInput.value, 10),
|
||||||
excludedFolders,
|
excludedFolders,
|
||||||
|
|
@ -65,6 +68,9 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||||
settings: newSettings,
|
settings: newSettings,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Clear cache when settings change
|
||||||
|
await browser.runtime.sendMessage({ action: "clearCache" });
|
||||||
|
|
||||||
// Show save confirmation
|
// Show save confirmation
|
||||||
saveStatus.classList.add("visible");
|
saveStatus.classList.add("visible");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,42 @@
|
||||||
// Embermarks - Popup Script
|
// Embermarks - Popup Script
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
const container = document.getElementById('bookmarks');
|
const container = document.getElementById("bookmarks");
|
||||||
const refreshBtn = document.getElementById('refresh');
|
const refreshBtn = document.getElementById("refresh");
|
||||||
|
|
||||||
// Load bookmarks
|
// Load bookmarks (respects cache settings)
|
||||||
await loadBookmarks(container);
|
await loadBookmarks(container, false);
|
||||||
|
|
||||||
// Refresh button
|
// Refresh button always forces a fresh load
|
||||||
refreshBtn.addEventListener('click', async () => {
|
refreshBtn.addEventListener("click", async () => {
|
||||||
await loadBookmarks(container);
|
await loadBookmarks(container, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load and display bookmarks
|
||||||
|
async function loadBookmarks(container, forceRefresh) {
|
||||||
|
showLoading(container);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const bookmarks = await browser.runtime.sendMessage({
|
||||||
|
action: "getForgottenBookmarks",
|
||||||
|
forceRefresh: forceRefresh,
|
||||||
|
});
|
||||||
|
|
||||||
|
while (container.firstChild) {
|
||||||
|
container.removeChild(container.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bookmarks || bookmarks.length === 0) {
|
||||||
|
showEmptyState(container, "none");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const bookmark of bookmarks) {
|
||||||
|
container.appendChild(createBookmarkCard(bookmark));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load bookmarks:", error);
|
||||||
|
showError(container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
27
shared/ui.js
27
shared/ui.js
|
|
@ -151,30 +151,3 @@ function showError(container) {
|
||||||
|
|
||||||
container.appendChild(emptyState);
|
container.appendChild(emptyState);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load and display bookmarks
|
|
||||||
async function loadBookmarks(container) {
|
|
||||||
showLoading(container);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const bookmarks = await browser.runtime.sendMessage({
|
|
||||||
action: "getForgottenBookmarks",
|
|
||||||
});
|
|
||||||
|
|
||||||
while (container.firstChild) {
|
|
||||||
container.removeChild(container.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bookmarks || bookmarks.length === 0) {
|
|
||||||
showEmptyState(container, "none");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const bookmark of bookmarks) {
|
|
||||||
container.appendChild(createBookmarkCard(bookmark));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to load bookmarks:", error);
|
|
||||||
showError(container);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue