diff --git a/frontend/src/app.css b/frontend/src/app.css index b6e8988..bdfc863 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -11,6 +11,7 @@ --color-danger: #db6060; --color-info: #4dc7ed; --color-warning: #f5e872; + --color-success: #5fb87a; --color-tag-default: #444455; --color-nav-bg: rgba(0, 0, 0, 0.45); --color-nav-active: rgba(52, 50, 73, 0.72); diff --git a/frontend/src/lib/components/file/FileCard.svelte b/frontend/src/lib/components/file/FileCard.svelte index 7ea084e..a6decee 100644 --- a/frontend/src/lib/components/file/FileCard.svelte +++ b/frontend/src/lib/components/file/FileCard.svelte @@ -132,6 +132,9 @@
{/if}
+ {#if file.needs_review} +
+ {/if} {#if selected} + +
+ + + +
+ void; onAddToPool: () => void; + onMarkReviewed: () => void; onDelete: () => void; } - let { onEditTags, onAddToPool, onDelete }: Props = $props(); + let { onEditTags, onAddToPool, onMarkReviewed, onDelete }: Props = $props(); @@ -140,6 +142,14 @@ background-color: color-mix(in srgb, var(--color-warning) 15%, transparent); } + .mark-reviewed { + color: var(--color-success); + } + + .mark-reviewed:hover { + background-color: color-mix(in srgb, var(--color-success) 15%, transparent); + } + .delete { color: var(--color-danger); } diff --git a/frontend/src/lib/utils/dsl.ts b/frontend/src/lib/utils/dsl.ts index 6406be3..6966389 100644 --- a/frontend/src/lib/utils/dsl.ts +++ b/frontend/src/lib/utils/dsl.ts @@ -5,6 +5,7 @@ * t= — has tag * m= — exact MIME * m~ — MIME LIKE pattern + * r=1 / r=0 — needs review / review done * ( ) & | ! — grouping / boolean operators * * Example: {t=uuid1,&,!,t=uuid2} → has tag1 AND NOT tag2 @@ -31,6 +32,8 @@ export function tokenLabel(token: string, tagNames: Map): string if (token === '!') return 'NOT'; if (token === '(') return '('; if (token === ')') return ')'; + if (token === 'r=1') return 'Needs review'; + if (token === 'r=0') return 'Reviewed'; if (token.startsWith('t=')) { const id = token.slice(2); return tagNames.get(id) ?? token; diff --git a/frontend/src/routes/files/+page.svelte b/frontend/src/routes/files/+page.svelte index dc1a5b8..f3f2b04 100644 --- a/frontend/src/routes/files/+page.svelte +++ b/frontend/src/routes/files/+page.svelte @@ -114,6 +114,22 @@ void tick().then(() => document.querySelector('.tag-sheet input')?.focus()); } + // Mark the current selection as review-done (tagging finished). Best-effort + // optimistic update of the local list so the "needs review" badges clear. + async function markSelectionReviewed() { + const ids = [...$selectionStore.ids]; + if (ids.length === 0) return; + selectionStore.exit(); + try { + await api.post('/files/bulk/review', { file_ids: ids, needs_review: false }); + files = files.map((f) => + ids.includes(f.id ?? '') ? { ...f, needs_review: false } : f + ); + } catch { + // ignore — list already reflects the intended state optimistically + } + } + function openFilterAndFocus() { filterOpen = true; void tick().then(() => document.querySelector('.bar .search')?.focus()); @@ -835,6 +851,8 @@ nextId={viewerNextId} onNavigate={pageTo} onClose={closeViewer} + onReviewChange={(id, nr) => + (files = files.map((f) => (f.id === id ? { ...f, needs_review: nr } : f)))} /> {/if} @@ -843,6 +861,7 @@ (confirmDeleteFiles = true)} /> {/if}