diff --git a/frontend/src/lib/components/file/FileViewer.svelte b/frontend/src/lib/components/file/FileViewer.svelte index 273c1f8..2e4f18c 100644 --- a/frontend/src/lib/components/file/FileViewer.svelte +++ b/frontend/src/lib/components/file/FileViewer.svelte @@ -554,7 +554,7 @@
- + - import { selectionStore, selectionCount } from '$lib/stores/selection'; - interface Props { + count: number; + onClear: () => void; onEditTags: () => void; onAddToPool: () => void; onMarkReviewed: () => void; onDelete: () => void; + // Optional: only shown in a pool context (removes the files from that pool + // without deleting them). Omitted on the global files list. + onRemoveFromPool?: () => void; } - let { onEditTags, onAddToPool, onMarkReviewed, onDelete }: Props = $props(); + let { + count, + onClear, + onEditTags, + onAddToPool, + onMarkReviewed, + onDelete, + onRemoveFromPool + }: Props = $props(); @@ -72,6 +86,7 @@ .row { display: flex; align-items: center; + flex-wrap: wrap; gap: 4px; } @@ -124,6 +139,7 @@ font-size: 0.85rem; font-family: inherit; font-weight: 600; + white-space: nowrap; } .edit-tags { @@ -150,6 +166,15 @@ background-color: color-mix(in srgb, var(--color-success) 15%, transparent); } + .remove-pool { + color: var(--color-text-muted); + } + + .remove-pool:hover { + background-color: color-mix(in srgb, var(--color-accent) 15%, transparent); + color: var(--color-text-primary); + } + .delete { color: var(--color-danger); } diff --git a/frontend/src/routes/files/+page.svelte b/frontend/src/routes/files/+page.svelte index c715655..e2ecd9f 100644 --- a/frontend/src/routes/files/+page.svelte +++ b/frontend/src/routes/files/+page.svelte @@ -12,7 +12,7 @@ import SelectionBar from '$lib/components/layout/SelectionBar.svelte'; import InfiniteScroll from '$lib/components/common/InfiniteScroll.svelte'; import { fileSorting, type FileSortField } from '$lib/stores/sorting'; - import { selectionStore, selectionActive } from '$lib/stores/selection'; + import { selectionStore, selectionActive, selectionCount } from '$lib/stores/selection'; import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; import BulkTagEditor from '$lib/components/file/BulkTagEditor.svelte'; import PoolPicker from '$lib/components/file/PoolPicker.svelte'; @@ -784,6 +784,8 @@ {#if $selectionActive} selectionStore.exit()} onEditTags={openTagEditor} onAddToPool={openPoolPicker} onMarkReviewed={markSelectionReviewed} diff --git a/frontend/src/routes/pools/[id]/+page.svelte b/frontend/src/routes/pools/[id]/+page.svelte index 0a2c1af..177b9e7 100644 --- a/frontend/src/routes/pools/[id]/+page.svelte +++ b/frontend/src/routes/pools/[id]/+page.svelte @@ -7,6 +7,8 @@ import FileViewer from '$lib/components/file/FileViewer.svelte'; import FilterBar from '$lib/components/file/FilterBar.svelte'; import PoolPicker from '$lib/components/file/PoolPicker.svelte'; + import BulkTagEditor from '$lib/components/file/BulkTagEditor.svelte'; + import SelectionBar from '$lib/components/layout/SelectionBar.svelte'; import InfiniteScroll from '$lib/components/common/InfiniteScroll.svelte'; import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; import { parseDslFilter } from '$lib/utils/dsl'; @@ -44,11 +46,13 @@ let filterOpen = $state(false); let activeTokens = $derived(parseDslFilter(filterParam)); - // ---- Selection (for removal) ---- + // ---- Selection + bulk actions (mirrors the files page) ---- let selectedIds = $state(new Set()); let selectionMode = $derived(selectedIds.size > 0); let lastSelectedIdx = $state(null); let confirmRemove = $state(false); + let confirmDeleteFiles = $state(false); + let tagEditorOpen = $state(false); let poolPickerOpen = $state(false); // ---- Add files mode ---- @@ -238,10 +242,20 @@ lastSelectedIdx = idx; } + function clearSelection() { + selectedIds = new Set(); + lastSelectedIdx = null; + } + + function openTagEditor() { + tagEditorOpen = true; + void tick().then(() => document.querySelector('.tag-sheet input')?.focus()); + } + async function removeSelected() { confirmRemove = false; const ids = [...selectedIds]; - selectedIds = new Set(); + clearSelection(); try { await api.post(`/pools/${poolId}/files/remove`, { file_ids: ids }); files = files.filter((f) => !ids.includes(f.id ?? '')); @@ -251,6 +265,34 @@ } } + // Mark the selection as review-done; optimistically clear the local badges. + async function markSelectionReviewed() { + const ids = [...selectedIds]; + if (ids.length === 0) return; + clearSelection(); + 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 — the list already reflects the intended state + } + } + + // Move the selection to trash. Trashed files drop out of the pool view (which + // only lists live files); pool membership is kept, so file_count is left to + // the server's truth on the next load rather than adjusted optimistically. + async function deleteSelected() { + confirmDeleteFiles = false; + const ids = [...selectedIds]; + clearSelection(); + try { + await api.post('/files/bulk/delete', { file_ids: ids }); + files = files.filter((f) => !ids.includes(f.id ?? '')); + } catch { + // silently ignore — list already updated optimistically + } + } + // ---- File viewer overlay (shallow routing) ---- // Open the viewer on top of the still-mounted pool grid so the back button (and // the viewer's own close) returns here — with the pool's list and scroll intact — @@ -629,8 +671,8 @@ aria-label="Sort files" > - - + + {#if sortKey !== 'manual'} @@ -739,46 +781,61 @@ {/if} - + {#if selectionMode && !addMode} -