feat(icons): preserve user-uploaded icons from bulk fetch/clear
Auto-fetched favicons and user uploads shared one icon_b64 column, so
"Clear cache" wiped custom uploads and "Re-fetch all" overwrote them — data
loss the user can't undo (a custom icon exists precisely because DDG can't
fetch that site).
New icon_custom flag (0=fetched cache, 1=user upload). Invariant: an
automatic action never touches a custom icon; only the user changes/removes
it per entry.
- Schema: icon_custom INTEGER on vault_entries.
- POST /entries/{id}/icon takes {custom}; GET /entries echoes icon_custom;
DELETE /entries/icons/all filters COALESCE(icon_custom,0)=0.
- saveEntryIcon(id, uri, isCustom): upload→true, fetch/remove→false.
- backfillFavicons skips icon_custom even on force (Re-fetch all).
- clearAllFavicons clears only non-custom locally + refreshes the open editor
(the icon preview didn't update on clear before).
- duplicate re-stamps a copied custom icon; import-overwrite carries the flag.
Not propagated across bulk-import / sync (the bulk INSERT doesn't read it) —
a restored icon counts as fetched. Documented; custom icons are typically
self-hosted sites DDG won't re-fetch anyway.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4319,8 +4319,9 @@ function soIconField(entry) {
|
||||
reader.onload = async ev => {
|
||||
const dataUri = ev.target.result; // 'data:image/...;base64,...'
|
||||
entry.icon_b64 = dataUri;
|
||||
entry.icon_custom = 1;
|
||||
paintPreview(dataUri);
|
||||
await saveEntryIcon(entry.id, dataUri);
|
||||
await saveEntryIcon(entry.id, dataUri, true); // user upload → custom
|
||||
// Refresh entry cards / health view so they show the new icon too.
|
||||
render();
|
||||
toast('Custom icon set');
|
||||
@@ -4332,8 +4333,9 @@ function soIconField(entry) {
|
||||
'Remove');
|
||||
resetBtn.addEventListener('click', async () => {
|
||||
entry.icon_b64 = null;
|
||||
entry.icon_custom = 0;
|
||||
paintPreview(null);
|
||||
await saveEntryIcon(entry.id, '');
|
||||
await saveEntryIcon(entry.id, '', false);
|
||||
render();
|
||||
toast('Icon removed');
|
||||
});
|
||||
@@ -5113,6 +5115,13 @@ async function duplicateEntry(entry) {
|
||||
})),
|
||||
});
|
||||
|
||||
// The create body sets icon_b64 but not provenance (defaults to
|
||||
// fetched). If the source was a user upload, re-stamp the copy as
|
||||
// custom so it's preserved from bulk fetch/clear like the original.
|
||||
if (r && typeof r.id === 'number' && entry.icon_custom && entry.icon_b64) {
|
||||
await saveEntryIcon(r.id, entry.icon_b64, true);
|
||||
}
|
||||
|
||||
// Copy attachments. The source's encrypted blobs are keyed to the
|
||||
// current vault key, so we just download → re-upload (no re-encrypt
|
||||
// needed; same key on both sides).
|
||||
|
||||
Reference in New Issue
Block a user