Commit Graph

20 Commits

Author SHA1 Message Date
H1K0 94d100675e feat(frontend): make keyboard shortcuts layout-independent
Command keys were matched by character (e.key), so on a non-Latin layout
(e.g. Russian) the physical g/f/e/p/x/j/k keys emitted Cyrillic letters
and nothing fired. Letter and digit commands now match by physical
position (e.code: KeyG, Digit1, Slash, …) across the global nav, the file
grid, and the viewer, so the same physical keys work on any layout. Named
keys (arrows, Enter, Esc, Delete), the Mod combos, and the filter's
literal operators (& | ! ( )) stay on e.key, where character matching is
correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 17:55:39 +03:00
H1K0 49e68cc263 feat(frontend): keyboard nav for the bulk tag editor
The grid's `e` opens the bulk tag editor, which has its own UI rather than
the shared TagPicker, so it needed the same keyboard handling: from the
search input, ↓/↑ highlight a suggestion and Enter adds it to all
selected files (focus stays for chaining); with the input empty ←/→ walk
the assigned tags and Del removes the focused one. Mirrors the viewer's
tag picker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 17:42:02 +03:00
H1K0 3a0dbc9ba7 feat(frontend): keyboard nav for the viewer, tag editor and filter
Viewer: j/k mirror the arrow keys, and `e` scrolls to the (lazy) Tags
section and drops the cursor into its filter, forcing the load so focus
lands even before the section is reached.

Tag picker & filter bar: from the search input, ↓/↑ highlight a
suggestion and Enter adds it (focus stays for chaining); with the input
empty ←/→ walk the added tags/tokens and Del removes the focused one. The
filter bar also inserts an operator token on & | ! ( ), applies on
Ctrl+Enter, resets on Ctrl+Backspace and closes on Esc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 17:40:03 +03:00
H1K0 9a20cc1c84 feat(frontend): keyboard roving-focus and bulk-action keys on the file grid
Arrow keys move a focus ring across the grid (column count derived from
the layout, scrolling the focused card into view and pulling the next
page near the end). Enter opens the focused file; Space/x select; e edits
tags (opening the sheet and focusing its tag filter); p adds to a pool;
Del moves to trash — each falling back to the focused card when nothing
is selected. / opens the filter and focuses its search. The ring only
appears once keyboard navigation starts and is dismissed on pointer use,
so it never distracts mouse users. Escape layering is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 17:30:13 +03:00
H1K0 d357ae3156 feat: open file original in a new tab via authenticated direct link
The file viewer's preview is now a real link (target=_blank) to the original,
instead of fetching it into a blob. A navigation can't send the auth header, so
the access token rides in the query — the auth middleware accepts ?access_token=
as a fallback, but only for GET, so a crafted link can't drive a mutation.

GetContent gains an ?inline=1 toggle (Content-Disposition: inline) so the tab
views the original instead of downloading it; download stays the default.

Documented in openapi.yaml; TestMediaQueryTokenAuth covers GET-with-query-token
(200), missing token (401) and query-token rejected on a non-GET (401).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 15:40:50 +03:00
H1K0 03936243e4 fix(frontend): reflect rule-applied tags in batch edit
BulkTagEditor optimistically marked only the clicked tag as common after a bulk
add, so tags applied by auto-tag rules (resolved server-side) never appeared.
Refetch /files/bulk/common-tags after each change and rebuild the common/partial
sets from the response, so rule-applied tags and partial->common shifts show up.

Backend bulk path was already correct — covered now by TestBulkTagAutoRule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 15:06:25 +03:00
H1K0 a78fc5ba9a feat(backend): log file views
deploy / deploy (push) Successful in 1m0s
The activity.file_views table existed but nothing ever wrote to it. Add a
POST /files/{id}/views endpoint: FileRepo.RecordView inserts a history row,
FileService.RecordView enforces view ACL first. The file viewer fires it
(fire-and-forget) when a file is opened, including while paging prev/next.

Documented in openapi.yaml; covered by TestRecordFileView (204 on view,
repeatable, 404 for unknown file).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 14:49:14 +03:00
H1K0 8f213e780c fix(frontend): sort a file's assigned tags by the chosen tag order
The file viewer's assigned-tags list showed tags in API order, ignoring the
sort the user picked on the tags page. Add a client-side sortTags helper and
order the assigned list (in TagPicker) by tagSorting, reactively so it re-sorts
the moment the sort changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 14:22:23 +03:00
H1K0 88a8cac048 fix(frontend): load full tag/category lists in pickers, honoring sort
deploy / deploy (push) Successful in 19s
The tag pickers, filter bar and batch editor loaded only /tags?limit=200 and
filtered client-side, so with more than 200 tags the rest were invisible and
unsearchable. Same for the category dropdowns on the tag forms.

