feat(app-phone): полноценный UI телефона — каталог Jellyfin, детали, пульт очков, реальное скачивание с S3

- 4 экрана по макету sketches/phone-ui: Каталог (табы Фильмы/Сериалы/Папки, поиск, бейджи зеркала), Детали (озвучки, статус конвертации, чипы скачивания), Очки (пульт, статус), Чат-заглушка
- S3 SigV4 presign (S3Presigner): mirror-client read-only креды; watch() и DownloadManager подписывают URL (бакет приватный)
- DownloadManager: реальное скачивание video+audio с прогрессом, readTimeout=0 (длинные файлы)
- PhoneActions.watch: параметр presign (app-host работает без изменений)
- TESTING.md: блок телефонных кейсов TP-1..6
This commit is contained in:
Hermes Agent
2026-08-19 09:13:56 +03:00
parent 4458678698
commit 6069400b88
19 changed files with 2145 additions and 68 deletions
@@ -4,6 +4,9 @@ import android.app.Application
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import pw.binom.viewmate.core.media.JellyfinClient
import pw.binom.viewmate.core.media.MirrorClient
@@ -12,15 +15,27 @@ import pw.binom.viewmate.core.phone.PhoneState
/**
* Application телефона: создаёт клиентов (Jellyfin/mirror), PhoneState,
* WS-сервер (GlassesServer) и PhoneActions. Синглтон-доступ через companion.
* WS-сервер (GlassesServer), DownloadManager и PhoneActions. Синглтон-доступ через companion.
*/
class PhoneApp : Application() {
lateinit var state: PhoneState
private set
lateinit var server: GlassesServer
private set
lateinit var actions: PhoneActions
val state: PhoneState = PhoneState()
/** WS-сервер для очков. */
val server: GlassesServer = GlassesServer(port = PhoneConfig.SERVER_PORT, state = state)
/** HTTP-клиенты бэкендов (Jellyfin-каталог, media-mirror). */
val jellyfin: JellyfinClient = JellyfinClient(PhoneConfig.JELLYFIN_URL, PhoneConfig.JELLYFIN_API_KEY)
val mirror: MirrorClient = MirrorClient(PhoneConfig.MIRROR_URL, PhoneConfig.MIRROR_API_KEY)
private val _userId = MutableStateFlow("")
/** Первый пользователь Jellyfin (подтягивается один раз при старте). */
val userId: StateFlow<String> = _userId.asStateFlow()
/** Реальное скачивание зеркал на телефон (с прогрессом). */
val downloadManager: DownloadManager by lazy { DownloadManager(this, mirror) }
var actions: PhoneActions? = null
private set
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@@ -29,18 +44,13 @@ class PhoneApp : Application() {
super.onCreate()
instance = this
state = PhoneState()
server = GlassesServer(port = PhoneConfig.SERVER_PORT, state = state)
val jellyfin = JellyfinClient(PhoneConfig.JELLYFIN_URL, PhoneConfig.JELLYFIN_API_KEY)
val mirror = MirrorClient(PhoneConfig.MIRROR_URL, PhoneConfig.MIRROR_API_KEY)
server.start()
log("server", "WS-сервер запущен на ${PhoneConfig.SERVER_PORT}")
// первый пользователь Jellyfin — один раз при старте
scope.launch {
val userId = runCatching { jellyfin.users().firstOrNull()?.Id }.getOrNull()
?: ""
_userId.value = userId
if (userId.isBlank()) log("app", "не удалось получить пользователя Jellyfin")
actions = PhoneActions(
jellyfin = jellyfin,