Files
ds-galo/openspec/specs/android-client/spec.md
T
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

622 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# android-client Specification
## Purpose
Android-клиент для self-hosted galo: настройка подключения к серверу, авторизация,
просмотр альбомов и медиа, а также фоновая загрузка фотографий и видео с телефона
с офлайн-кэшем.
## Requirements
### Requirement: Настройка подключения к серверу
Приложение SHALL требовать явного указания базового URL инстанса galo при первом
запуске и SHALL проверять доступность сервера перед переходом к работе.
#### Scenario: Успешная настройка
- **WHEN** пользователь вводит достижимый базовый URL и подтверждает
- **THEN** приложение проверяет `GET /api/health` и при успехе переходит к входу
#### Scenario: Сервер недоступен
- **WHEN** сервер недоступен или health вернул ошибку
- **THEN** приложение показывает ошибку и остаётся на экране настройки, сохраняя введённый URL
### Requirement: Небезопасное соединение по LAN
Приложение SHALL по умолчанию запрещать незашифрованный HTTP и SHALL разрешать его
только после явного включения пользователем режима небезопасного соединения.
#### Scenario: HTTP без разрешения
- **WHEN** пользователь указывает `http`-URL и режим небезопасного соединения выключен
- **THEN** приложение блокирует подключение и предлагает включить режим
#### Scenario: Режим включён
- **WHEN** пользователь включил режим небезопасного соединения
- **THEN** HTTP-подключение разрешено, а интерфейс явно отмечает небезопасное соединение
### Requirement: Разрешение относительных URL объектов
Приложение SHALL трактовать относительные URL объектов из REST-ответов и SSE как пути
на выбранном базовом URL сервера.
#### Scenario: Отдача объекта
- **WHEN** API возвращает `thumb_url` вида `/api/objects/<key>?...`
- **THEN** приложение запрашивает объект с того же инстанса, что и базовый URL
### Requirement: Вход и хранение сессии
Приложение SHALL позволять вход и регистрацию по email и паролю и SHALL хранить токены
только в защищённом хранилище платформы.
#### Scenario: Успешный вход
- **WHEN** вход проходит успешно
- **THEN** токены сохраняются в защищённом хранилище, пользователь видит список альбомов
#### Scenario: Перезапуск с сессией
- **WHEN** приложение перезапущено с сохранённой сессией
- **THEN** пользователь остаётся авторизованным без повторного ввода пароля
### Requirement: Автоматическое обновление токена
Приложение SHALL автоматически обновлять access-токен при ответе 401, пока refresh-токен
действителен, и SHALL завершать сессию при неудачном обновлении.
#### Scenario: Прозрачное обновление
- **WHEN** запрос получает 401 и refresh-токен действителен
- **THEN** приложение обновляет токен и повторяет исходный запрос без участия пользователя
#### Scenario: Обновление не удалось
- **WHEN** обновление токена завершилось ошибкой
- **THEN** сессия очищается и пользователь возвращается на экран входа
### Requirement: Список альбомов
Приложение SHALL показывать доступные пользователю альбомы одним смешанным списком:
серверные альбомы, получаемые из API, а при отсутствии сети — из локального кэша, и
альбомы устройства. Элементы SHALL различать источник. Связанная пара «альбом
устройства → серверный альбом» SHALL отображаться одной карточкой альбома устройства
с указателем связи и меткой режима; связанный серверный альбом SHALL NOT показываться
в списке отдельной записью.
#### Scenario: Онлайн
- **WHEN** авторизованный пользователь открывает приложение
- **THEN** отображается список альбомов с обложками или плейсхолдером
#### Scenario: Офлайн из кэша
- **WHEN** сеть недоступна, но кэш существует
- **THEN** список альбомов отображается из кэша с признаком офлайн-режима
#### Scenario: Альбомы устройства
- **WHEN** у приложения есть доступ к медиа устройства
- **THEN** в списке появляются альбомы устройства наряду с серверными и помечены источником
#### Scenario: Связанная пара
- **WHEN** альбом устройства синхронизируется с серверным альбомом
- **THEN** в списке показывается одна карточка альбома устройства с указателем связи и меткой режима, а связанный серверный альбом отдельной записью не показывается
### Requirement: Экран альбома устройства и настройки синхронизации
Приложение SHALL открывать альбом устройства с сеткой медиа и SHALL предоставлять
настройки синхронизации этого альбома по свайпу вправо. Настройки SHALL содержать
только выбор режима синхронизации.
#### Scenario: Открытие альбома устройства
- **WHEN** пользователь выбирает альбом устройства в списке
- **THEN** открывается сетка медиа этого альбома
#### Scenario: Настройки по свайпу
- **WHEN** пользователь свайпает вправо в открытом альбоме устройства
- **THEN** открываются настройки синхронизации с выбором режима и только им
#### Scenario: Метка перенесённого элемента
- **WHEN** оригинал элемента разгружен на сервер в режиме переноса
- **THEN** в сетке на элементе показывается значок облака
### Requirement: Доступ к альбомам устройства
Приложение SHALL запрашивать доступ к медиа устройства, нужный для перечисления
альбомов и чтения файлов, и SHALL работать без него, не показывая альбомы
устройства.
#### Scenario: Доступ предоставлен
- **WHEN** пользователь предоставляет доступ к медиа устройства
- **THEN** приложение перечисляет альбомы устройства и их содержимое
#### Scenario: Доступ не предоставлен
- **WHEN** доступ к медиа устройства не предоставлен
- **THEN** приложение продолжает работать с серверными альбомами и не показывает альбомы устройства
### Requirement: Постраничный просмотр альбома
Приложение SHALL показывать сетку медиа альбома с постраничной загрузкой по курсору и
SHALL подгружать следующую страницу при достижении конца списка.
#### Scenario: Догрузка страницы
- **WHEN** пользователь прокручивает сетку до конца
- **THEN** приложение запрашивает следующую страницу по курсору и добавляет её к сетке
#### Scenario: Пустой альбом
- **WHEN** в альбоме нет медиа
- **THEN** отображается пустое состояние
### Requirement: Просмотр медиа на весь экран
Полноэкранный просмотр SHALL поддерживать изображение с зумом пальцами, видео с постером
и управлением воспроизведением, а также панораму 360; навигация между элементами альбома
SHALL выполняться свайпом.
#### Scenario: Свайп между элементами
- **WHEN** пользователь свайпает в просмотре
- **THEN** показывается следующий или предыдущий элемент альбома без выхода из просмотра
#### Scenario: Видео
- **WHEN** открыт видеоэлемент
- **THEN** показывается постер и доступно воспроизведение с перемоткой
### Requirement: Обновления в реальном времени
Пока экран альбома активен, приложение SHALL отображать изменения по SSE (новые,
обновлённые и удалённые медиа, социальные события) и SHALL догонять пропущенное при
переподключении.
#### Scenario: Новое фото с другого устройства
- **WHEN** другое устройство добавляет фото в открытый альбом
- **THEN** новое фото появляется в сетке без ручного обновления
#### Scenario: Переподключение
- **WHEN** SSE-соединение разорвалось и восстановилось
- **THEN** приложение дозагружает изменения, произошедшие за время разрыва
### Requirement: Выбор медиа и альбома для загрузки
Приложение SHALL предоставлять выбор одного или нескольких фото и видео из системной
галереи и SHALL требовать выбора альбома назначения перед постановкой в очередь.
#### Scenario: Постановка в очередь
- **WHEN** пользователь выбрал файлы и альбом назначения
- **THEN** задания загрузки ставятся в очередь и пользователь видит их статус
#### Scenario: Отказ сервера
- **WHEN** сервер отклоняет загрузку (например, из-за прав или размера)
- **THEN** задание помечается ошибкой с причиной от сервера
### Requirement: Фоновая очередь загрузки
Загрузка SHALL выполняться по одному файлу за HTTP-запрос и SHALL продолжаться при
уходе приложения из foreground, показывая прогресс в уведомлении; при ошибке SHALL
выполняться повтор с нарастающей задержкой.
#### Scenario: Уход в фон
- **WHEN** приложение уходит в фон во время загрузки
- **THEN** загрузка продолжается и прогресс отображается в уведомлении
#### Scenario: Повтор при ошибке
- **WHEN** загрузка файла завершилась сетевой ошибкой
- **THEN** задание повторяется, а после исчерпания попыток помечается ошибкой
### Requirement: Условия сети для загрузки
Приложение SHALL позволять ограничить загрузку сетью Wi-Fi; при ожидании подходящей
сети задание остаётся в очереди и не выполняется.
#### Scenario: Только Wi-Fi
- **WHEN** ограничение «только Wi-Fi» включено, а доступна только сотовая сеть
- **THEN** загрузка не начинается до появления Wi-Fi, а задание остаётся в очереди
### Requirement: Сохранность очереди между запусками
Очередь загрузки SHALL сохраняться на устройстве и SHALL продолжаться после перезапуска
приложения или устройства.
#### Scenario: Возобновление после перезапуска
- **WHEN** приложение или устройство перезапускается при незавершённой очереди
- **THEN** незавершённые задания возобновляются
### Requirement: Конвертация HEIC в JPEG
Файлы HEIC/HEIF SHALL конвертироваться на устройстве в JPEG перед загрузкой с
сохранением даты съёмки и геолокации.
#### Scenario: Загрузка HEIC
- **WHEN** пользователь ставит в очередь HEIC-файл
- **THEN** загружается JPEG-версия, а дата съёмки и координаты из EXIF сохраняются
#### Scenario: Ошибка конвертации
- **WHEN** конвертация завершилась ошибкой
- **THEN** задание помечается ошибкой, повреждённый файл не загружается
### Requirement: Кэш медиа по устойчивому ключу
Приложение SHALL кэшировать полученные превью и оригиналы по устойчивому идентификатору
объекта, не зависящему от подписи и срока действия URL.
#### Scenario: Смена подписи
- **WHEN** URL одного и того же объекта со временем получает новую подпись
- **THEN** приложение переиспользует закэшированный объект вместо повторной загрузки
#### Scenario: Офлайн-отдача из кэша
- **WHEN** сеть недоступна, а объект есть в кэше
- **THEN** объект отображается из кэша
### Requirement: Единый формат ошибок API
Приложение SHALL распознавать ошибки API формата `{error:{code,message}}` и SHALL
показывать пользователю понятное сообщение.
#### Scenario: Ошибка с кодом и сообщением
- **WHEN** сервер возвращает ошибку с `code` и `message`
- **THEN** приложение показывает сообщение, при необходимости адаптированное под язык интерфейса
### Requirement: Зум изображения жестами
Полноэкранный просмотр изображения SHALL поддерживать увеличение и уменьшение щипком
(pinch), а при увеличенном изображении — панорамирование перетаскиванием.
#### Scenario: Щипок увеличивает изображение
- **WHEN** пользователь разводит пальцы на изображении
- **THEN** изображение увеличивается относительно вписанного вида
#### Scenario: Панорамирование при увеличении
- **WHEN** изображение увеличено, и пользователь перетаскивает его пальцем
- **THEN** видимая область изображения смещается, а границы изображения не отрываются от краёв области просмотра
### Requirement: Двойной тап переключает вписанный вид и 100%
Двойной тап по изображению SHALL переключать вписанный вид и масштаб 100% натурального
размера с якорем в точку нажатия.
#### Scenario: Вход в 100% из вписанного вида
- **WHEN** изображение показано вписанным, и пользователь делает двойной тап
- **THEN** изображение показывается в масштабе 100% натурального размера с центром в точке нажатия
#### Scenario: Возврат во вписанный вид
- **WHEN** изображение увеличено, и пользователь делает двойной тап
- **THEN** изображение возвращается во вписанный вид
### Requirement: Предел зума по натуральному размеру
Зум изображения SHALL ограничиваться снизу вписанным видом, а сверху — 200% натурального
размера изображения; при входе в зум SHALL догружаться оригинал изображения.
#### Scenario: Верхний предел
- **WHEN** пользователь продолжает увеличивать изображение за 200% натурального размера
- **THEN** масштаб останавливается на 200%, изображение не увеличивается дальше
#### Scenario: Оригинал при увеличении
- **WHEN** пользователь входит в зум
- **THEN** догружается оригинал изображения и подменяет превью по факту загрузки
### Requirement: Навигация и сброс при зуме
Пока изображение увеличено, горизонтальный свайп SHALL NOT листать альбом, а жест 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 довосстанавливаться при следующей проверке (при запуске и при открытии альбома).
#### 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** лист показывает локальные атрибуты и локальные действия элемента