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).
This commit is contained in:
iu
2026-10-13 21:49:12 +03:00
parent d1bb964685
commit fa210e6c7f
75 changed files with 6094 additions and 167 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-13
@@ -0,0 +1,129 @@
# 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
Нет — все решения, влияющие на спек и декомпозицию, приняты выше.
@@ -0,0 +1,52 @@
# Proposal
## Why
В web у альбомов и фото есть полноценное управление атрибутами и действиями (панель
обложки/информации/навигации, разделы «Действия», «Метки», «Информация» с настройками
EXIF-атрибутов). В Android-клиенте этого нет: экран альбома — только сетка, просмотр —
только изображение с навигацией, а `GaloApi` умеет лишь переименовать/удалить/переместить.
С телефона нельзя ни посмотреть свойства, ни выполнить действия «как в web».
## What Changes
- Новый **экранный жест**: свайп справа налево в любом альбоме (серверном и локальном)
открывает панель **«Атрибуты и действия»** (drawer справа).
- Панель альбома повторяет структуру web (`AlbumPanel` + экран настроек):
- Атрибуты: «Обложка» (превью, выбрать/сбросить), «Информация» (название, описание,
счётчики), «Навигация» (участники, настройки, ссылки для просмотра).
- Действия: «Переименовать», «Удалить».
- Источник данных: серверный альбом — данные сервера; локальный, связанный с серверным —
реальные атрибуты связанного серверного альбома; локальный без связи — локальные
значения (обложка/имя/счётчик), без раздела «Навигация».
- Новый **жест просмотра**: свайп вверх в полноэкранном просмотре (серверном и device)
открывает лист **«Атрибуты и действия»**, повторяющий web `PhotoPanel`:
- Атрибуты: «Информация» (автор, файл, тип, разрешение, EXIF, снято, координаты,
загружено) с настройками списка EXIF-атрибутов; «Метки».
- Действия: «Открыть (просмотр)» (из сетки), «Скачать», «Переместить», «Копировать»,
«Установить как обложку альбома», «Удалить»; переименование по тапу на имя.
- Для device-медиа: если элемент сопоставлен серверному фото — реальные серверные
атрибуты и действия; иначе локальные атрибуты и локальные действия.
- Расширение клиентского слоя Android: `GaloApi` + DTO получают операции альбома
(обновление/обложка/удаление/участники/share), копирование, метки, скачивание, `exif`
и `preferences`; новый экран «Настройки атрибутов» (видимость + подписи, в профиль).
- **Бэкенд не меняется**: все нужные endpoints уже есть.
## Capabilities
### New Capabilities
<!-- none -->
### Modified Capabilities
- `android-client`: добавляются требования к панели «Атрибуты и действия» на экранах
альбомов, к листу «Атрибуты и действия» в полноэкранном просмотре, к выбору источника
атрибутов для локальных альбомов/медиа и к поддерживающим операциям API/настройкам
атрибутов в профиле.
## Impact
- `android/app/src/main/java/com/galo/android/`: `data/network/GaloApi.kt`, `.../dto/Dtos.kt`,
`domain/model/`, `ui/album/`, `ui/viewer/`, `ui/devicealbum/`, новые экраны настроек.
- Тесты Android (`android/app/src/androidTest`, `test`).
- Бэкенд, web и схема spec-ов не затрагиваются (кроме новой delta для `android-client`).
@@ -0,0 +1,266 @@
# Spec Delta
## ADDED Requirements
### Requirement: Проверка доступа «Все файлы» при запуске
Приложение SHALL при запуске, когда оно уже настроено и авторизовано, проверять наличие доступа
«Все файлы» и при его отсутствии SHALL предлагать его включить, открывая системный экран настроек.
#### Scenario: Доступ не выдан
- **WHEN** приложение запущено и доступ «Все файлы» не выдан
- **THEN** показывается диалог с предложением включить доступ
#### Scenario: Включение доступа
- **WHEN** пользователь подтверждает
- **THEN** открывается системный экран выдачи доступа «Все файлы»
#### Scenario: Доступ уже выдан
- **WHEN** доступ уже выдан
- **THEN** диалог не показывается
### Requirement: Возврат оригиналов при выходе из переноса
При переключении альбома из режима переноса в «копии» или «выключено» приложение SHALL
возвращать оригиналы с сервера (когда пользователь выбирает возврат), а незавершённый возврат
SHALL довосстанавливаться при следующей проверке (при запуске и при открытии альбома).
#### Scenario: Перенос → копии
- **WHEN** пользователь переключает альбом из переноса в копии и подтверждает возврат оригиналов
- **THEN** оригиналы скачиваются с сервера и заменяют превью на устройстве
#### Scenario: Довосстановление
- **WHEN** альбом не в режиме переноса, но содержит превью с серверным фото
- **THEN** при запуске или открытии альбома оригиналы довосстанавливаются
#### Scenario: Перенос → выключено
- **WHEN** пользователь переключает перенос в выключено
- **THEN** предлагается вернуть оригиналы или оставить превью
### Requirement: Единая проверка соответствия режиму
При запуске и при открытии альбома приложение SHALL приводить локальное содержимое в
соответствие режиму для всех сочетаний состояния ассета: в «переносе» разгружать уже загруженные
оригиналы, а в «копиях»/«выключено» восстанавливать превью; отсутствующий локальный индекс SHALL
восстанавливаться по метке происхождения.
#### Scenario: Копии с разгруженными файлами
- **WHEN** альбом в режиме копий, а часть файлов разгружена (превью)
- **THEN** проверка восстанавливает оригиналы
#### Scenario: Перенос с неразгруженными файлами
- **WHEN** альбом в режиме переноса, файл есть на сервере, но оригинал ещё на телефоне
- **THEN** проверка разгружает оригинал
#### Scenario: Потерянный индекс
- **WHEN** у разгруженного медиа нет записи в локальном индексе
- **THEN** связь и происхождение восстанавливаются по метке в файле
#### Scenario: Выключенный режим
- **WHEN** альбом без синхронизации содержит превью
- **THEN** оригиналы восстанавливаются, а сервер не затрагивается
### Requirement: Отсутствие дублей и гонок при синхронизации
Проверка синхронизации SHALL выполняться последовательно (без одновременных проходов по одной
галерее) и SHALL NOT ставить повторную загрузку, пока по тому же элементу уже есть задание в
очереди. Метка происхождения в разгруженном файле SHALL хранить режим синхронизации, а не
состояние элемента, чтобы восстановление не меняло режим галереи.
#### Scenario: Повторная проверка во время загрузки
- **WHEN** проверка запускается снова, пока загрузка элемента ещё стоит в очереди
- **THEN** повторная загрузка не ставится
#### Scenario: Метка происхождения хранит режим
- **WHEN** разгруженный файл восстанавливается после переустановки
- **THEN** режим галереи берётся из метки как режим (перенос/копии), а не как состояние элемента
### Requirement: Подпись карточки альбома
Карточка альбома SHALL показывать в первой строке имя альбома, а для связанной пары — со
стрелкой имя связанного серверного альбома, и только когда имена различаются. Вторая строка
SHALL показывать значками расположение содержимого и число элементов: значок карты памяти для
локального без синхронизации, значки карты памяти и облака для режима копий, значок облака для
режима переноса; для серверного альбома — значок облака.
#### Scenario: Локальный без синхронизации
- **WHEN** показан локальный альбом без синхронизации
- **THEN** вторая строка показывает значок карты памяти и число элементов
#### Scenario: Режим копий
- **WHEN** локальный альбом синхронизируется в режиме копий
- **THEN** вторая строка показывает значки карты памяти и облака и число элементов
#### Scenario: Режим переноса
- **WHEN** локальный альбом синхронизируется в режиме переноса
- **THEN** вторая строка показывает значок облака и число элементов
#### Scenario: Разные имена связанной пары
- **WHEN** имя локального альбома не совпадает с именем связанного серверного
- **THEN** первая строка показывает «имя → имя серверного»
#### Scenario: Одинаковые имена
- **WHEN** имена совпадают
- **THEN** стрелка и имя серверного не показываются
### Requirement: Панель атрибутов и действий альбома
Приложение SHALL открывать панель «Атрибуты и действия» по свайпу справа налево на экране
любого альбома — серверного и локального. Панель SHALL повторять структуру web: разделы
«Обложка», «Информация», «Навигация» и раздел «Действия».
#### Scenario: Открытие свайпом
- **WHEN** пользователь свайпает справа налево в открытом альбоме
- **THEN** открывается панель «Атрибуты и действия»
#### Scenario: Структура панели
- **WHEN** панель открыта
- **THEN** показаны разделы «Обложка», «Информация», «Навигация» и «Действия»
#### Scenario: Закрытие панели
- **WHEN** пользователь закрывает панель (свайп в обратную сторону или касание вне панели)
- **THEN** панель скрывается, сетка медиа снова доступна
### Requirement: Источник атрибутов локального альбома
Для локального альбома приложение SHALL показывать ту же панель «Атрибуты и действия», что и
для серверного, а метаданные SHALL брать локально (имя, число элементов, локальная обложка,
режим синхронизации, связанный серверный альбом). Действия локального альбома SHALL
применяться локально.
#### Scenario: Локальные метаданные
- **WHEN** пользователь открывает панель локального альбома
- **THEN** раздел «Информация» показывает локальные имя, число элементов и обложку
#### Scenario: Неактивная навигация без связи
- **WHEN** локальный альбом не связан с серверным
- **THEN** строки «Участники» и «Ссылки для просмотра» показаны, но неактивны
#### Scenario: Активная навигация при связи
- **WHEN** локальный альбом связан с серверным
- **THEN** строки «Участники» и «Ссылки для просмотра» активны и относятся к связанному серверному альбому
### Requirement: Переименование и обложка локального альбома
Для локального альбома действие «Переименовать» SHALL менять отображаемое имя в приложении, не
изменяя медиа или bucket на устройстве, а «Выбрать обложку»/«Сбросить» SHALL назначать или
снимать обложку из медиа этого альбома. Имя и обложка SHALL храниться локально.
#### Scenario: Переименование
- **WHEN** пользователь вводит новое имя локального альбома
- **THEN** панель, заголовок и карточка в списке альбомов показывают новое имя
#### Scenario: Назначение и сброс обложки
- **WHEN** пользователь выбирает медиа обложкой или сбрасывает её
- **THEN** панель и карточка альбома отражают выбор
### Requirement: Удаление локального альбома с телефона
Действие «Удалить» локального альбома SHALL после подтверждения удалять все фото и видео
этого альбома с устройства, запрашивая при необходимости системное согласие.
#### Scenario: Подтверждение
- **WHEN** пользователь выбирает «Удалить» локального альбома
- **THEN** открывается диалог с предупреждением, что медиа будут удалены с телефона
#### Scenario: Удаление
- **WHEN** пользователь подтверждает удаление
- **THEN** медиа удаляются с устройства, а альбом исчезает из приложения
### Requirement: Атрибуты альбома
Панель SHALL показывать обложку с выбором и сбросом, информацию (название, описание, число
фото и участников) и переходы к участникам, настройкам и ссылкам для просмотра. Управление
обложкой и раздел «Навигация» SHALL быть доступны только владельцу и администратору.
#### Scenario: Управление обложкой
- **WHEN** владелец или администратор выбирает или сбрасывает обложку
- **THEN** изменение сохраняется на сервере и отражается в списке и панели
#### Scenario: Наблюдатель
- **WHEN** наблюдатель открывает панель
- **THEN** управление обложкой не показывается, а строки «Настройки» и «Ссылки для просмотра» неактивны
#### Scenario: Переход к настройкам
- **WHEN** владелец или администратор открывает настройки из раздела «Навигация»
- **THEN** доступны правка названия и описания и управление ссылками для просмотра
### Requirement: Действия с альбомом
Панель SHALL содержать действия «Переименовать» и «Удалить». Удаление SHALL подтверждаться
диалогом, а удалённый альбом SHALL помещать свои фото в корзину.
#### Scenario: Переименование
- **WHEN** владелец или администратор выбирает «Переименовать» и вводит новое название
- **THEN** название сохраняется на сервере и отражается в списке и заголовке
#### Scenario: Удаление с подтверждением
- **WHEN** владелец выбирает «Удалить» и подтверждает в диалоге
- **THEN** альбом удаляется, а его фото попадают в корзину
### Requirement: Лист атрибутов и действий фото
Полноэкранный просмотр SHALL открывать лист «Атрибуты и действия» по свайпу вверх как в
серверном, так и в device-просмотре. Лист SHALL повторять структуру web: разделы
«Информация», «Метки» и «Действия». При зуме изображения, панораме или активном управлении
видео свайп вверх SHALL NOT открывать лист.
#### Scenario: Открытие листа
- **WHEN** пользователь свайпает вверх во вписанном изображении
- **THEN** открывается лист «Атрибуты и действия» для текущего элемента
#### Scenario: Зум или панорама
- **WHEN** изображение увеличено или открыта панорама, и пользователь свайпает вверх
- **THEN** лист не открывается, жест обрабатывается изображением
#### Scenario: Видео
- **WHEN** открыт видеоэлемент, и пользователь взаимодействует с плеером
- **THEN** свайп вверх не мешает управлению воспроизведением
### Requirement: Атрибуты фото в листе
Раздел «Информация» SHALL показывать автора, размер файла, тип, разрешение, EXIF-атрибуты,
дату съёмки, координаты и дату загрузки. Раздел «Метки» SHALL позволять добавлять и снимать
метки с поиском по меткам пользователя и созданием новых.
#### Scenario: Информация
- **WHEN** лист открыт
- **THEN** раздел «Информация» показывает метаданные и EXIF текущего фото
#### Scenario: Метки
- **WHEN** пользователь добавляет или снимает метку в разделе «Метки»
- **THEN** изменение сразу отражается в просмотре без перезагрузки
### Requirement: Настройки списка атрибутов
Раздел «Информация» SHALL давать доступ к настройкам со списком EXIF-атрибутов, где
пользователь отмечает отображаемые атрибуты и задаёт их подписи, с действиями «отметить все»
и «снять все». Настройки SHALL применяться ко всем фото и сохраняться в профиле пользователя.
#### Scenario: Открытие настроек
- **WHEN** пользователь открывает настройки атрибутов
- **THEN** показан список EXIF-атрибутов с галочками видимости и полями подписей
#### Scenario: Сохранение настроек
- **WHEN** пользователь отмечает атрибуты, задаёт подписи и сохраняет
- **THEN** настройки сохраняются в профиле и применяются в разделе «Информация»
#### Scenario: Сброс настроек
- **WHEN** пользователь сбрасывает настройки
- **THEN** применяется базовый набор атрибутов с исходными подписями
### Requirement: Действия с фото в листе
Раздел «Действия» SHALL содержать «Скачать», «Переместить», «Копировать», «Установить как
обложку альбома» (по правам) и «Удалить»; из сетки также «Открыть (просмотр)». Удаление SHALL
подтверждаться диалогом. Переименование SHALL выполняться по тапу на имя файла.
#### Scenario: Состав действий
- **WHEN** лист открыт
- **THEN** показаны действия «Скачать», «Переместить», «Копировать», «Установить как обложку» и «Удалить» по правам
#### Scenario: Удаление фото
- **WHEN** пользователь выбирает «Удалить» и подтверждает
- **THEN** фото уходит в корзину, а просмотр переходит к соседнему элементу
#### Scenario: Переименование фото
- **WHEN** автор, владелец или администратор меняет имя файла на непустое
- **THEN** новое имя сохраняется и отражается в просмотре и сетке
### Requirement: Источник атрибутов device-медиа
Для device-медиа приложение SHALL показывать атрибуты и действия сопоставленного серверного
фото, когда сопоставление есть, и локальные атрибуты и действия (имя, размер, тип,
разрешение, дата, путь) при его отсутствии.
#### Scenario: Сопоставлено серверному фото
- **WHEN** device-медиа сопоставлено серверному фото
- **THEN** лист показывает серверные атрибуты и серверные действия этого фото
#### Scenario: Не сопоставлено
- **WHEN** сопоставления с серверным фото нет
- **THEN** лист показывает локальные атрибуты и локальные действия элемента
@@ -0,0 +1,65 @@
# Tasks
## 1. Клиентский API и DTO
- [x] 1.1 Добавить в `PhotoDto`/`Photo` поля `exif`, `tags`, `uploader`, `download_url` и разобрать их в маппере; проверить unit-тестом десериализации ответа `GET /photos/:id`
- [x] 1.2 Добавить `preferences` в ответ `me()` (`UserDto`/DTO профиля); проверить unit-тестом, что сохранённые `photo_info` читаются
- [x] 1.3 Добавить в `GaloApi` обновление альбома (`PATCH /albums/:id` с `title`/`description`/`cover_photo_id`/`clear_cover`), удаление (`DELETE`), список участников и share-ссылки; проверить unit-тестами запросов
- [x] 1.4 Добавить в `GaloApi` копирование (`POST /photo-batch/copy`), метки (`POST/DELETE /photos/:id/tags`, `GET /tags?q=`) и сохранение профиля (`PATCH /auth/me/preferences`); проверить unit-тестами
- [x] 1.5 Обновить `android/README.md` разделом о новых операциях API и проверить, что приведённые команды/примеры соответствуют коду
## 2. Модель атрибутов и резолвинг источника
- [x] 2.1 Ввести модель «атрибуты альбома» и резолвер источник→модель (серверный / локальный связанный / локальный без связи) отдельным классом домена; проверить unit-тестами все три случая
- [x] 2.2 Передавать сопоставление device-медиа→серверное фото (`serverPhotoByMediaId`) в `DeviceViewerScreen`; проверить unit-тестом, что элемент без сопоставления получает локальный источник, а со сопоставлением — серверный
## 3. Панель «Атрибуты и действия» на экранах альбомов
- [x] 3.1 Добавить на `AlbumScreen` end-drawer, открываемый свайпом справа налево, с общим контентом панели; проверить androidTest-жестом открытия и закрытия
- [x] 3.2 Добавить такой же end-drawer на `DeviceAlbumScreen`, не меняя левый drawer настроек синхронизации; проверить androidTest, что оба drawer'а открываются независимо
- [x] 3.3 Реализовать разделы «Обложка», «Информация», «Навигация» с гейтингом по роли (владелец/админ); проверить androidTest: у наблюдателя нет управления обложкой и навигации
- [x] 3.4 Реализовать действия «Переименовать» и «Удалить» с диалогом подтверждения и постановкой фото в корзину; проверить androidTest обоих сценариев
- [x] 3.5 Добавить экран настроек альбома (правка названия/описания, список участников, управление share-ссылками) и переход к нему из «Навигации»; проверить androidTest сохранения названия и создания/отзыва ссылки
## 4. Лист «Атрибуты и действия» в просмотре
- [x] 4.1 Добавить `ModalBottomSheet` в `ViewerScreen` и `DeviceViewerScreen` с открытием по свайпу вверх и условием «не открывать при зуме, панораме и активном видео»; проверить androidTest: при зуме/панораме лист не открывается
- [x] 4.2 Реализовать раздел «Информация» (автор, файл, тип, разрешение, EXIF, снято, координаты, загружено) и раздел «Метки» с добавлением/снятием и поиском; проверить androidTest появления метки и обновления без перезагрузки
- [x] 4.3 Реализовать действия «Скачать», «Переместить», «Копировать», «Установить как обложку», «Удалить» (по правам) и переименование по тапу на имя; проверить androidTest каждого действия, включая подтверждение удаления
## 5. Настройки списка атрибутов
- [x] 5.1 Перенести каталог EXIF-атрибутов и базовый набор (эквивалент web `exif.ts`) в Android; проверить unit-тестом, что базовый набор и подписи совпадают с web
- [x] 5.2 Реализовать экран настроек атрибутов (галочки видимости, поля подписей, «отметить все»/«снять все») и сохранение/сброс через `preferences`; проверить androidTest сохранения и сброса настроек
## 6. Интеграция и проверка
- [x] 6.1 Прогнать сквозные сценарии: серверный альбом (панель+настройки), device-альбом связанный и без связи, просмотр серверный и device (лист, метки, действия); проверить androidTest-сценариями
- [x] 6.2 Собрать и прогнать статический анализ: `./gradlew :app:assembleDebug :app:lint :app:testDebugUnitTest` без ошибок
## 7. Локальные атрибуты и действия (все альбомы)
- [x] 7.1 Добавить app-only метаданные локального альбома (Room `device_album_meta`: отображаемое имя и обложка) и применять их в панели и списке альбомов; проверить unit- и Robolectric-тестами
- [x] 7.2 Показывать раздел «Навигация» у всех альбомов; строки «Участники»/«Ссылки» активны только при связи с серверным альбомом, иначе неактивны; проверить тестом
- [x] 7.3 Реализовать «Переименовать» (отображаемое имя в приложении) и локальную обложку (выбор из медиа альбома/сброс) для локального альбома; проверить тестами
- [x] 7.4 Реализовать «Удалить» локального альбома: подтверждение + удаление медиа с телефона через системное согласие (`MediaStore.createDeleteRequest`); проверить тестами
## 8. Полная проверка синхронизации
- [x] 8.1 `SyncWorker` реконсилирует все известные галереи (связанные + с локальным индексом), восстанавливает частичные связи через `recover` и перевыставляет незавершённые загрузки; проверить unit-тестами (`reconcileAll`, `indexedAlbumIds`)
- [x] 8.2 Запускать стартовую проверку после загрузки настроек (base URL) и вызывать `reconcile` при смене режима на серверный (не создавая связь с пустым серверным альбомом); проверить сборкой и тестами
- [x] 8.3 Разгрузка в режиме переноса выполняется автоматически после загрузки и повторно на следующем проходе (без баннера и отдельных запросов согласия); проверить unit-тестом повтора `AWAITING_FREE`
- [x] 8.4 Проверять доступ «Все файлы» при запуске и предлагать включить (диалог + системный экран); проверить Robolectric-тестом
- [x] 8.5 Проверка и исправление при открытии галереи: любой ассет, уже загруженный на сервер, но всё ещё с оригиналом на телефоне, разгружается в режиме переноса; проверить unit-тестами
- [x] 8.6 Возврат оригиналов при перенос→копии/выключено: восстановление не обрывается на единичных ошибках + довосстановление при запуске и открытии альбома; проверить тестами (`restoreTargets`, матрица переходов)
- [x] 8.7 Единая политика и проверка «режим × состояние» (`GallerySyncPolicy`/`SyncCheck`) с восстановлением индекса по метке; применяется при запуске и открытии альбома; проверить тестами на все сочетания
- [x] 8.8 Устранить гонки и дубли: сериализация `reconcile`, пропуск повторной загрузки при наличии задания в очереди, метка происхождения хранит режим (не состояние); проверить unit-тестами и инструментальными E2E на эмуляторе
## 9. Подпись карточки альбома
- [x] 9.1 Переделать подпись карточки: первая строка — имя и «→ имя серверного» только при различии имён; вторая строка — значки расположения (карта памяти / карта памяти + облако / облако) и число элементов; проверить Robolectric-тестами
## Workflow follow-up
- Архивация change после прохождения ревью проекта.
- Проверка результата архивации.
+263
View File
@@ -356,3 +356,266 @@ SHALL выполняться свайпом.
- **WHEN** оригинал загружен или недоступен
- **THEN** индикатор загрузки скрыт
### Requirement: Проверка доступа «Все файлы» при запуске
Приложение SHALL при запуске, когда оно уже настроено и авторизовано, проверять наличие доступа
«Все файлы» и при его отсутствии SHALL предлагать его включить, открывая системный экран настроек.
#### Scenario: Доступ не выдан
- **WHEN** приложение запущено и доступ «Все файлы» не выдан
- **THEN** показывается диалог с предложением включить доступ
#### Scenario: Включение доступа
- **WHEN** пользователь подтверждает
- **THEN** открывается системный экран выдачи доступа «Все файлы»
#### Scenario: Доступ уже выдан
- **WHEN** доступ уже выдан
- **THEN** диалог не показывается
### Requirement: Возврат оригиналов при выходе из переноса
При переключении альбома из режима переноса в «копии» или «выключено» приложение SHALL
возвращать оригиналы с сервера (когда пользователь выбирает возврат), а незавершённый возврат
SHALL довосстанавливаться при следующей проверке (при запуске и при открытии альбома).
#### Scenario: Перенос → копии
- **WHEN** пользователь переключает альбом из переноса в копии и подтверждает возврат оригиналов
- **THEN** оригиналы скачиваются с сервера и заменяют превью на устройстве
#### Scenario: Довосстановление
- **WHEN** альбом не в режиме переноса, но содержит превью с серверным фото
- **THEN** при запуске или открытии альбома оригиналы довосстанавливаются
#### Scenario: Перенос → выключено
- **WHEN** пользователь переключает перенос в выключено
- **THEN** предлагается вернуть оригиналы или оставить превью
### Requirement: Единая проверка соответствия режиму
При запуске и при открытии альбома приложение SHALL приводить локальное содержимое в
соответствие режиму для всех сочетаний состояния ассета: в «переносе» разгружать уже загруженные
оригиналы, а в «копиях»/«выключено» восстанавливать превью; отсутствующий локальный индекс SHALL
восстанавливаться по метке происхождения.
#### Scenario: Копии с разгруженными файлами
- **WHEN** альбом в режиме копий, а часть файлов разгружена (превью)
- **THEN** проверка восстанавливает оригиналы
#### Scenario: Перенос с неразгруженными файлами
- **WHEN** альбом в режиме переноса, файл есть на сервере, но оригинал ещё на телефоне
- **THEN** проверка разгружает оригинал
#### Scenario: Потерянный индекс
- **WHEN** у разгруженного медиа нет записи в локальном индексе
- **THEN** связь и происхождение восстанавливаются по метке в файле
#### Scenario: Выключенный режим
- **WHEN** альбом без синхронизации содержит превью
- **THEN** оригиналы восстанавливаются, а сервер не затрагивается
### Requirement: Отсутствие дублей и гонок при синхронизации
Проверка синхронизации SHALL выполняться последовательно (без одновременных проходов по одной
галерее) и SHALL NOT ставить повторную загрузку, пока по тому же элементу уже есть задание в
очереди. Метка происхождения в разгруженном файле SHALL хранить режим синхронизации, а не
состояние элемента, чтобы восстановление не меняло режим галереи.
#### Scenario: Повторная проверка во время загрузки
- **WHEN** проверка запускается снова, пока загрузка элемента ещё стоит в очереди
- **THEN** повторная загрузка не ставится
#### Scenario: Метка происхождения хранит режим
- **WHEN** разгруженный файл восстанавливается после переустановки
- **THEN** режим галереи берётся из метки как режим (перенос/копии), а не как состояние элемента
### Requirement: Подпись карточки альбома
Карточка альбома SHALL показывать в первой строке имя альбома, а для связанной пары — со
стрелкой имя связанного серверного альбома, и только когда имена различаются. Вторая строка
SHALL показывать значками расположение содержимого и число элементов: значок карты памяти для
локального без синхронизации, значки карты памяти и облака для режима копий, значок облака для
режима переноса; для серверного альбома — значок облака.
#### Scenario: Локальный без синхронизации
- **WHEN** показан локальный альбом без синхронизации
- **THEN** вторая строка показывает значок карты памяти и число элементов
#### Scenario: Режим копий
- **WHEN** локальный альбом синхронизируется в режиме копий
- **THEN** вторая строка показывает значки карты памяти и облака и число элементов
#### Scenario: Режим переноса
- **WHEN** локальный альбом синхронизируется в режиме переноса
- **THEN** вторая строка показывает значок облака и число элементов
#### Scenario: Разные имена связанной пары
- **WHEN** имя локального альбома не совпадает с именем связанного серверного
- **THEN** первая строка показывает «имя → имя серверного»
#### Scenario: Одинаковые имена
- **WHEN** имена совпадают
- **THEN** стрелка и имя серверного не показываются
### Requirement: Панель атрибутов и действий альбома
Приложение SHALL открывать панель «Атрибуты и действия» по свайпу справа налево на экране
любого альбома — серверного и локального. Панель SHALL повторять структуру web: разделы
«Обложка», «Информация», «Навигация» и раздел «Действия».
#### Scenario: Открытие свайпом
- **WHEN** пользователь свайпает справа налево в открытом альбоме
- **THEN** открывается панель «Атрибуты и действия»
#### Scenario: Структура панели
- **WHEN** панель открыта
- **THEN** показаны разделы «Обложка», «Информация», «Навигация» и «Действия»
#### Scenario: Закрытие панели
- **WHEN** пользователь закрывает панель (свайп в обратную сторону или касание вне панели)
- **THEN** панель скрывается, сетка медиа снова доступна
### Requirement: Источник атрибутов локального альбома
Для локального альбома приложение SHALL показывать ту же панель «Атрибуты и действия», что и
для серверного, а метаданные SHALL брать локально (имя, число элементов, локальная обложка,
режим синхронизации, связанный серверный альбом). Действия локального альбома SHALL
применяться локально.
#### Scenario: Локальные метаданные
- **WHEN** пользователь открывает панель локального альбома
- **THEN** раздел «Информация» показывает локальные имя, число элементов и обложку
#### Scenario: Неактивная навигация без связи
- **WHEN** локальный альбом не связан с серверным
- **THEN** строки «Участники» и «Ссылки для просмотра» показаны, но неактивны
#### Scenario: Активная навигация при связи
- **WHEN** локальный альбом связан с серверным
- **THEN** строки «Участники» и «Ссылки для просмотра» активны и относятся к связанному серверному альбому
### Requirement: Переименование и обложка локального альбома
Для локального альбома действие «Переименовать» SHALL менять отображаемое имя в приложении, не
изменяя медиа или bucket на устройстве, а «Выбрать обложку»/«Сбросить» SHALL назначать или
снимать обложку из медиа этого альбома. Имя и обложка SHALL храниться локально.
#### Scenario: Переименование
- **WHEN** пользователь вводит новое имя локального альбома
- **THEN** панель, заголовок и карточка в списке альбомов показывают новое имя
#### Scenario: Назначение и сброс обложки
- **WHEN** пользователь выбирает медиа обложкой или сбрасывает её
- **THEN** панель и карточка альбома отражают выбор
### Requirement: Удаление локального альбома с телефона
Действие «Удалить» локального альбома SHALL после подтверждения удалять все фото и видео
этого альбома с устройства, запрашивая при необходимости системное согласие.
#### Scenario: Подтверждение
- **WHEN** пользователь выбирает «Удалить» локального альбома
- **THEN** открывается диалог с предупреждением, что медиа будут удалены с телефона
#### Scenario: Удаление
- **WHEN** пользователь подтверждает удаление
- **THEN** медиа удаляются с устройства, а альбом исчезает из приложения
### Requirement: Атрибуты альбома
Панель SHALL показывать обложку с выбором и сбросом, информацию (название, описание, число
фото и участников) и переходы к участникам, настройкам и ссылкам для просмотра. Управление
обложкой и раздел «Навигация» SHALL быть доступны только владельцу и администратору.
#### Scenario: Управление обложкой
- **WHEN** владелец или администратор выбирает или сбрасывает обложку
- **THEN** изменение сохраняется на сервере и отражается в списке и панели
#### Scenario: Наблюдатель
- **WHEN** наблюдатель открывает панель
- **THEN** управление обложкой не показывается, а строки «Настройки» и «Ссылки для просмотра» неактивны
#### Scenario: Переход к настройкам
- **WHEN** владелец или администратор открывает настройки из раздела «Навигация»
- **THEN** доступны правка названия и описания и управление ссылками для просмотра
### Requirement: Действия с альбомом
Панель SHALL содержать действия «Переименовать» и «Удалить». Удаление SHALL подтверждаться
диалогом, а удалённый альбом SHALL помещать свои фото в корзину.
#### Scenario: Переименование
- **WHEN** владелец или администратор выбирает «Переименовать» и вводит новое название
- **THEN** название сохраняется на сервере и отражается в списке и заголовке
#### Scenario: Удаление с подтверждением
- **WHEN** владелец выбирает «Удалить» и подтверждает в диалоге
- **THEN** альбом удаляется, а его фото попадают в корзину
### Requirement: Лист атрибутов и действий фото
Полноэкранный просмотр SHALL открывать лист «Атрибуты и действия» по свайпу вверх как в
серверном, так и в device-просмотре. Лист SHALL повторять структуру web: разделы
«Информация», «Метки» и «Действия». При зуме изображения, панораме или активном управлении
видео свайп вверх SHALL NOT открывать лист.
#### Scenario: Открытие листа
- **WHEN** пользователь свайпает вверх во вписанном изображении
- **THEN** открывается лист «Атрибуты и действия» для текущего элемента
#### Scenario: Зум или панорама
- **WHEN** изображение увеличено или открыта панорама, и пользователь свайпает вверх
- **THEN** лист не открывается, жест обрабатывается изображением
#### Scenario: Видео
- **WHEN** открыт видеоэлемент, и пользователь взаимодействует с плеером
- **THEN** свайп вверх не мешает управлению воспроизведением
### Requirement: Атрибуты фото в листе
Раздел «Информация» SHALL показывать автора, размер файла, тип, разрешение, EXIF-атрибуты,
дату съёмки, координаты и дату загрузки. Раздел «Метки» SHALL позволять добавлять и снимать
метки с поиском по меткам пользователя и созданием новых.
#### Scenario: Информация
- **WHEN** лист открыт
- **THEN** раздел «Информация» показывает метаданные и EXIF текущего фото
#### Scenario: Метки
- **WHEN** пользователь добавляет или снимает метку в разделе «Метки»
- **THEN** изменение сразу отражается в просмотре без перезагрузки
### Requirement: Настройки списка атрибутов
Раздел «Информация» SHALL давать доступ к настройкам со списком EXIF-атрибутов, где
пользователь отмечает отображаемые атрибуты и задаёт их подписи, с действиями «отметить все»
и «снять все». Настройки SHALL применяться ко всем фото и сохраняться в профиле пользователя.
#### Scenario: Открытие настроек
- **WHEN** пользователь открывает настройки атрибутов
- **THEN** показан список EXIF-атрибутов с галочками видимости и полями подписей
#### Scenario: Сохранение настроек
- **WHEN** пользователь отмечает атрибуты, задаёт подписи и сохраняет
- **THEN** настройки сохраняются в профиле и применяются в разделе «Информация»
#### Scenario: Сброс настроек
- **WHEN** пользователь сбрасывает настройки
- **THEN** применяется базовый набор атрибутов с исходными подписями
### Requirement: Действия с фото в листе
Раздел «Действия» SHALL содержать «Скачать», «Переместить», «Копировать», «Установить как
обложку альбома» (по правам) и «Удалить»; из сетки также «Открыть (просмотр)». Удаление SHALL
подтверждаться диалогом. Переименование SHALL выполняться по тапу на имя файла.
#### Scenario: Состав действий
- **WHEN** лист открыт
- **THEN** показаны действия «Скачать», «Переместить», «Копировать», «Установить как обложку» и «Удалить» по правам
#### Scenario: Удаление фото
- **WHEN** пользователь выбирает «Удалить» и подтверждает
- **THEN** фото уходит в корзину, а просмотр переходит к соседнему элементу
#### Scenario: Переименование фото
- **WHEN** автор, владелец или администратор меняет имя файла на непустое
- **THEN** новое имя сохраняется и отражается в просмотре и сетке
### Requirement: Источник атрибутов device-медиа
Для device-медиа приложение SHALL показывать атрибуты и действия сопоставленного серверного
фото, когда сопоставление есть, и локальные атрибуты и действия (имя, размер, тип,
разрешение, дата, путь) при его отсутствии.
#### Scenario: Сопоставлено серверному фото
- **WHEN** device-медиа сопоставлено серверному фото
- **THEN** лист показывает серверные атрибуты и серверные действия этого фото
#### Scenario: Не сопоставлено
- **WHEN** сопоставления с серверным фото нет
- **THEN** лист показывает локальные атрибуты и локальные действия элемента