Album panel and photo sheet with attributes/actions for every album (server and device), local album rename/cover/delete, EXIF attribute settings, and a unified device-gallery sync check: - add repository "Атрибуты и действия" panel (swipe left) on server and device albums: cover, info, navigation, rename/delete; album settings screen; photo sheet (swipe up) with info, tags and actions; EXIF attribute settings backed by profile preferences - local albums use local metadata and actions: app-only display name and cover (new device_album_meta table), delete removes photos from the phone via the system delete consent - unified mode x state policy (GallerySyncPolicy/SyncCheck) runs on start and on album open: upload/offload in move, restore originals in copy/off - fix move->copy restore, offload of any linked-but-local original, provenance recovery that no longer overwrites the album mode, serialized reconcile and duplicate-upload suppression; prompt for "all files" access on start - album card caption: name (with linked server name when different) and storage icons (memory card / cloud) with item count Sync delta specs into android-client and archive the change add-mobile-album-photo-actions. Verified: unit/Robolectric suite green, :app:assembleDebug, :app:lint, and the DeviceSyncE2ETest instrumented suite on the emulator (7/7).
130 lines
11 KiB
Markdown
130 lines
11 KiB
Markdown
# Design
|
||
|
||
## Context
|
||
|
||
См. `proposal.md` — зачем. Текущее состояние, влияющее на решение:
|
||
|
||
- `GaloApi` умеет только `albums list/get/create`, `photos list/get/upload`, `renamePhoto`,
|
||
`deletePhoto`, `movePhotos`. Доступны, но не задействованы: `PATCH/DELETE /albums/:id`,
|
||
`/albums/:id/members`, `/albums/:id/share`, `POST /photo-batch/copy`,
|
||
`/photos/:id/tags`, `/tags`, `PATCH /auth/me/preferences`.
|
||
- Сервер уже отдаёт в JSON фото `exif`, `tags`, `uploader`, `download_url`, а `/auth/me` —
|
||
`preferences`; `PhotoDto`/`UserDto` в Android этих полей не читают.
|
||
- `DeviceAlbumViewModel` держит `serverPhotoByMediaId` (сопоставление локального медиа
|
||
серверному фото), но `DeviceViewerScreen` его не получает.
|
||
- `DeviceAlbumScreen` уже использует `ModalNavigationDrawer` (стартовый/левый drawer) под
|
||
настройки синхронизации.
|
||
- Просмотр (`ViewerScreen`/`DeviceViewerScreen`) занят жестами: горизонтальный пейджер,
|
||
pinch-zoom, double-tap, панорама (drag по двум осям), плеер видео.
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:**
|
||
- Единая панель «Атрибуты и действия» на всех экранах альбомов (свайп справа налево).
|
||
- Единый лист «Атрибуты и действия» в обоих просмотрах (свайп вверх).
|
||
- Резолвинг источника атрибутов: сервер / связанный сервер / локальные значения.
|
||
- Расширение клиентского API/DTO до паритета с web без изменений бэкенда.
|
||
|
||
**Non-Goals:**
|
||
- Изменения бэкенда, web и схемы spec-ов (кроме этой delta).
|
||
- Новые серверные способы работы с медиа (всё уже есть в API).
|
||
- Пересмотр существующих жестов просмотра (пейджер, зум, панорама) и drawer'а синхронизации.
|
||
|
||
## Decisions
|
||
|
||
### 1. Панель альбома = `ModalNavigationDrawer` с end-drawer
|
||
Свайп справа налево открывает правый (end) `ModalNavigationDrawer` на `AlbumScreen` и на
|
||
`DeviceAlbumScreen`. Так сохраняется симметрия: на device-экране левый drawer — синхронизация,
|
||
правый — атрибуты/действия; на серверном — только правый.
|
||
_Альтернатива_: bottom sheet на альбоме — не повторяет web-раскладку «правая панель» и хуже
|
||
для длинного списка атрибутов.
|
||
|
||
### 2. Один компонент панели; атрибуты локального альбома — локальные
|
||
Общий composable-контент панели принимает «модель атрибутов альбома», которую строит экран:
|
||
|
||
- серверный альбом → из `AlbumDto` (включая `role` из envelope);
|
||
- локальный альбом → локальные метаданные (`DeviceAlbum` + app-only метаданные), связанный
|
||
серверный альбом используется только как источник для навигации «Участники»/«Ссылки».
|
||
|
||
Секции панели показываются всегда; строки навигации, требующие серверной стороны, отображаются
|
||
неактивными, когда связи нет.
|
||
|
||
_Альтернатива_: три отдельные панели — дублирование и расхождение поведения.
|
||
|
||
### 2a. App-only метаданные локального альбома
|
||
Имя (отображаемое) и выбранная обложка локального альбома хранятся в новой Room-таблице
|
||
`device_album_meta` (ключ — id альбома-устройства). «Переименовать» пишет `displayName`,
|
||
«Выбрать обложку»/«Сбросить» — `coverUri`. Список альбомов и панель применяют их поверх
|
||
данных MediaStore.
|
||
|
||
_Альтернатива_: менять bucket в MediaStore — невозможно без прав и необратимо.
|
||
|
||
### 2b. Удаление локального альбома удаляет медиа телефона
|
||
«Удалить» собирает все URI медиа альбома и запрашивает системное согласие
|
||
(`MediaStore.createDeleteRequest`) через тот же механизм, что и разгрузка в режиме C. После
|
||
подтверждения система удаляет файлы; приложение забывает локальный индекс и метаданные
|
||
(`forgetAlbum`).
|
||
|
||
### 3. Лист фото = `ModalBottomSheet`, открытие по порогу свайпа вверх
|
||
`ViewerScreen` и `DeviceViewerScreen` добавляют `detectVerticalDragGestures` под условием:
|
||
не открывать при активном зуме, панораме и поверх контролов видео. В счёт: открывать при
|
||
`!zoomed && !isPanorama && kind != VIDEO` (для видео — через видимый drag-handle/кнопку).
|
||
_Альтернатива_: кнопка вместо жеста — расходится с исходным требованием (свайп вверх).
|
||
|
||
### 4. Резолвинг атрибутов/действий device-медиа
|
||
Если `serverPhotoByMediaId[mediaId]` есть — лист грузит серверное фото (`GET /photos/:id`) и
|
||
показывает серверные атрибуты/действия; иначе — локальную модель из `DeviceMedia`.
|
||
Сопоставление передаётся в `DeviceViewerScreen` из `DeviceAlbumViewModel`.
|
||
|
||
### 5. Расширение клиентского API/DTO (без бэкенда)
|
||
- `PhotoDto` + `Photo`: `exif`, `tags`, `uploader`, `download_url`, `likes_count`,
|
||
`comments_count` (используется то, что нужно листу).
|
||
- `UserDto`/`me()`: `preferences`.
|
||
- `GaloApi`: `updateAlbum` (title/description/cover/clear), `deleteAlbum`, `members`,
|
||
share-links, `copyPhotos`, `addTag`/`removeTag`/`tags`, `download` (через существующий
|
||
`download_url`), `savePreferences`.
|
||
- `ProfileApi` не трогаем; `preferences` добавляется в ответ `me()`.
|
||
|
||
### 6. Настройки атрибутов — новый экран, состояние в профиле
|
||
Переносим каталог `EXIF_LABELS`/`EXIF_BASE` (web `exif.ts`) в Android-ресурс/объект и храним
|
||
`photo_info.visible` + `photo_info.labels` в `preferences` через `PATCH /auth/me/preferences`
|
||
(тот же формат, что у web). _Альтернатива_: собственный формат — рассинхрон настроек между
|
||
web и мобильным для одного пользователя.
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- [Конфликт жеста «вверх» с зумом/панорамой/плеером] → открывать лист только вне этих
|
||
состояний; для видео — не перехватывать жест, доступ через handle/кнопку.
|
||
- [Удаление/переименование локального альбома не имеет смысла и деструктивно] → действия
|
||
серверного уровня показываются только при связи и применяются к серверному альбому; без
|
||
связи раздел действий пуст.
|
||
- [Офлайн] → операции серверного уровня недоступны офлайн; используем уже существующее
|
||
офлайн-состояние экрана и показываем ошибку без падения.
|
||
- [Дублирование EXIF-каталога между web и Android] → переносим один в один и фиксируем
|
||
базовый набор; расхождение контролируется тестом.
|
||
- [Права доступа] → гейтим секции по `role` (владелец/админ/автор), как web.
|
||
|
||
### 7. Полная проверка синхронизации при запуске
|
||
`SyncWorker` реконсилирует все известные галереи (связанные плюс имеющие локальный индекс),
|
||
восстанавливает утраченные связи по меткам происхождения (случай частичной синхронизации) и
|
||
перевыставляет незавершённые загрузки (`requeuePending`). Запуск воркера происходит после
|
||
загрузки настроек (когда известен base URL), а смена режима на серверный сразу вызывает
|
||
`reconcile`. Та же проверка выполняется при открытии галереи. Разгрузка в режиме переноса
|
||
автоматическая: любой ассет, уже имеющий серверное фото и всё ещё хранящий оригинал на
|
||
устройстве, разгружается (включая повторные попытки и ассеты, оставшиеся «ожидающими
|
||
освобождения»). Обратный случай (переход из переноса в копии/выключено) возвращает оригиналы с
|
||
server и довосстанавливает незавершённый возврат при следующей проверке; восстановление не
|
||
обрывается на единичных ошибках. Решение по каждому ассету вынесено в политику
|
||
`GallerySyncPolicy` («режим × состояние × связанность»), которую применяет общая проверка
|
||
`SyncCheck` — так охвачены все сочетания, включая «копии с разгруженными файлами».
|
||
|
||
## Migration Plan
|
||
|
||
Изменение аддитивное, бэкенд и данные не мигрируются. Выкатывается в составе обычного релиза
|
||
Android-клиента; откат — откат версии приложения. Пользовательские `preferences` не меняют
|
||
формат, поэтому web и мобильный остаются совместимы.
|
||
|
||
## Open Questions
|
||
|
||
Нет — все решения, влияющие на спек и декомпозицию, приняты выше.
|