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
+9 -25
View File
@@ -7,24 +7,13 @@
interface Props {
photos: Photo[];
index: number;
canEdit?: boolean;
canDelete?: boolean;
onClose: () => void;
onIndexChange: (index: number) => void;
onMakeCover?: (photo: Photo) => void;
onDelete?: (photo: Photo) => void;
}
let {
photos,
index,
canEdit = false,
canDelete = false,
onClose,
onIndexChange,
onMakeCover,
onDelete
}: Props = $props();
let { photos, index, canDelete = false, onClose, onIndexChange, onDelete }: Props = $props();
let fresh = $state<Photo | null>(null);
let touchX = 0;
@@ -398,21 +387,16 @@
>
<Icon name={isFullscreen ? 'fullscreen_exit' : 'fullscreen'} />
</button>
{#if canEdit}
<button
type="button"
class={btn}
title="Сделать обложкой"
onclick={() => current && onMakeCover?.(current)}
>
<Icon name="wallpaper" class="size-4" />
<span class="hidden sm:inline">Обложка</span>
</button>
{/if}
{#if fresh?.download_url}
<a href={fresh.download_url} class={btn} target="_blank" rel="noreferrer">
<a
href={fresh.download_url}
class={btn}
target="_blank"
rel="noreferrer"
aria-label="Скачать"
title="Скачать"
>
<Icon name="download" class="size-4" />
<span class="hidden sm:inline">Скачать</span>
</a>
{/if}
{#if canDelete}