From eb3cf72969b2f783a8a7cda3a9264f79b285e9ab Mon Sep 17 00:00:00 2001 From: Masahiko AMANO Date: Fri, 3 Jul 2026 08:50:21 +0300 Subject: [PATCH] feat(frontend): richer pool multi-select and clearer date label Give the in-pool file selection the same action set as the files grid. SelectionBar is now driven by props (count + callbacks) instead of reading the selection store directly, with an optional "Remove from pool" action. The pool page uses it to offer Edit tags, Add to pool, Mark reviewed, Remove from pool and Delete (to trash), replacing the previous add/remove-only bar. Also rename the content_datetime label from the photo-centric "Date taken" to "Content date" across the files sort, the file viewer field and the pool sort options (and align the pool's "created" option with the files grid). Co-Authored-By: Claude Opus 4.8 --- .../src/lib/components/file/FileViewer.svelte | 2 +- .../lib/components/layout/SelectionBar.svelte | 35 ++- frontend/src/routes/files/+page.svelte | 4 +- frontend/src/routes/pools/[id]/+page.svelte | 248 +++++++++++------- 4 files changed, 180 insertions(+), 109 deletions(-) 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} -