feat(photos): full EXIF metadata extraction, backfill and info panel

- backend: switch EXIF parser to dsoprea/go-exif/v3 (all recognised tags, numeric normalization); store in photos.exif jsonb (migration 000003); add storage.Get and cmd/backfill-exif to backfill existing photos from S3 originals
- frontend: info panel renders all EXIF attributes with RU labels, enum translation and units (1/N, f/N, mm, EV); service tags hidden
- viewer: remove make-cover action (cover managed from panel only), icon-only download
- openspec: sync lightroom-workspace-ui artifacts (EXIF, cover-from-panel) and add plan for configurable info attributes/labels stored in user profile
This commit is contained in:
iu
2026-10-10 20:31:34 +03:00
parent d8da885020
commit e1592549b2
20 changed files with 744 additions and 108 deletions
@@ -4,7 +4,6 @@
import { api } from '$lib/api';
import Viewer from '$lib/components/Viewer.svelte';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import { albums } from '$lib/stores/albums.svelte';
import { auth } from '$lib/stores/auth.svelte';
import type { Album, Photo } from '$lib/types';
@@ -15,7 +14,6 @@
let role = $state('');
let loading = $state(true);
let error = $state('');
let saved = $state(false);
let pendingDelete = $state<Photo | null>(null);
let deleting = $state(false);
@@ -51,17 +49,6 @@
if (p) goto(`/albums/${albumId}/photos/${p.id}`, { replaceState: true, noScroll: true });
}
async function makeCover(photo: Photo) {
if (!canEdit) return;
try {
await albums.setCover(albumId, photo.id);
saved = true;
setTimeout(() => (saved = false), 2000);
} catch (e) {
error = (e as Error).message;
}
}
async function removePhoto(photo: Photo) {
pendingDelete = photo;
}
@@ -104,18 +91,13 @@
</div>
{:else}
<div class="flex h-full flex-col">
{#if saved}
<p class="bg-success/10 text-success px-3 py-1 text-center text-xs">Обложка обновлена</p>
{/if}
<div class="min-h-0 flex-1">
<Viewer
{photos}
{index}
{canEdit}
{canDelete}
onClose={close}
onIndexChange={change}
onMakeCover={makeCover}
onDelete={removePhoto}
/>
</div>