ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m1s
ci / db / db (push) Successful in 1m16s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m12s
ci / api / api (push) Successful in 45s
ci / recommender / recommender (push) Successful in 1m50s
ci / ts / ts (packages + web) (push) Failing after 6m53s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m13s
- Introduced a new warning for users uploading non-FLAC or WAV formats, indicating that these formats should only be used as a last resort. - Added functionality to highlight files needing format attention in the upload queue. - Updated the UI to provide clearer guidance on acceptable audio formats during uploads. - Improved documentation to reflect changes in upload requirements and user interface updates.
13 KiB
13 KiB
Android-приложение (apps/mobile)
Стек
| Задача | Выбор |
|---|---|
| Фреймворк | React Native + Expo SDK, prebuild / dev client (нативный код под контролем). New Architecture выключена, пока react-native-track-player её не поддерживает |
| Навигация | expo-router (файловые маршруты) |
| Аудио | react-native-track-player v4 — ExoPlayer, MediaSession, уведомление, экран блокировки, Android Auto-совместимость |
| Анимация волны | @shopify/react-native-skia + Reanimated: кадр собирается в UI-потоке (createPicture в useDerivedValue), формулы — общие с вебом (packages/core/src/wave-anim.ts, worklet'ы), отрисовка — src/ui/wave/draw.ts; вне экрана и в фоне кадры не считаются |
| Серверное состояние | TanStack Query (+ persist в MMKV) |
| Клиентское состояние | Zustand (общие сторы из packages/core) |
| Хранилище | expo-secure-store (refresh-токены, список серверов, недавние запросы); expo-sqlite — офлайн-каталог и очередь событий (вместе с загрузками) |
| Загрузки | expo-file-system (фоновые загрузки, resumable) |
| Сеть | @react-native-community/netinfo (Wi-Fi / сотовая → качество) |
| i18n | i18next, словари из packages/i18n |
| Минимальная версия | Android 8.0 (API 26), target — актуальный API |
Сборка APK: ABI и 16 КБ страницы
- APK только под
arm64-v8a(plugins/withArm64Only.js): нативные модули собираются под одну ABI, а без фильтра в APK попадали неполные каталогиx86,x86_64,armeabi-v7aиз библиотек React Native — эмулятор x86 и 32-битные устройства выбирали их и падали при старте (couldn't find DSO to load: libexpo-modules-core.so). - Все
.soвыровнены по 16 КБ: устройства с Android 15+ и 16 КБ страницами памяти не загружают библиотеки с выравниванием 4 КБ, приложение закрывается сразу после запуска.
Экраны
Сводка внешнего вида — design.md.
- Подключение и вход: адрес сервера (проверка
GET /server-info: версия сервера, HTTPS, поддерживается ли эта версия приложения), список сохранённых серверов, затем логин, пароль, язык. Далее те же setup-экраны, что и в вебе. - Нижняя навигация: Главная · Поиск · Коллекция · Профиль. Мини-плеер над навигацией; свайп вверх — полноэкранный
плеер в одном из двух видов (выбор в профиле):
cover— крупная обложка (по умолчанию) илиwave— визуализация волны; прогресс, лайк / дизлайк, скачать, очередь (нижний лист). - Поиск: недавние запросы (хранятся локально), подсказка про опечатки и раскладку, значок «скачано» у треков.
- Главная: кнопка «Моя волна» (
orbилиliquidпо настройке, тот же алгоритм, что в вебе, на Skia; пульс в такт BPM / energy изfeaturesтрека — на Android спектр недоступен из ExoPlayer без нативного модуля, поэтому анимация управляется метаданными и позицией трека), выбор занятия / настроения. - Коллекция: История, Лайки, Дизлайки, Скачанное.
- Коллекция → Скачанное: занято на устройстве / свободно в телефоне, «Скачивать новые лайки», «Скачать все лайки», очередь загрузок с пометкой «по Wi-Fi».
- Профиль: смена пароля (лист), язык, тема, вид волны, вид плеера, качество звука (лист: Wi-Fi / сотовая / загрузки), «только Wi-Fi для загрузок», кэш недавних треков (размер + «Очистить»), проверка обновлений (точка, если есть новая версия), текущий сервер, «Админка — откроется в браузере» (для admin / moderator), выход.
- Тема:
ui_prefs.theme(dark / light / system); цвета берутся черезuseColors()из контекста темы (src/theme.ts), до входа — по настройке телефона. - Качество звука хранится на устройстве (
src/lib/prefs.ts, SecureStore): Wi-Fi (по умолчанию high), сотовая (medium), загрузки (high) → параметрqстрима. - Кэш недавних треков — кэш ExoPlayer (
cacheDir/TrackPlayer, 500 МБ). Пока сервис плеера жив, SimpleCache держит индекс в памяти, поэтому «Очистить» помечает кэш, а удаляется он при следующем холодном запуске доsetupPlayer. - Офлайн-баннер вверху, когда нет связи с сервером.
- Админка открывается в браузере (ссылка в профиле у персонала).
Воспроизведение
packages/coreсодержит общую логику очереди, событий и сохранения позиции; мобильный реализует адаптерAudioEngineповерхreact-native-track-player.- Фоновый сервис плеера (
PlaybackService) обрабатывает кнопки уведомления / гарнитуры и шлёт события, даже когда UI выгружен. - Если трек скачан — играется локальный файл, иначе стрим с качеством по типу сети.
- Встроенный кэш ExoPlayer (
maxCacheSize500 МБ) для недавних стримов. - Аудиофокус: пауза при звонке, приглушение при навигационных подсказках.
Офлайн
- Пользователь скачивает трек, альбом, «все лайки» (опционально — авто-загрузка новых лайков).
- Файлы — во внутреннем хранилище приложения (
documentDirectory/offline/<trackId>.opus), не видны другим приложениям. - SQLite хранит метаданные скачанных треков (название, исполнитель, обложка-файл, длительность, features) — раздел «Скачанное» и плеер работают полностью без сети.
- Очередь загрузок: не более 2 параллельно, возобновление после обрыва, «только по Wi-Fi» по умолчанию.
- Без сети:
- доступны «Скачанное» и офлайн-волна (локальный упрощённый алгоритм: перемешивание скачанных с учётом лайков и энергии под выбранный контекст);
- события пишутся в SQLite-очередь и отправляются при появлении сети (
POST /events, идемпотентно); - реакции (лайк / дизлайк) — оптимистично локально + очередь мутаций.
- Подписанные ссылки не кэшируются — при восстановлении сети запрашиваются заново.
Реализация (src/offline/):
downloads.ts— очередь загрузок: таблицаdownloads(DTO трека JSON + статус), файлы вdocumentDirectory/offline/<owner>/, обложка 300 px рядом;owner— хэш «сервер + пользователь». Перед загрузкой берётся свежий DTO (POST /tracks/batch), т.к. подписанная ссылка живёт 12 ч.events.ts— sink дляEventBuffer: при сетевой ошибке пачка уходит вpending_eventsи досылается при появлении сети. Реакции так же: без сети — вpending_reactions(по треку хранится последняя), UI и DTO скачанного обновляются сразу; ответ сервера 4xx откатывает реакцию в UI, при досылке — отбрасывает отложенную.- Запуск без сети:
AuthSession.restore()берёт последнего пользователя изRefreshStore.getUser(состояниеsigned_inсoffline: true); access-токен получается перед первым запросом, когда сеть вернётся. - Офлайн-волна: если
POST /wave/sessionsне удался — перемешанные скачанные треки без дизлайков, лайки ближе к началу.
Самообновление
Первая установка — публичная страница /android веб-клиента: последний APK, адрес сервера и инструкция.
Приложение распространяется только через Gitea Releases (не Google Play).
Поток:
- При запуске (не чаще раза в 6 ч) и вручную в профиле:
GET /api/v1/app/android/latestна сервере Pipu. Сервер сам опрашивает Gitea API (GET /api/v1/repos/{owner}/{repo}/releases/latest) с серверным токеном, кэширует ответ на 10 мин и APK на диске. Так:- мобильному приложению не нужен доступ к Gitea и токен (репозиторий может быть приватным);
- версия приложения согласована с версией сервера (
min_supportedизserver-info).
- Если
version_codeновее — диалог с changelog (Markdown из релиза), кнопки «Обновить» / «Позже». Если текущая версия нижеmin_supported— обновление обязательно. - Загрузка APK через
expo-file-systemвcacheDirectoryс прогрессом; проверка SHA-256 (значение из релиза). Хэш считается кусками по 1 МБ (js-sha256), APK целиком в память JS не читается. Загрузка идёт сBearer, сервер отдаёт файл с поддержкойRangeи сам сверяет SHA-256 при кэшировании из Gitea. - Установка:
Intent.ACTION_VIEWсcontent://URI (FileSystem.getContentUriAsync— FileProvider expo-file-system) и флагомFLAG_GRANT_READ_URI_PERMISSIONчерезexpo-intent-launcher(src/lib/update.ts). - Если нет разрешения «установка из неизвестных источников» — открываем
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCESдля нашего пакета с пояснением.
Требования:
- В манифесте:
REQUEST_INSTALL_PACKAGES(FileProvider добавляет expo-file-system). - Сервер:
GITEA_URL,GITEA_REPO,GITEA_TOKEN, кэш APK —UPDATES_DIR(/updates). Без них —404 app.update_unavailable, приложение молча пропускает проверку. - Все версии подписываются одним и тем же ключом — иначе Android откажет в обновлении.
versionCodeмонотонно растёт:major·1 000 000 + minor·10 000 + patch·100 + (99 для выпуска, N для -rc.N).- Ассеты релиза:
pipu-music-<version>.apk,pipu-music-<version>.apk.sha256.
Безопасность
- Refresh-токен —
expo-secure-store(Android Keystore). - HTTPS обязателен. Самоподписанный сертификат принимается, если при подключении указан его отпечаток или на устройство установлен пользовательский CA.
android:allowBackup="false"для данных с токенами.