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:
@@ -24,12 +24,15 @@
|
||||
- Тёмная графитовая тема по умолчанию при сохранении остальных тем.
|
||||
- Просмотр фото как маршрут в каркасе, общий с публичной share-страницей.
|
||||
- Адаптивность до мобильного без потери каркаса.
|
||||
- Пер-пользовательские настройки раздела «Информация» (видимость атрибутов и подписи),
|
||||
хранимые в профиле.
|
||||
|
||||
**Non-Goals:**
|
||||
- Мультивыбор фото и массовые действия / bulk-удаление.
|
||||
- Замена роутинга на «чистые панели» без маршрутов (вариант P).
|
||||
- Новые внешние зависимости (иконки и токены вендорятся/расширяются).
|
||||
- Изменение доменных контрактов API (только добавление поля ответа и FK).
|
||||
- Порядок атрибутов и настройки на уровне альбома/фото — настройки глобальны для пользователя.
|
||||
|
||||
## Decisions
|
||||
|
||||
@@ -59,10 +62,10 @@ ON DELETE SET NULL`. БД сама чинит любой путь удалени
|
||||
безвреден: альбом вставляется с `NULL`, фото ссылается на альбом, cover выставляется позже.
|
||||
Альтернатива — чистка в `photos.Delete` (легко забыть на новых путях удаления).
|
||||
|
||||
### 5. Точки выбора обложки: панель + просмотр (T1+T2)
|
||||
### 5. Точка выбора обложки: правая панель (T1)
|
||||
Правая панель даёт `[Выбрать обложку]` (модальный пикер из миниатюр альбома) и `[Сбросить]`;
|
||||
в просмотре — действие «Сделать обложкой» для текущего фото. Всё под `roles.CanEditAlbum`.
|
||||
Hover-оверлей в сетке (T3) не берём — шум и случайные клики.
|
||||
всё под `roles.CanEditAlbum`. Действие «Сделать обложкой» в тулбаре просмотра и hover-оверлей
|
||||
в сетке (T3) не используются — шум и случайные клики.
|
||||
|
||||
### 6. Тема: сохранить темы, dark = графит и дефолт (α)
|
||||
Тёмная тема перенастраивается в графит Lightroom и становится значением по умолчанию
|
||||
@@ -101,11 +104,16 @@ filmstrip, панель метаданных/комментариев, тулб
|
||||
|
||||
### 13. Просмотр: полный экран и зум
|
||||
Полный экран — `requestFullscreen` на корне `Viewer` (кнопка + `f`), состояние — по
|
||||
`fullscreenchange`. Зум — локальные `zoomed`/`scale`: колесо меняет масштаб с привязкой к
|
||||
курсору; `Space` переключает 100% (1:1); при увеличении изображение панорамируется
|
||||
перетаскиванием и скроллбарами. Хоткеи игнорируются при фокусе в поле ввода. Зум
|
||||
сбрасывается при смене фото. Альтернатива — только `object-fit` без ручного масштаба —
|
||||
не даёт 1:1 и панорамирования.
|
||||
`fullscreenchange`. В полноэкранном режиме верхняя/нижняя панели и стрелки соседних фото
|
||||
скрыты и всплывают у соответствующих кромок (панели — по касанию края, скрытие ~1 с после
|
||||
ухода; стрелки — при курсоре в 1/6 левой/правой области, скрытие ~300 мс; в зуме стрелки
|
||||
скрыты полностью). Зум — состояние `zoomed`/`scale` с масштабом от истинных размеров фото:
|
||||
колесо зумит с привязкой к указателю (без прокрутки, `overscroll-contain`), предел — 200%;
|
||||
`Space` возвращает из любого зума во вписанный вид, а из вписанного — в 100%; вход в зум
|
||||
догружает оригинал и подменяет его по факту загрузки (до этого — превью); панорамирование —
|
||||
перетаскиванием и скроллом. Хоткеи игнорируются при фокусе в поле ввода; зум сбрасывается при
|
||||
смене фото. Альтернатива — только `object-fit` без ручного масштаба — не даёт 1:1 и
|
||||
панорамирования.
|
||||
|
||||
### 14. Панели: ресайз и сворачиваемые разделы
|
||||
Ширина рейла и панели хранится в `localStorage`, меняется перетаскиванием разделителя
|
||||
@@ -124,6 +132,33 @@ filmstrip, панель метаданных/комментариев, тулб
|
||||
панель обходилась одним запросом. Переименование публикует `photo.updated`, страница альбома
|
||||
обновляет запись в списке.
|
||||
|
||||
### 17. Настройки информации: оверрайды в профиле пользователя
|
||||
Настройки раздела «Информация» хранятся в профиле как оверрайды, а не как полный список:
|
||||
`users.preferences` (jsonb, дефолт `{}`) с секцией `photo_info` вида
|
||||
`{ "hidden": ["Contrast", ...], "labels": { "Make": "Камера", ... } }`. Отсутствие ключа
|
||||
означает, что атрибут видим и подпись дефолтная, поэтому новые и незнакомые теги EXIF не
|
||||
«теряются» и не требуют миграций данных. `GET /auth/me` отдаёт `preferences` вместе с
|
||||
пользователем; сохранение — `PATCH /auth/me/preferences` (полная замена объекта с серверной
|
||||
валидацией: строки и массивы строк, ограничение длины подписи/ключа и числа скрытых ключей).
|
||||
Настройки не входят в `PublicUser` (приватны). UI: `PanelSection` получает слот `actions` под
|
||||
шестерёнку; модалка `PhotoInfoSettingsDialog` строится из объединения известного каталога
|
||||
подписей и ключей текущего фото; список в панели фильтруется по оверрайдам, а скрытые
|
||||
атрибуты сворачиваются под кнопку «…» (раскрытие — временное, для текущего фото).
|
||||
Альтернативы: полный упорядоченный список в профиле (хрупок к новым тегам, нужны миграции) и
|
||||
хранение на клиенте (не переносится между устройствами) — отклонены.
|
||||
|
||||
### 18. Метаданные фото: устойчивый парсер и хранение в jsonb
|
||||
EXIF извлекается при загрузке и хранится в `photos.exif` (jsonb, миграция `000003_photo_exif`)
|
||||
как плоская карта «тег → значение» всех распознаваемых библиотекой тегов. Парсер —
|
||||
`github.com/dsoprea/go-exif/v3`: он переживает нестандартные и битые блоки EXIF, на которых
|
||||
падал прежний `rwcarlsen/goexif` (реальные снимки HUAWEI давали zero-length sub-IFD и пустой
|
||||
результат). Числовые значения нормализуются (дроби → число); дата съёмки и GPS дополнительно
|
||||
пишутся в колонки `taken_at`, `latitude`/`longitude`. На клиенте теги показываются с русскими
|
||||
подписями и переводом enum. Для ранее загруженных фото предусмотрена разовая команда
|
||||
`cmd/backfill-exif`, которая тянет оригиналы из S3 (`storage.Get`) и заполняет
|
||||
`exif`/`taken_at`/координаты. Альтернатива — фиксированный набор полей — отклонена:
|
||||
пользователю нужны все атрибуты.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Много альбомов → много presign в списке] → presign это локальная крипто-операция без сети;
|
||||
@@ -147,19 +182,29 @@ filmstrip, панель метаданных/комментариев, тулб
|
||||
- [Крупные файлы утяжеляют память] → лимит 100 МБ и чтение через `LimitReader`; превышение
|
||||
даёт понятную 413.
|
||||
- [Сохранённые ширина/состояние панелей] → значения ограничиваются допустимым диапазоном.
|
||||
- [Настройки в профиле: рост jsonb и приватность] → `preferences` не входят в `PublicUser`;
|
||||
серверная валидация ограничивает структуру, длину подписей и число скрытых ключей.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Миграция `000002_album_cover_fk`: `UPDATE albums SET cover_photo_id = NULL WHERE
|
||||
cover_photo_id IS NOT NULL AND cover_photo_id NOT IN (SELECT id FROM photos);` затем
|
||||
`ADD CONSTRAINT ... ON DELETE SET NULL`. Down — `DROP CONSTRAINT`.
|
||||
2. Бэкенд: `models.Album` (+`CoverThumbURL`, `PhotosCount`, `MembersCount`), `albums/store.go`
|
||||
2. Миграция `000003_photo_exif`: `ALTER TABLE photos ADD COLUMN IF NOT EXISTS exif jsonb NOT
|
||||
NULL DEFAULT '{}'::jsonb`. Down — `DROP COLUMN`.
|
||||
3. Миграция `000004_user_preferences`: `ALTER TABLE users ADD COLUMN preferences jsonb NOT
|
||||
NULL DEFAULT '{}'::jsonb`. Down — `DROP COLUMN`.
|
||||
4. Бэкенд: `models.Album` (+`CoverThumbURL`, `PhotosCount`, `MembersCount`), `albums/store.go`
|
||||
(LEFT JOIN + счётчики), `albums/service.go` (enrich + presign), `photos` (автообложка только
|
||||
если NULL; `PATCH /photos/:id` для переименования; лимит 100 МБ и `413`).
|
||||
3. Фронтенд: каркас и маршруты, компоненты (шелл с ресайзом/фуллскрином, рейл, панель с
|
||||
разделами и EXIF, модалки, `Viewer` с зумом), стор альбомов, токены и скроллбары, `types.ts`.
|
||||
4. Откат: снять constraint (down-миграция); фронтенд откатывается независимо, поле ответа
|
||||
игнорируется старым клиентом.
|
||||
если NULL; `PATCH /photos/:id` для переименования; лимит 100 МБ и `413`; извлечение EXIF в
|
||||
`photos.exif` и команда `cmd/backfill-exif`), `models.User` (+`Preferences`), `auth`
|
||||
(`GetUserByID` c `preferences`, `Me` отдаёт настройки, `PATCH /auth/me/preferences` с
|
||||
валидацией).
|
||||
5. Фронтенд: каркас и маршруты, компоненты (шелл с ресайзом/фуллскрином, рейл, панель с
|
||||
разделами и EXIF, модалки, `Viewer` с зумом), слот действий и модалка настроек атрибутов,
|
||||
стор настроек, `types.ts`.
|
||||
6. Откат: снять constraint и колонки `photos.exif`/`users.preferences` (down-миграции);
|
||||
фронтенд откатывается независимо, поля ответа игнорируются старым клиентом.
|
||||
|
||||
## Open Questions
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
- **Создание альбома.** Инлайн-форма заменяется иконочной кнопкой с хинтом, открывающей
|
||||
модалку ввода названия.
|
||||
- **Обложки.** `Album` получает `cover_thumb_url`; карточки и рейл показывают реальную
|
||||
миниатюру. Выбор обложки: пикер в правой панели и действие «сделать обложкой» в
|
||||
просмотре. Автообложка пишется только когда обложка ещё не задана; FK
|
||||
миниатюру. Выбор обложки: пикер в правой панели (действие «сделать обложкой» в
|
||||
просмотре не используется). Автообложка пишется только когда обложка ещё не задана; FK
|
||||
`albums.cover_photo_id -> photos(id) ON DELETE SET NULL` чинит висячие ссылки.
|
||||
- **Просмотр как маршрут.** `Lightbox`-оверлей заменяется маршрутом
|
||||
`/albums/:id/photos/:photoId` внутри шелла: крупное фото, filmstrip снизу, панель
|
||||
@@ -31,10 +31,20 @@
|
||||
ошибкой при превышении.
|
||||
- **Переименование фото.** `PATCH /photos/:id` меняет `original_filename`; инлайн-редактор
|
||||
в правой панели, доступный автору фото и owner/admin.
|
||||
- **Просмотр.** Полноэкранный режим (кнопка и хоткей `f`), зум колесом мыши, `Space` — 100%
|
||||
с панорамированием; удаление текущего фото с модальным подтверждением; возврат к обзору.
|
||||
- **Метаданные фото (EXIF).** При загрузке система извлекает все распознаваемые EXIF-атрибуты
|
||||
(камера, объектив, экспозиция, ISO, дата съёмки, GPS и др.) и хранит их в `photos.exif`;
|
||||
они возвращаются вместе с фото и показываются в разделе «Информация». Для ранее загруженных
|
||||
фото есть команда бэкфилла метаданных из оригиналов.
|
||||
- **Просмотр.** Полноэкранный режим (кнопка и `f`) с автоскрытием верхней/нижней панелей и
|
||||
стрелок у кромок; зум колесом к указателю (без прокрутки, максимум 200%), `Space`
|
||||
возвращает из любого зума во вписанный вид, в зуме догружается оригинал изображения;
|
||||
удаление текущего фото с модальным подтверждением; возврат к обзору.
|
||||
- **Разделы и панели.** Правая панель — сворачиваемые разделы (состояние сохраняется), с
|
||||
разделом «Информация» (EXIF/метаданные); левая и правая панели ресайзятся перетаскиванием.
|
||||
- **Настройки информации.** Раздел «Информация» получает кнопку-шестерёнку, открывающую
|
||||
модалку со списком EXIF-атрибутов: переключатели видимости и пользовательские подписи.
|
||||
Скрытые атрибуты сворачиваются под элементом «…» (клик раскрывает все). Настройки хранятся
|
||||
в профиле пользователя (server-side) и применяются ко всем фото.
|
||||
- **Счётчики.** В ответе альбома — число фото и участников.
|
||||
- **Темы и хром.** Существующие 4 темы сохраняются; `dark` — графит Lightroom и дефолт,
|
||||
`neutral` — тёплый графит; переключатель упорядочен тёмная → нейтральная → светлая →
|
||||
@@ -53,7 +63,8 @@
|
||||
|
||||
- `albums`: отображение обложки, счётчики фото/участников и явный выбор обложки.
|
||||
- `photos`: автообложка только при отсутствии выбора; переименование фото; лимит размера
|
||||
загрузки.
|
||||
загрузки; извлечение и хранение EXIF-метаданных.
|
||||
- `auth`: хранение пользовательских настроек отображения информации в профиле.
|
||||
|
||||
## Impact
|
||||
|
||||
@@ -63,14 +74,20 @@
|
||||
(presign → `CoverThumbURL`, обогащение списка); `internal/models` (`Album.CoverThumbURL`,
|
||||
`PhotosCount`, `MembersCount`); `internal/photos` (автообложка только при
|
||||
`cover_photo_id IS NULL`; `PATCH /photos/:id` для переименования; лимит загрузки 100 МБ с
|
||||
`413 file_too_large`; событие `photo.updated`).
|
||||
`413 file_too_large`; событие `photo.updated`; извлечение EXIF через
|
||||
`github.com/dsoprea/go-exif/v3` в `photos.exif`, миграция `000003_photo_exif`, команда
|
||||
`cmd/backfill-exif` и `storage.Get`); миграция `users.preferences`,
|
||||
`models.User.Preferences`, `internal/auth` (`Me` отдаёт настройки,
|
||||
`PATCH /auth/me/preferences` с валидацией).
|
||||
- **Frontend:** реструктуризация `+layout.svelte` в группы `(app)`/`(public)`, `AppShell`
|
||||
с ресайзом панелей и кнопкой полного экрана; компоненты рейла, контекстной панели со
|
||||
сворачиваемыми разделами и EXIF, модалки создания/пикера обложки/подтверждения,
|
||||
`Viewer` с полноэкранным режимом и зумом, общий стор альбомов, контрол плотности; новый
|
||||
маршрут `albums/[id]/photos/[photoId]`; новые значения токенов темы и стили скроллбаров;
|
||||
`types.ts` (+`cover_thumb_url`, счётчики, `photos_count`); новые иконки в реестре.
|
||||
- **Спеки:** `ui-workspace` (новая), `albums`, `photos` (дельты). `ui-iconography` не
|
||||
маршрут `albums/[id]/photos/[photoId]`; слот действий в `PanelSection`, модалка настроек
|
||||
атрибутов с пользовательскими подписями, стор настроек, фильтрация и «…» в панели;
|
||||
новые значения токенов темы и стили скроллбаров; `types.ts` (+`cover_thumb_url`, счётчики,
|
||||
`photos_count`, `preferences`); новые иконки в реестре.
|
||||
- **Спеки:** `ui-workspace` (новая), `albums`, `photos`, `auth` (дельты). `ui-iconography` не
|
||||
затрагивается — иконки по-прежнему наследуют цвет темы.
|
||||
- **Совместимость:** API-контракты сохраняются; добавляется поле ответа и один FK. Ломающих
|
||||
изменений API нет.
|
||||
|
||||
@@ -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** элемент «…» не отображается
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
- [x] 4.1 Компонент `AlbumPanel`: превью обложки, название, счётчики, переходы к участникам/настройкам/ссылкам; управление обложкой только для owner/admin. Проверка: viewer не видит управление обложкой.
|
||||
- [x] 4.2 Модальный `CoverPicker` (сетка миниатюр) + `PATCH /albums/:id` с `cover_photo_id`/`clear_cover`. Проверка: назначение и сброс обложки отражаются в рейле, карточке и панели.
|
||||
- [x] 4.3 Действие «Сделать обложкой» в тулбаре просмотра (роль-gated). Проверка: из просмотра текущее фото становится обложкой альбома.
|
||||
- [x] 4.3 Выбор обложки выполняется только из правой панели (модальный пикер); действие «Сделать обложкой» в тулбаре просмотра не используется. Проверка: обложка задаётся и сбрасывается из панели, в тулбаре просмотра кнопки нет.
|
||||
|
||||
## 5. Центр: сетка, плотность, загрузка
|
||||
|
||||
@@ -47,9 +47,22 @@
|
||||
|
||||
- [x] 8.1 Переименование фото: `PATCH /photos/:id` (права автора/owner/admin, пустое имя 400) и инлайн-редактор в правой панели; событие `photo.updated`. Проверка: переименование сохраняется, пустое имя отклоняется, чужое фото — отказ, событие приходит.
|
||||
- [x] 8.2 Удаление фото из просмотра с модальным подтверждением; переход к соседнему и обновление ленты. Проверка: после удаления лента не содержит удалённое фото.
|
||||
- [x] 8.3 Полноэкранный просмотр (кнопка и `f`) и зум (колесо, `Space` 100%, панорамирование) в обычном и полноэкранном режиме. Проверка: хоткеи и зум работают, при вводе текста не срабатывают.
|
||||
- [x] 8.3 Полноэкранный просмотр (кнопка и `f`) с автоскрытием верхней/нижней панелей у кромок и стрелок (1/6 края, скрытие ~300 мс, в зуме скрыты); зум колесом к указателю без прокрутки (макс 200%), `Space` возвращает из любого зума во вписанный вид, в зуме подгружается оригинал. Проверка: поведение подтверждено в браузере (автоскрытие, зум к указателю, предел 200%, подмена превью→оригинал).
|
||||
- [x] 8.4 Сворачиваемые разделы правой панели с сохранением состояния и раздел «Информация» (EXIF). Проверка: состояние сохраняется между сессиями, метаданные отображаются.
|
||||
- [x] 8.5 Ресайз левой и правой панелей перетаскиванием с сохранением ширины. Проверка: ширина восстанавливается после перезагрузки.
|
||||
- [x] 8.6 Лимит загрузки 100 МБ и понятные ошибки (клиентская проверка и `413 file_too_large`). Проверка: 60 МБ проходит разбор, превышение лимита даёт явную ошибку размера.
|
||||
- [x] 8.7 Счётчики `photos_count`/`members_count` в ответе альбома. Проверка: `GET /api/albums` и `GET /api/albums/:id` содержат счётчики.
|
||||
- [x] 8.8 Тема и хром: нейтральная тема — тёплый графит, порядок и значок переключателя, стилизация скроллбаров. Проверка: темы переключаются согласованно, скроллбары в тон темы.
|
||||
- [x] 8.9 Извлечение EXIF: парсер заменён на `github.com/dsoprea/go-exif/v3` (все распознаваемые теги, нормализация чисел), хранение в `photos.exif` (миграция `000003_photo_exif`, jsonb `'{}'` по умолчанию), отдача в API фото; дата съёмки и GPS дублируются в `taken_at`/координаты. Проверка: реальное фото HUAWEI nova 15 Pro даёт 67 атрибутов; `go test ./...`.
|
||||
- [x] 8.10 Бэкфилл метаданных существующих фото: команда `cmd/backfill-exif` + `storage.Get` + `photos.Store.BackfillEXIF` (тянет оригиналы из S3, заполняет `exif`/`taken_at`/координаты). Проверка: прогон обновил 25 фото, `taken_at` проставлен.
|
||||
- [x] 8.11 Раздел «Информация» показывает все EXIF-атрибуты с русскими подписями, переводом enum и единицами (выдержка `1/N`, `f/N`, `N мм`, EV); служебные/дублирующие теги скрыты. Проверка: Playwright на реальном фото — 9/9.
|
||||
|
||||
## 9. Настройки информации пользователя
|
||||
|
||||
- [ ] 9.1 Миграция `000004_user_preferences` (up/down): `ADD COLUMN preferences jsonb NOT NULL DEFAULT '{}'`. Проверка: `make migrate` применяется, `\d users` показывает колонку, down её снимает.
|
||||
- [ ] 9.2 `models.User` (+`Preferences`), `auth/store.go` (выборка `preferences` в `GetUserByID`, метод `UpdatePreferences`), `auth/handlers.go`/`service.go` (`Me` возвращает `preferences`; `PATCH /auth/me/preferences` с валидацией структуры, длины подписей/ключей и числа скрытых). Проверка: невалидный payload → 400, валидный сохраняется.
|
||||
- [ ] 9.3 Маршрут `PATCH /auth/me/preferences` в `app.go` под middleware. Проверка: без токена 401, с токеном сохранение и чтение согласованы.
|
||||
- [ ] 9.4 Frontend: `types.ts` (+`preferences`), auth-стор (+`preferences`, сохранение через API). Проверка: `cd frontend && npm run check`.
|
||||
- [ ] 9.5 `PanelSection` — слот `actions` для шестерёнки; новый `PhotoInfoSettingsDialog` (список атрибутов, видимость, подписи, сброс к дефолту). Проверка: изменения применяются к разделу и сохраняются в профиле.
|
||||
- [ ] 9.6 `AlbumPanel`: фильтрация атрибутов по оверрайдам, элемент «…» для показа скрытых (при отсутствии скрытых — не показывается). Проверка: скрытые сворачиваются, «…» раскрывает все, дефолт — все видимы.
|
||||
- [ ] 9.7 Проверка: `cd frontend && npm run check`, `cd backend && go test ./...`, `make build`, `openspec validate "lightroom-workspace-ui"`.
|
||||
|
||||
Reference in New Issue
Block a user