Files
ds-galo/openspec/changes/archive/2026-10-12-add-android-app/specs/android-client/spec.md
T

233 lines
13 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.
# Spec Delta
## Purpose
Android-клиент для self-hosted galo: настройка подключения к серверу, авторизация,
просмотр альбомов и медиа, а также фоновая загрузка фотографий и видео с телефона
с офлайн-кэшем.
## ADDED 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, а при отсутствии сети — из локального кэша.
#### 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** приложение показывает сообщение, при необходимости адаптированное под язык интерфейса