diff --git a/frontend/src/lib/components/file/FileViewer.svelte b/frontend/src/lib/components/file/FileViewer.svelte
index a936447..eb0bffe 100644
--- a/frontend/src/lib/components/file/FileViewer.svelte
+++ b/frontend/src/lib/components/file/FileViewer.svelte
@@ -4,6 +4,7 @@
import { api, ApiError } from '$lib/api/client';
import { authStore } from '$lib/stores/auth';
import TagPicker from '$lib/components/file/TagPicker.svelte';
+ import PoolPicker from '$lib/components/file/PoolPicker.svelte';
import type { File, Tag } from '$lib/api/types';
interface Props {
@@ -26,6 +27,7 @@
let loading = $state(true);
let saving = $state(false);
let error = $state('');
+ let poolPickerOpen = $state(false);
// Tags are loaded lazily — the Tags section sits below a full-viewport
// preview, so fetching them on open just hammers the DB for data the user
@@ -196,6 +198,17 @@
}
function handleKeydown(e: KeyboardEvent) {
+ // While the pool picker is open it owns the keyboard: Escape closes it
+ // (even from its search field), and every other key is swallowed so the
+ // viewer's shortcuts don't fire behind the modal. Typing still works —
+ // non-Escape keys aren't prevented, only ignored here.
+ if (poolPickerOpen) {
+ if (e.key === 'Escape') {
+ e.preventDefault();
+ poolPickerOpen = false;
+ }
+ return;
+ }
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
if (e.ctrlKey || e.metaKey || e.altKey) return;
// Letter keys are matched by physical position (e.code) so j/k/e work on any
@@ -265,6 +278,32 @@
{file?.original_name ?? ''}
+ {#if file}
+
+ {/if}
@@ -409,6 +448,10 @@
+{#if poolPickerOpen && file}
+ (poolPickerOpen = false)} />
+{/if}
+
diff --git a/frontend/src/routes/files/+page.svelte b/frontend/src/routes/files/+page.svelte
index ebd7e4e..dc1a5b8 100644
--- a/frontend/src/routes/files/+page.svelte
+++ b/frontend/src/routes/files/+page.svelte
@@ -15,9 +15,10 @@
import { selectionStore, selectionActive } 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';
import { tick, flushSync } from 'svelte';
import { parseDslFilter } from '$lib/utils/dsl';
- import type { File, FileCursorPage, Pool, PoolOffsetPage } from '$lib/api/types';
+ import type { File, FileCursorPage } from '$lib/api/types';
import { appSettings } from '$lib/stores/appSettings';
// What the section cache stores for the Files grid. `resetKey` guards against
@@ -207,44 +208,14 @@
}
// ---- Add to pool picker ----
+ // The picker itself (load, search, add) lives in PoolPicker; here we just
+ // gate it open and clear the selection once files land in a pool.
let poolPickerOpen = $state(false);
- let pools = $state([]);
- let poolsLoading = $state(false);
- let poolPickerSearch = $state('');
- let poolPickerError = $state('');
- async function openPoolPicker() {
+ function openPoolPicker() {
poolPickerOpen = true;
- poolPickerError = '';
- poolsLoading = true;
- poolPickerSearch = '';
- try {
- const res = await api.get('/pools?limit=200&sort=name&order=asc');
- pools = res.items ?? [];
- } catch {
- poolPickerError = 'Failed to load pools';
- } finally {
- poolsLoading = false;
- }
}
- async function addToPool(poolId: string) {
- const ids = [...$selectionStore.ids];
- poolPickerOpen = false;
- selectionStore.exit();
- try {
- await api.post(`/pools/${poolId}/files`, { file_ids: ids });
- } catch {
- // silently ignore
- }
- }
-
- let filteredPools = $derived(
- poolPickerSearch.trim()
- ? pools.filter((p) => p.name?.toLowerCase().includes(poolPickerSearch.toLowerCase()))
- : pools
- );
-
function handleUploaded(file: File) {
files = [file, ...files];
}
@@ -904,52 +875,11 @@
{/if}
{#if poolPickerOpen}
-
-
(poolPickerOpen = false)}>
-
-
- Add {$selectionStore.ids.size} file{$selectionStore.ids.size !== 1 ? 's' : ''} to pool
-
-