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