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)
This commit is contained in:
@@ -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<string, unknown>);
|
||||
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<string, unknown>);
|
||||
|
||||
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';
|
||||
</script>
|
||||
|
||||
{#if photoId}
|
||||
{#if photo}
|
||||
{#if panelPhoto}
|
||||
{@const p = panelPhoto}
|
||||
<div>
|
||||
<div class="p-4">
|
||||
{#if editingName}
|
||||
@@ -195,10 +210,10 @@
|
||||
title="Переименовать"
|
||||
onclick={startEdit}
|
||||
>
|
||||
{photo.original_filename}
|
||||
{p.original_filename}
|
||||
</button>
|
||||
{:else}
|
||||
<h2 class="text-text truncate text-sm font-semibold">{photo.original_filename}</h2>
|
||||
<h2 class="text-text truncate text-sm font-semibold">{p.original_filename}</h2>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -218,12 +233,12 @@
|
||||
</button>
|
||||
{/snippet}
|
||||
<dl class="space-y-1">
|
||||
<div class={metaRow}><span>Автор</span><span class="text-text">{photo.uploader.name}</span></div>
|
||||
<div class={metaRow}><span>Файл</span><span class="text-text">{formatSize(photo.size)}</span></div>
|
||||
<div class={metaRow}><span>Тип</span><span class="text-text">{photo.mime_type}</span></div>
|
||||
<div class={metaRow}><span>Автор</span><span class="text-text">{p.uploader.name}</span></div>
|
||||
<div class={metaRow}><span>Файл</span><span class="text-text">{formatSize(p.size)}</span></div>
|
||||
<div class={metaRow}><span>Тип</span><span class="text-text">{p.mime_type}</span></div>
|
||||
<div class={metaRow}>
|
||||
<span>Разрешение</span>
|
||||
<span class="text-text">{photo.width}×{photo.height}</span>
|
||||
<span class="text-text">{p.width}×{p.height}</span>
|
||||
</div>
|
||||
{#each showAll ? allExifRows : visibleExifRows as row (row.key)}
|
||||
<div class={metaRow}>
|
||||
@@ -242,27 +257,27 @@
|
||||
…
|
||||
</button>
|
||||
{/if}
|
||||
{#if photo.taken_at}
|
||||
{#if p.taken_at}
|
||||
<div class={metaRow}>
|
||||
<span>Снято</span>
|
||||
<span class="text-text">{new Date(photo.taken_at).toLocaleString()}</span>
|
||||
<span class="text-text">{new Date(p.taken_at).toLocaleString()}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if photo.latitude != null && photo.longitude != null}
|
||||
{#if p.latitude != null && p.longitude != null}
|
||||
<div class={metaRow}>
|
||||
<span>Координаты</span>
|
||||
<span class="text-text">{photo.latitude.toFixed(5)}, {photo.longitude.toFixed(5)}</span>
|
||||
<span class="text-text">{p.latitude.toFixed(5)}, {p.longitude.toFixed(5)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class={metaRow}>
|
||||
<span>Загружено</span>
|
||||
<span class="text-text">{new Date(photo.created_at).toLocaleString()}</span>
|
||||
<span class="text-text">{new Date(p.created_at).toLocaleString()}</span>
|
||||
</div>
|
||||
</dl>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection id="photo-comments" title="Комментарии">
|
||||
<Comments photoId={photo.id} />
|
||||
<Comments photoId={p.id} />
|
||||
</PanelSection>
|
||||
</div>
|
||||
|
||||
@@ -278,8 +293,7 @@
|
||||
onClose={() => (settingsOpen = false)}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{:else if album}
|
||||
{:else if album && !photoId}
|
||||
<div>
|
||||
<PanelSection id="album-cover" title="Обложка">
|
||||
<div class="bg-bg-muted border-border aspect-video overflow-hidden rounded-lg border">
|
||||
|
||||
Reference in New Issue
Block a user