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
@@ -0,0 +1,24 @@
# Spec Delta
## ADDED Requirements
### Requirement: Хранение пользовательских настроек интерфейса
Система SHALL хранить настройки отображения в профиле пользователя и SHALL возвращать их при
получении текущего пользователя. Настройки SHALL быть приватными для пользователя.
Отсутствие настроек SHALL трактоваться как значения по умолчанию.
#### Scenario: Получение настроек
- **WHEN** аутентифицированный пользователь запрашивает свой профиль
- **THEN** ответ содержит его настройки отображения (или пустой объект по умолчанию)
#### Scenario: Сохранение настроек
- **WHEN** пользователь сохраняет настройки отображения
- **THEN** система сохраняет их в профиле и возвращает сохранённое состояние
#### Scenario: Изоляция настроек
- **WHEN** другой пользователь запрашивает свой профиль
- **THEN** он не получает настройки первого пользователя
#### Scenario: Некорректные настройки
- **WHEN** пользователь присылает настройки недопустимой структуры или размера
- **THEN** система отклоняет запрос с ошибкой и не меняет сохранённые настройки
@@ -41,3 +41,15 @@
#### Scenario: Файл в пределах лимита
- **WHEN** файл не превышает лимит
- **THEN** система принимает multipart-загрузку и обрабатывает фото
### Requirement: Метаданные фото (EXIF)
При загрузке фото система SHALL извлекать распознаваемые EXIF-атрибуты и SHALL возвращать их
вместе с данными фото; при отсутствии метаданных SHALL возвращать пустой набор.
#### Scenario: Фото с метаданными
- **WHEN** загружается фото с EXIF
- **THEN** данные фото содержат извлечённые атрибуты (камера, экспозиция, ISO, дата съёмки и др.)
#### Scenario: Фото без метаданных
- **WHEN** загружается фото без читаемых EXIF
- **THEN** данные фото не содержат атрибутов, а загрузка выполняется успешно
@@ -139,7 +139,8 @@ SHALL отображаться вне каркаса, но в общей тем
### Requirement: Сворачиваемые разделы панели
Контекстная панель SHALL группировать содержимое в сворачиваемые разделы, а состояние
сворачивания SHALL сохраняться; раздел «Информация» SHALL показывать метаданные EXIF фото.
сворачивания SHALL сохраняться; раздел «Информация» SHALL показывать метаданные фото,
включая извлечённые EXIF-атрибуты.
#### Scenario: Сворачивание раздела
- **WHEN** пользователь нажимает заголовок раздела
@@ -147,30 +148,47 @@ SHALL отображаться вне каркаса, но в общей тем
#### Scenario: Метаданные фото
- **WHEN** открыт просмотр фото
- **THEN** раздел «Информация» показывает размер, тип, разрешение, дату съёмки и координаты
- **THEN** раздел «Информация» показывает размер, тип, разрешение, дату съёмки, координаты и извлечённые EXIF-атрибуты
### Requirement: Полноэкранный просмотр фотографии
В режиме просмотра SHALL быть доступен переход в полноэкранный режим кнопкой и хоткеем `f`.
В полноэкранном режиме верхняя и нижняя панели SHALL скрываться и показываться у
соответствующей кромки экрана.
#### Scenario: Вход в полный экран
- **WHEN** пользователь нажимает кнопку полного экрана или клавишу `f`
- **THEN** просмотр разворачивается на весь экран
- **THEN** просмотр разворачивается на весь экран, панели скрыты
#### Scenario: Панель по кромке
- **WHEN** курсор касается верхней или нижней кромки экрана
- **THEN** соответствующая панель показывается и скрывается примерно через секунду после ухода курсора
#### Scenario: Выход из полного экрана
- **WHEN** пользователь повторно нажимает кнопку или клавишу `f`
- **THEN** полноэкранный режим закрывается
### Requirement: Зум в просмотре
Просмотр SHALL поддерживать зум: колесо мыши меняет масштаб, `Space` переключает 100%, а при
увеличении фото SHALL панорамироваться. Зум SHALL работать и в полноэкранном режиме.
Просмотр SHALL поддерживать зум колесом мыши без прокрутки, с привязкой к указателю и
верхним пределом 200% размеров фото. `Space` SHALL возвращать изображение из любого зума во
вписанный вид, а из вписанного — в 100%. При зуме SHALL догружаться оригинал изображения.
Стрелки соседних фото SHALL появляться у левой/правой кромки и прятаться в зуме. Зум SHALL
работать и в полноэкранном режиме.
#### Scenario: Зум колесом
#### Scenario: Зум колесом к указателю
- **WHEN** пользователь вращает колесо мыши над фото
- **THEN** масштаб фото меняется относительно положения курсора
- **THEN** масштаб меняется относительно курсора, страница не прокручивается, предел — 200%
#### Scenario: 100% по пробелу
#### Scenario: Пробел
- **WHEN** пользователь нажимает `Space`
- **THEN** фото показывается в масштабе 100% (1:1), повторное нажатие возвращает вписанный вид
- **THEN** из вписанного вида изображение переходит в 100%, а из любого зума возвращается во вписанный
#### Scenario: Оригинал в зуме
- **WHEN** пользователь входит в зум
- **THEN** догружается оригинал изображения и подменяется по факту загрузки
#### Scenario: Стрелки соседних фото
- **WHEN** курсор находится у левой или правой кромки области просмотра
- **THEN** показывается соответствующая стрелка, скрываясь примерно через 300 мс после ухода; в зуме стрелки скрыты
#### Scenario: Панорамирование
- **WHEN** фото увеличено
@@ -198,3 +216,38 @@ SHALL отображаться вне каркаса, но в общей тем
#### Scenario: Возврат из просмотра
- **WHEN** пользователь нажимает кнопку «назад» в режиме просмотра
- **THEN** открывается альбом с сеткой фото
### Requirement: Настройки списка атрибутов информации
Раздел «Информация» SHALL предоставлять доступ к настройкам (иконка-шестерёнка), открывающим
модальное окно, в котором пользователь SHALL выбирать отображаемые EXIF-атрибуты и задавать
их подписи. Настройки SHALL применяться ко всем фото и SHALL сохраняться в профиле
пользователя. При отсутствии настроек SHALL отображаться все доступные атрибуты.
#### Scenario: Открытие настроек
- **WHEN** пользователь нажимает иконку настроек в разделе «Информация»
- **THEN** открывается модальное окно со списком EXIF-атрибутов: переключатели видимости и поля подписей
#### Scenario: Сохранение настроек
- **WHEN** пользователь скрывает атрибут, задаёт подпись и сохраняет
- **THEN** раздел «Информация» учитывает изменения, а настройки сохраняются в профиле
#### Scenario: Сброс настроек
- **WHEN** пользователь сбрасывает настройки к значениям по умолчанию
- **THEN** показываются все атрибуты с исходными подписями
### Requirement: Свёртка скрытых атрибутов
Если настройками часть атрибутов скрыта, в конце списка атрибутов SHALL отображаться элемент
«…»; нажатие на него SHALL раскрывать все атрибуты текущего фото, включая скрытые. Если
скрытых атрибутов нет, элемент «…» SHALL не отображаться.
#### Scenario: Скрытые атрибуты
- **WHEN** в разделе «Информация» есть скрытые настройками атрибуты
- **THEN** после видимых атрибутов показан элемент «…»
#### Scenario: Раскрытие всех атрибутов
- **WHEN** пользователь нажимает «…»
- **THEN** отображаются все атрибуты фото, включая скрытые
#### Scenario: Нет скрытых атрибутов
- **WHEN** ни один атрибут не скрыт
- **THEN** элемент «…» не отображается