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,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** лист показывает локальные атрибуты и локальные действия элемента