feat: context menus, unified photo panel, tags, album trash and counters

- backend: deleting an album moves its photos to the trash without losing files (photos.album_id nullable, FK ON DELETE SET NULL, migration 000006; dropped S3 prefix delete); restore accepts an album target (album_required when the album is gone); user-scoped tags (000007) with GET /tags, POST/DELETE /photos/:id/tags, tags included in photo responses; GET /trash/count
- frontend: right-click context menus for photos (grid + viewer) and albums (rail + overview); single shared PhotoPanel reused in viewer and gallery selection (not a copy); viewer download/delete moved to the panel 'Действия'; transparent compact viewer top bar; trash entry as last rail item with per-album photo counts and trash count; album rename/delete dialogs
- openspec: sync lightroom-workspace-ui artifacts (specs photos/ui-workspace, design #24-30, tasks 9.17-9.24)
This commit is contained in:
iu
2026-10-10 22:45:41 +03:00
parent 2d6a3fea1a
commit 61b45b2742
37 changed files with 1639 additions and 328 deletions
@@ -205,9 +205,60 @@ owner/admin) и загрузка в цель (member+).
### 24. Компактный верхний хром
Верхняя панель приложения (`AppShell`) и верхняя панель режима просмотра (`Viewer`) сведены к
минимуму: узкие вертикальные отступы (`py-0.5`/`py-1`), иконочные органы управления без рамок,
иконки `size-4`, компактный переключатель темы. Функциональность (меню, панель, темы, полный
иконки `size-4`, компактный переключатель темы. Верхняя панель просмотра — прозрачный оверлей
(`absolute`, без фона и рамки, `pointer-events-none` с активными кнопками), поэтому не занимает
место в раскладке и увеличивает область фото. Функциональность (меню, панель, темы, полный
экран, навигация, скачивание, удаление) сохраняется.
### 25. Действия в режиме просмотра — в правой панели
Скачивание и удаление убраны из верхней панели просмотра и перенесены в правую панель, в раздел
«Действия», вместе с «Переместить» и «Копировать». Скачивание — ссылка на presigned оригинал;
перенос/копирование — через `transfer`/`TransferDialog`; удаление инициируется стором
`viewerActions`, а обрабатывает его страница просмотра (та же логика перехода к соседнему фото
после удаления).
### 26. Единая панель фото
Содержимое правой панели для одного фото (имя файла, «Действия», «Информация», «Комментарии»)
вынесено в общий компонент `PhotoPanel`, который переиспользуется и в режиме просмотра
(`viewer=true`), и при одиночном выборе фото в сетке — это одна и та же панель, а не копия.
Отличие только в наборе действий: в просмотре нет «Открыть», а удаление идёт через стор
`viewerActions` (обрабатывает страница просмотра с переходом к соседнему фото); при выборе в
сетке удаление выполняет сам компонент и очищает выбор. Мультивыбор (>1) показывает только
раздел «Действия».
### 27. Контекстное меню фото
Правый клик по фото в сетке и по изображению в просмотре открывает контекстное меню с теми же
пунктами, что и раздел «Действия». Меню — общий стор `contextMenu` (`x`, `y`, items) и глобальный
компонент `ContextMenu` (портал), закрывается по клику вне, `Escape`, скроллу и ресайзу.
Пункты формируются на местах: в сетке — «Открыть (просмотр)», «Скачать», «Переместить»,
«Копировать», «Удалить» (для мультивыбора без «Скачать»); в просмотре — без «Открыть».
### 28. Удаление альбома и восстановление фото
Удаление альбома не должно терять файлы: перед удалением альбома его фото мягко удаляются
(`deleted_at`), а FK `photos.album_id` меняется на `ON DELETE SET NULL` (колонка nullable,
миграция `000006`) — после удаления альбома фото остаются строками с `album_id = NULL` в
корзине. Объекты S3 не удаляются (каскадный `DeletePrefix` убран: он ломал бы копии в других
альбомах). `GET /trash` включает такие «без альбома» фото загрузчика. Восстановление с
указанием целевого альбома (`POST /photos/:id/restore {album_id}`) переносит фото туда; без
цели и без альбома возвращается ошибка `album_required`, и клиент предлагает выбрать альбом или
создать новый.
### 29. Контекстное меню альбома и счётчики в рейле
Правый клик по альбому (в рейле и на обзоре) открывает меню «Переименовать»/«Удалить» через
общий стор `contextMenu`; диалоги переименования и подтверждения удаления — глобальный
`AlbumActionsDialogs` (стор `albumActions`). В рейле у каждого альбома показано число фото
(`album.photos_count`), у корзины — число удалённых фото (лёгкий `GET /trash/count`, стор
`trash`).
### 30. Метки фото (область — пользователь)
Метки принадлежат пользователю: таблицы `tags` (`user_id`, уникальность по `lower(name)`) и
`photo_tags` (миграция `000007`). Фото отдаёт метки текущего пользователя (подзапрос
`json_agg` в `photoColumns` по `$2`-viewer). Эндпоинты: `GET /tags?q=` (поиск по меткам
пользователя), `POST /photos/:id/tags {name}` (создаёт метку при необходимости и привязывает,
возвращает обновлённое фото), `DELETE /photos/:id/tags/:tag_id`. UI — раздел «Метки» в
`PhotoPanel`: чипы с удалением и поле с автодополнением (поиск + «Создать «…»»). Права: метку
можно ставить на фото альбома, доступного пользователю.
## Risks / Trade-offs
- [Много альбомов → много presign в списке] → presign это локальная крипто-операция без сети;
@@ -35,10 +35,19 @@
(камера, объектив, экспозиция, ISO, дата съёмки, GPS и др.) и хранит их в `photos.exif`;
они возвращаются вместе с фото и показываются в разделе «Информация». Для ранее загруженных
фото есть команда бэкфилла метаданных из оригиналов.
- **Метки фото.** Пользователь присваивает фото свои метки: создаёт новые и выбирает из списка
с поиском; метки показываются и снимаются в разделе «Метки» панели фото.
- **Контекстные меню.** Правый клик по фото (в сетке и в просмотре) открывает меню с пунктами
раздела «Действия»; правый клик по альбому (в рейле и на обзоре) — меню «Переименовать»/
«Удалить».
- **Удаление альбома.** Удаление альбома помещает его фото в корзину, не удаляя объекты
хранилища; при восстановлении фото без альбома предлагается выбрать существующий альбом или
создать новый.
- **Просмотр.** Полноэкранный режим (кнопка и `f`) с автоскрытием верхней/нижней панелей и
стрелок у кромок; зум колесом к указателю (без прокрутки, максимум 200%), `Space`
возвращает из любого зума во вписанный вид, в зуме догружается оригинал изображения;
удаление текущего фото с модальным подтверждением; возврат к обзору.
действия просмотра (скачать, удалить, перенести, копировать) — в правой панели, в разделе
«Действия»; удаление с модальным подтверждением; возврат к обзору.
- **Разделы и панели.** Правая панель — сворачиваемые разделы (состояние сохраняется), с
разделом «Информация» (EXIF/метаданные); левая и правая панели ресайзятся перетаскиванием.
Выбор фото в сетке показывается в панели (одиночный клик), двойной клик открывает просмотр.
@@ -55,7 +64,8 @@
помещает фото в корзину; объекты S3 удаляются лишь когда на них нет ссылок в других
альбомах. Корзина — последний элемент списка альбомов в левом рейле (иконка корзины); на
обзоре убрана пустая правая панель.
- **Счётчики.** В ответе альбома — число фото и участников.
- **Счётчики.** В ответе альбома — число фото и участников; в рейле у каждого альбома показано
число фото, у корзины — число удалённых фото.
- **Темы и хром.** Существующие 4 темы сохраняются; `dark` — графит Lightroom и дефолт,
`neutral` — тёплый графит; переключатель упорядочен тёмная → нейтральная → светлая →
системная; добавляются токены `--chrome`/`--canvas`; скроллбары стилизованы в тон темы.
@@ -72,9 +82,10 @@
### Modified Capabilities
- `albums`: отображение обложки, счётчики фото/участников и явный выбор обложки.
- `albums`: отображение обложки, счётчики фото/участников, явный выбор обложки и удаление в
корзину.
- `photos`: автообложка только при отсутствии выбора; переименование фото; лимит размера
загрузки; извлечение и хранение EXIF-метаданных; перенос/копирование и корзина.
загрузки; извлечение и хранение EXIF-метаданных; перенос/копирование, корзина и метки.
- `auth`: хранение пользовательских настроек отображения информации в профиле.
## Impact
@@ -89,7 +100,11 @@
`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` с валидацией).
`PATCH /auth/me/preferences` с валидацией); миграции `000005_photo_trash` (мягкое удаление),
`000006_photo_album_nullable` (FK `photos.album_id` → `ON DELETE SET NULL`, удаление альбома
в корзину), `000007_photo_tags` (метки); `GET /trash`/`GET /trash/count`,
`POST /photos/:id/restore` (c `album_id`), `DELETE /photos/:id/permanent`,
`POST /photo-batch/{delete,move,copy}`, `GET /tags`, `POST/DELETE /photos/:id/tags`.
- **Frontend:** реструктуризация `+layout.svelte` в группы `(app)`/`(public)`, `AppShell`
с ресайзом панелей и кнопкой полного экрана; компоненты рейла, контекстной панели со
сворачиваемыми разделами и EXIF, модалки создания/пикера обложки/подтверждения,
@@ -97,7 +112,9 @@
маршрут `albums/[id]/photos/[photoId]`; слот действий в `PanelSection`, модалка настроек
атрибутов с пользовательскими подписями, стор настроек, фильтрация и «…» в панели;
новые значения токенов темы и стили скроллбаров; `types.ts` (+`cover_thumb_url`, счётчики,
`photos_count`, `preferences`); новые иконки в реестре.
`photos_count`, `preferences`, `tags`); общие компоненты `PhotoPanel`, `ContextMenu`,
`TransferDialog`, `AlbumActionsDialogs`; маршрут `/trash`; сторы `selection`, `transfer`,
`contextMenu`, `albumActions`, `trash`, `viewerActions`; новые иконки в реестре.
- **Спеки:** `ui-workspace` (новая), `albums`, `photos`, `auth` (дельты). `ui-iconography` не
затрагивается — иконки по-прежнему наследуют цвет темы.
- **Совместимость:** API-контракты сохраняются; добавляется поле ответа и один FK. Ломающих
@@ -87,3 +87,32 @@
#### Scenario: Окончательное удаление при наличии ссылок
- **WHEN** на объект хранилища ссылается копия в другом альбоме
- **THEN** запись удаляется, а объект хранилища сохраняется
### Requirement: Удаление альбома и восстановление фото без альбома
Удаление альбома SHALL помещать его фото в корзину, не удаляя объекты хранилища. Если при
восстановлении фото его альбом отсутствует, система SHALL предложить выбрать существующий
альбом или создать новый.
#### Scenario: Удаление альбома
- **WHEN** владелец удаляет альбом
- **THEN** его фото оказываются в корзине без привязки к альбому, файлы сохраняются
#### Scenario: Восстановление без альбома
- **WHEN** пользователь восстанавливает фото, у которого нет альбома
- **THEN** система предлагает выбрать альбом или создать новый и восстанавливает фото в него
### Requirement: Метки фото
Пользователь SHALL присваивать фото свои метки (область — пользователь): создавать новые и
выбирать из списка с поиском; метки SHALL отдаваться вместе с фото и сниматься.
#### Scenario: Создание метки
- **WHEN** пользователь вводит новую метку и подтверждает
- **THEN** метка создаётся и привязывается к фото
#### Scenario: Выбор из списка с поиском
- **WHEN** пользователь вводит часть названия метки
- **THEN** показываются подходящие метки пользователя для выбора
#### Scenario: Снятие метки
- **WHEN** пользователь убирает метку с фото
- **THEN** метка отвязывается от фото
@@ -316,10 +316,69 @@ Ctrl/Cmd-клик — добавить или убрать, Shift-клик —
- **WHEN** пользователь открывает обзор альбомов
- **THEN** правая панель не отображается
### Requirement: Действия в режиме просмотра
В режиме просмотра правая панель SHALL содержать раздел «Действия» со строками-пунктами
(иконка + подпись) «Скачать», «Переместить», «Копировать» и «Удалить» (удаление — по правам).
Верхняя панель просмотра SHALL не дублировать скачивание и удаление.
#### Scenario: Действия в просмотре
- **WHEN** открыт режим просмотра фото
- **THEN** раздел «Действия» содержит «Скачать», «Переместить», «Копировать» и «Удалить», а в верхней панели этих кнопок нет
#### Scenario: Удаление из панели
- **WHEN** пользователь удаляет фото из раздела «Действия»
- **THEN** открывается подтверждение, и после удаления просмотр переходит к соседнему фото
### Requirement: Контекстное меню фото
Правый клик по фото в сетке и по изображению в режиме просмотра SHALL открывать контекстное
меню с пунктами раздела «Действия» («Скачать», «Переместить», «Копировать», «Удалить»; в сетке
также «Открыть (просмотр)»), выполняющими те же действия.
#### Scenario: Меню в сетке
- **WHEN** пользователь кликает правой кнопкой по фото в сетке
- **THEN** открывается меню с пунктами «Открыть (просмотр)», «Скачать», «Переместить», «Копировать», «Удалить»
#### Scenario: Меню в просмотре
- **WHEN** пользователь кликает правой кнопкой по изображению в режиме просмотра
- **THEN** открывается меню с пунктами «Скачать», «Переместить», «Копировать», «Удалить»
### Requirement: Контекстное меню альбома
Правый клик по альбому в рейле и по карточке на обзоре SHALL открывать меню с пунктами
«Переименовать» и «Удалить»; переименование SHALL открывать диалог, удаление — подтверждение.
#### Scenario: Меню альбома
- **WHEN** пользователь кликает правой кнопкой по альбому
- **THEN** открывается меню с «Переименовать» и «Удалить»
#### Scenario: Удаление альбома
- **WHEN** пользователь подтверждает удаление альбома
- **THEN** альбом удаляется, а его фото попадают в корзину
### Requirement: Счётчики в рейле
Рейл SHALL показывать число фото у каждого альбома и число элементов в корзине.
#### Scenario: Счётчики
- **WHEN** пользователь смотрит на рейл альбомов
- **THEN** у каждого альбома показано число фото, а у корзины — число удалённых фото
### Requirement: Метки в панели фото
Панель фото SHALL содержать раздел «Метки» с чипами меток фото и полем добавления, дающим
поиск по меткам пользователя и создание новых.
#### Scenario: Раздел «Метки»
- **WHEN** открыт просмотр или выбран один фото в сетке
- **THEN** в панели есть раздел «Метки» с текущими метками и полем добавления с поиском
### Requirement: Компактный верхний хром
Верхняя панель приложения и верхняя панель режима просмотра SHALL иметь минимальную высоту
(узкие отступы, иконочные органы управления), сохраняя все действия доступными.
(узкие отступы, иконочные органы управления), сохраняя все действия доступными. Верхняя панель
режима просмотра SHALL быть прозрачным оверлеем и SHALL не занимать место в раскладке,
увеличивая область фото.
#### Scenario: Компактные верхние панели
- **WHEN** открыт список альбомов или режим просмотра фото
- **THEN** верхняя панель занимает минимум высоты, а её действия (навигация, полный экран, скачивание, удаление) доступны
#### Scenario: Прозрачная панель просмотра
- **WHEN** открыт режим просмотра фото
- **THEN** верхняя панель нарисована поверх фото без фона и не уменьшает область изображения
@@ -76,3 +76,10 @@
- [x] 9.15 Проверка: `cd frontend && npm run check`, `cd backend && go test ./...`, `make build`, `openspec validate "lightroom-workspace-ui"`.
- [x] 9.16 Раздел «Действия» в панели — пункты-надписи с иконками (не кнопки): «Открыть (просмотр)», «Переместить», «Копировать», «Удалить»; надпись «Выбрано:» убрана. Проверка: Playwright — «Открыть (просмотр)» открывает просмотр, раздел присутствует (4/4).
- [x] 9.17 Компактный верхний хром: верхняя панель приложения и верхняя панель режима просмотра сведены к минимуму (узкие отступы, иконочные кнопки `size-4`, компактный переключатель темы). Проверка: высота `<header>` ~31px, верхней панели просмотра ~29px; `npm run check`.
- [x] 9.18 Действия просмотра перенесены в правую панель: в разделе «Действия» — «Скачать», «Переместить», «Копировать», «Удалить»; из верхней панели просмотра скачивание и удаление убраны; удаление через стор `viewerActions` и страницу просмотра. Проверка: Playwright — верх без скачать/удалить, панель с действиями, удаление из панели переходит к соседнему фото (9/9).
- [x] 9.19 Единая панель фото: содержимое для одного фото вынесено в общий компонент `PhotoPanel`, переиспользуемый в просмотре и при одиночном выборе в сетке (не копия); мультивыбор — только «Действия». Проверка: Playwright — просмотр 9/9, выбор 5/5, мультивыбор 9/9; `npm run check`.
- [x] 9.20 Контекстное меню фото (правая кнопка) в сетке и просмотре с пунктами из «Действий»; общий стор `contextMenu` и глобальный `ContextMenu`. Проверка: Playwright — меню в сетке (5 пунктов), меню в просмотре (4 пункта), «Открыть» из меню и «Переместить» → диалог (12/12).
- [x] 9.21 Контекстное меню альбома («Переименовать»/«Удалить») в рейле и на обзоре; диалоги в `AlbumActionsDialogs`. Проверка: Playwright — меню, переименование, удаление (4/4).
- [x] 9.22 Удаление альбома помещает фото в корзину без потери файлов (FK `photos.album_id` → `ON DELETE SET NULL`, миграция `000006`); восстановление без альбома предлагает выбрать/создать альбом (`album_required` + `POST /photos/:id/restore {album_id}`). Проверка: Playwright — удалить альбом → фото в корзине, диалог восстановления, фото в новом альбоме (7/7).
- [x] 9.23 Счётчики в рейле: число фото у альбома и число элементов корзины (`GET /trash/count`, стор `trash`); обновление счётчика альбома после загрузки/удаления. Проверка: Playwright — альбом 2, корзина 0→1 (3/3).
- [x] 9.24 Метки фото (область — пользователь): таблицы `tags`/`photo_tags` (миграция `000007`), `GET /tags?q=`, `POST /photos/:id/tags`, `DELETE /photos/:id/tags/:tag_id`, метки в ответе фото; раздел «Метки» в панели с чипами, поиском и созданием. Проверка: API — создание/поиск/снятие; Playwright — создание, снятие, выбор из поиска (4/4).