From 78996bc053e6294972f0633bc3243fa0fed5421a Mon Sep 17 00:00:00 2001 From: Iu Date: Sat, 10 Oct 2026 21:33:59 +0300 Subject: [PATCH] feat(frontend): select photo in album grid and show info in panel - single click selects a photo (ring + aria-pressed) and shows its info in the right panel without navigating; double click opens the viewer - shared 'selection' store; panel uses the route photo in viewer mode and the selected photo in album mode; selection resets on album change - openspec: sync lightroom-workspace-ui (grid-selection requirement, viewer chrome wording without timers, nested f fullscreen, tasks 9.9/9.10) --- frontend/src/lib/components/AlbumPanel.svelte | 60 ++++++++++++------- frontend/src/lib/components/PhotoGrid.svelte | 13 +++- frontend/src/lib/stores/selection.svelte.ts | 18 ++++++ .../src/routes/(app)/albums/[id]/+page.svelte | 7 +++ .../changes/lightroom-workspace-ui/design.md | 20 +++++-- .../lightroom-workspace-ui/proposal.md | 1 + .../specs/ui-workspace/spec.md | 24 +++++++- .../changes/lightroom-workspace-ui/tasks.md | 4 +- 8 files changed, 114 insertions(+), 33 deletions(-) create mode 100644 frontend/src/lib/stores/selection.svelte.ts diff --git a/frontend/src/lib/components/AlbumPanel.svelte b/frontend/src/lib/components/AlbumPanel.svelte index 37ee6a9..6a705f5 100644 --- a/frontend/src/lib/components/AlbumPanel.svelte +++ b/frontend/src/lib/components/AlbumPanel.svelte @@ -8,6 +8,7 @@ import PhotoInfoSettingsDialog from './PhotoInfoSettingsDialog.svelte'; import { albums } from '$lib/stores/albums.svelte'; import { auth } from '$lib/stores/auth.svelte'; + import { selection } from '$lib/stores/selection.svelte'; import { buildExifRows, EXIF_BASE, EXIF_CATALOG, EXIF_LABELS } from '$lib/exif'; import type { Album, Photo, Preferences } from '$lib/types'; @@ -25,10 +26,15 @@ let savingName = $state(false); let nameError = $state(''); - const canEdit = $derived(role === 'owner' || role === 'admin'); - const canRename = $derived(!!photo && (canEdit || photo.uploader_id === auth.user?.id)); + const selectedPhoto = $derived(selection.albumId === albumId ? selection.photo : null); + const panelPhoto = $derived(photoId ? photo : selectedPhoto); - const exif = $derived((photo?.exif ?? {}) as Record); + const canEdit = $derived(role === 'owner' || role === 'admin'); + const canRename = $derived( + !!panelPhoto && (canEdit || panelPhoto.uploader_id === auth.user?.id) + ); + + const exif = $derived((panelPhoto?.exif ?? {}) as Record); const prefs = $derived(auth.preferences); const labelOverrides = $derived(prefs.photo_info?.labels ?? {}); @@ -97,24 +103,33 @@ .catch((e) => (error = (e as Error).message)); }); + $effect(() => { + // сброс временного состояния панели при выборе другого фото в сетке + selectedPhoto?.id; + editingName = false; + nameError = ''; + showAll = false; + }); + function startEdit() { - if (!photo) return; - nameDraft = photo.original_filename; + if (!panelPhoto) return; + nameDraft = panelPhoto.original_filename; nameError = ''; editingName = true; } async function saveName(ev: Event) { ev.preventDefault(); - if (!photo || !nameDraft.trim()) return; + if (!panelPhoto || !nameDraft.trim()) return; savingName = true; nameError = ''; try { - const data = await api<{ photo: Photo }>(`/photos/${photo.id}`, { + const data = await api<{ photo: Photo }>(`/photos/${panelPhoto.id}`, { method: 'PATCH', body: { original_filename: nameDraft.trim() } }); photo = data.photo; + if (selectedPhoto?.id === data.photo.id) selection.select(albumId, data.photo); editingName = false; } catch (e) { nameError = (e as Error).message; @@ -154,8 +169,8 @@ const metaRow = 'text-text-muted flex justify-between gap-2 py-0.5 text-xs'; -{#if photoId} - {#if photo} +{#if panelPhoto} + {@const p = panelPhoto}
{#if editingName} @@ -195,10 +210,10 @@ title="Переименовать" onclick={startEdit} > - {photo.original_filename} + {p.original_filename} {:else} -

{photo.original_filename}

+

{p.original_filename}

{/if}
@@ -218,12 +233,12 @@ {/snippet}
-
Автор{photo.uploader.name}
-
Файл{formatSize(photo.size)}
-
Тип{photo.mime_type}
+
Автор{p.uploader.name}
+
Файл{formatSize(p.size)}
+
Тип{p.mime_type}
Разрешение - {photo.width}×{photo.height} + {p.width}×{p.height}
{#each showAll ? allExifRows : visibleExifRows as row (row.key)}
@@ -242,27 +257,27 @@ … {/if} - {#if photo.taken_at} + {#if p.taken_at}
Снято - {new Date(photo.taken_at).toLocaleString()} + {new Date(p.taken_at).toLocaleString()}
{/if} - {#if photo.latitude != null && photo.longitude != null} + {#if p.latitude != null && p.longitude != null}
Координаты - {photo.latitude.toFixed(5)}, {photo.longitude.toFixed(5)} + {p.latitude.toFixed(5)}, {p.longitude.toFixed(5)}
{/if}
Загружено - {new Date(photo.created_at).toLocaleString()} + {new Date(p.created_at).toLocaleString()}
- +
@@ -278,8 +293,7 @@ onClose={() => (settingsOpen = false)} /> {/if} - {/if} -{:else if album} +{:else if album && !photoId}
diff --git a/frontend/src/lib/components/PhotoGrid.svelte b/frontend/src/lib/components/PhotoGrid.svelte index bc5bacc..a80dc38 100644 --- a/frontend/src/lib/components/PhotoGrid.svelte +++ b/frontend/src/lib/components/PhotoGrid.svelte @@ -5,10 +5,12 @@ interface Props { photos: Photo[]; + selectedId?: string | null; + onSelect?: (index: number) => void; onOpen?: (index: number) => void; } - let { photos, onOpen }: Props = $props(); + let { photos, selectedId = null, onSelect, onOpen }: Props = $props(); {#if photos.length === 0} @@ -22,8 +24,13 @@