Files
iu fa210e6c7f feat(android): album/photo attribute-actions and device gallery sync fixes
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).
2026-10-13 21:49:12 +03:00

130 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
Нет — все решения, влияющие на спек и декомпозицию, приняты выше.