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();
@@ -37,6 +38,7 @@
+
@@ -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}