Add fetchAllTags / fetchAllCategories helpers that page past the server's
per-request cap of 200, and order results by the sort the user chose on the
tags / categories page (tagSorting / categorySorting) instead of a hardcoded
name-asc. Wire them into FilterBar, TagPicker, TagRuleEditor, BulkTagEditor and
the tag new/edit category dropdowns.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 13:58:23 +03:00
H1K0 0e7890a465 style(project): format Go with gofmt, set up Prettier for the frontend
Run gofmt -w across the backend, normalising the manually-aligned := blocks
to the gofmt standard. No code behaviour changes.

Add Prettier (+ prettier-plugin-svelte) to the frontend with the SvelteKit
default config (tabs, single quotes) so formatting is reproducible, then run
it over the whole tree. Add format / format:check npm scripts and a
.prettierignore (build output, generated schema.ts, static assets).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 11:01:29 +03:00
H1K0 fa491487b7 feat(frontend): open file viewer as overlay over the mounted list
The viewer was a separate /files/[id] route, so returning tore down and
reloaded the whole grid. Now opening a file uses SvelteKit shallow
routing (pushState + page.state.fileId): the list stays mounted and the
viewer renders as a full-screen overlay on top of it, like Immich. The
URL still becomes /files/<id> and the back button (or Escape/close)
dismisses the overlay via history.back(), revealing the untouched grid —
no reload — then scrolls it to the last-viewed file instantly.

- Extract the viewer UI/logic into a reusable FileViewer component
  (file fetch, preview, lazy tags, save, prev/next, keyboard).
- List: neighbours come straight from its own files[]; paging past the
  loaded set pulls the next page by cursor (prefetch near the end).
- Paging uses replaceState so one back press returns to the grid.
- /files/[id] remains as a thin standalone fallback for deep links /
  hard reloads, resolving neighbours via the anchor API and returning
  to the grid with ?anchor=<id>.
- Remove the now-unused filesCache snapshot store (the list is never
  unmounted, so there's nothing to snapshot/restore).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 00:02:25 +03:00
H1K0 9b1aa40522 feat(frontend): implement bulk tag editing for multi-file selection
- Add BulkTagEditor component: loads common/partial tags via
  POST /files/bulk/common-tags and applies changes via POST /files/bulk/tags
- Common tags shown solid with × to remove from all files
- Partial tags shown with dashed border and ~ indicator; clicking promotes
  to common (adds to files that are missing it)
- Wire "Edit tags" button in SelectionBar to a bottom sheet with the editor
- Add mock handlers for /files/bulk/common-tags and /files/bulk/tags

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 08:40:53 +03:00
H1K0 6e24060d99 feat(frontend): add clear button to TagPicker search input
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 23:16:49 +03:00
H1K0 b9cace2997 feat(frontend): implement file upload with drag-and-drop and per-file progress
- client.ts: add uploadWithProgress() using XHR for upload progress events
- FileUpload.svelte: drag-drop zone wrapper, multi-file queue with individual
  progress bars, success/error status, MIME rejection message, dismiss panel
- Header.svelte: optional onUpload prop renders upload icon button
- files/+page.svelte: wire upload button, prepend uploaded files to grid
- vite-mock-plugin.ts: handle POST /files, unshift new file into mock array
- Fix crypto.randomUUID() crash on non-secure HTTP context (use Date.now + Math.random)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 14:02:26 +03:00
H1K0 a5b610d472 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>
2026-04-05 13:55:04 +03:00
H1K0 6fa340b17c feat(frontend): make header and filter bar sticky on scroll
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 13:32:35 +03:00
H1K0 aebf7127af feat(frontend): implement file selection with long-press, shift+click, and touch drag
- selection.ts: store with select/deselect/toggle/enter/exit, derived count and active
- FileCard: long-press (400ms) enters selection mode, shows check overlay, blocks context menu
- Header: Select/Cancel button toggles selection mode
- SelectionBar: floating bar above navbar with count, Edit tags, Add to pool, Delete
- Shift+click range-selects between last and current index (desktop)
- Touch drag-to-select/deselect after long-press; non-passive touchmove blocks scroll only during drag

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 13:30:26 +03:00
H1K0 63ea1a4d6a feat(frontend): make filter expression tokens draggable for reordering
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 12:57:45 +03:00
H1K0 27d8215a0a feat(frontend): add header, filter bar, and sorting store for files page
- sorting.ts: per-section sort store (sort field + order) persisted to localStorage
- dsl.ts: build/parse DSL filter strings ({t=uuid,&,|,!,...})
- Header.svelte: sort dropdown, asc/desc toggle, filter toggle button
- FilterBar.svelte: tag token picker with operator buttons, search, apply/reset
- files/+page.svelte: wired header + filter bar, resets pagination on sort/filter change
- vite-mock-plugin.ts: added 5 mock tags for filter bar development

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 12:47:18 +03:00
H1K0 e72d4822e9 feat(frontend): implement file gallery page with infinite scroll
Adds InfiniteScroll component (IntersectionObserver, 300px margin,
CSS spinner). Adds FileCard component (fetch thumbnail with JWT auth
header, blob URL, shimmer placeholder). Adds files/+page.svelte with
160×160 flex-wrap grid and cursor pagination. Updates mock plugin with
75 sample files, cursor pagination, and colored SVG thumbnail handler.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 03:34:33 +03:00