# 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 Нет — все решения, влияющие на спек и декомпозицию, приняты выше.