feat(frontend): implement file viewer page with metadata editing and tag picker

- files/[id]/+page.svelte: full-screen preview (100dvh), sticky top bar,
  prev/next nav via anchor API, notes/datetime/is_public editing, TagPicker,
  EXIF display, keyboard navigation (←/→/Esc)
- TagPicker.svelte: assigned tags with remove, searchable available tags to add
- Fix infinite request loop: previewSrc read inside $effect tracked as dependency;
  wrapped in untrack() to prevent re-triggering on blob URL assignment
- vite-mock-plugin: add GET/PATCH /files/{id}, preview endpoint, tags CRUD,
  anchor-based pagination, in-memory mutable state for file overrides and tags
- files/+page.svelte: migrate from deprecated $app/stores to $app/state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-05 13:55:04 +03:00
parent 84c47d0282
commit a5b610d472
4 changed files with 882 additions and 6 deletions
+3 -3
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { page } from '$app/stores';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import { api } from '$lib/api/client';
import { ApiError } from '$lib/api/client';
@@ -29,7 +29,7 @@
let error = $state('');
let filterOpen = $state(false);
let filterParam = $derived($page.url.searchParams.get('filter'));
let filterParam = $derived(page.url.searchParams.get('filter'));
let activeTokens = $derived(parseDslFilter(filterParam));
let sortState = $derived($fileSorting);
@@ -71,7 +71,7 @@
}
function applyFilter(filter: string | null) {
const url = new URL($page.url);
const url = new URL(page.url);
if (filter) {
url.searchParams.set('filter', filter);
} else {