359 lines
21 KiB
Markdown
359 lines
21 KiB
Markdown
# 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** индикатор загрузки скрыт
|