feat: photo multiselect, move/copy, trash and compact chrome

- backend: soft delete + trash (photos.deleted_at, migration 000005), GET /trash, restore, permanent delete with storage-key refcount (S3 objects removed only when no other album references them); batch endpoints photo-batch/{delete,move,copy} - metadata only, copies clone rows sharing storage keys
- frontend: Ctrl/Shift multiselect; right-panel 'Действия' as icon text rows (Открыть (просмотр), Переместить, Копировать, Удалить); drag selected photos onto rail album -> TransferDialog; /trash route; trash entry as last item in the left rail; empty right panel hidden on overview
- frontend: compact top bars (app header and viewer), compact theme switcher
- openspec: sync lightroom-workspace-ui artifacts (photos/ui-workspace specs, design #20-24, tasks 9.11-9.17)
This commit is contained in:
iu
2026-10-10 22:13:17 +03:00
parent 78996bc053
commit 2d6a3fea1a
27 changed files with 1097 additions and 85 deletions
@@ -174,6 +174,40 @@ EXIF извлекается при загрузке и хранится в `phot
маршрута, а в альбоме — у выбора; выбор сбрасывается при смене альбома. Альтернатива — хранить
выбор в URL (query) — избыточно для локального выделения.
### 20. Корзина: мягкое удаление и подсчёт ссылок
Удаление фото — мягкое (`photos.deleted_at`, миграция `000005_photo_trash`): списки и просмотр
исключают такие фото, `GET /trash` отдаёт их по альбомам пользователя (владелец или участник).
Восстановление снимает `deleted_at`. Окончательное удаление удаляет запись, и только если
после этого `storage_key` больше нигде не встречается (копии в других альбомах разделяют
объект), удаляются объекты S3 (оригинал, thumb, preview). Альтернатива — удалять S3 сразу —
порвала бы копии.
### 21. Перенос и копирование — только метаданные
Перенос меняет `photos.album_id`; копирование создаёт новую запись с тем же `storage_key`,
thumb/preview и метаданными (клон). Объекты S3 не дублируются, поэтому «ссылки в любых
альбомах» — это строки photos с общим `storage_key`. Права: удаление в источнике (автор или
owner/admin) и загрузка в цель (member+).
### 22. Мультивыбор и трансфер через общие сторы
Выбор — стор `selection` (набор фото + anchor); Ctrl/Cmd — toggle, Shift — диапазон по
индексам текущей сетки. Действия инициируют диалог: стор `transfer` хранит ids, необязательный
целевой альбом и действие; глобальный `TransferDialog` (портал) выбирает альбом и
перенос/копию. Перетаскивание с сетки на альбом рейла передаёт ids через `dataTransfer` типа
`application/x-galo-photos`; дроп-цель — элементы рейла. Обработчик загрузки показывает оверлей
только для типа `Files`, поэтому при переносе фото зона загрузки не появляется.
### 23. Рейл: вход в корзину и скрытие панели
В рейле альбомов последним элементом списка добавлена «Корзина» (иконка корзины), ведущая на
маршрут `/trash` со списком удалённых фото и действиями «Вернуть»/«Удалить навсегда». Пустая
правая панель на обзоре скрыта: `AppShell` получает `panelVisible`, который layout выключает на
корневом маршруте.
### 24. Компактный верхний хром
Верхняя панель приложения (`AppShell`) и верхняя панель режима просмотра (`Viewer`) сведены к
минимуму: узкие вертикальные отступы (`py-0.5`/`py-1`), иконочные органы управления без рамок,
иконки `size-4`, компактный переключатель темы. Функциональность (меню, панель, темы, полный
экран, навигация, скачивание, удаление) сохраняется.
## Risks / Trade-offs
- [Много альбомов → много presign в списке] → presign это локальная крипто-операция без сети;