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

11 KiB
Raw Permalink Blame History

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

Нет — все решения, влияющие на спек и декомпозицию, приняты выше.