feat(app-phone): трекбар-таймлайн в пульте (seek на любую позицию)
- MediaCommand.SEEK (абсолютный seek на seekMs, поле в протоколе уже было) - MovieController (очки): SEEK → player.seekTo(seekMs.clamp(0,duration)); чистая seekTargetMs - GlassesScreen (пульт): material3 Slider под статус-карточкой (виден при durationMs>0), локальный стейт драга — время под слайдером следует за пальцем (sliderValue ?: positionMs), после отпускания цель держится до подтверждения позиции от очков (±2.5с, таймаут 10с), звук: SEEK → audio.seekTo(seekMs) - Тесты 70/0 phone, 37/0 glasses (MovieControllerSeekTest, сериализация SEEK)
This commit is contained in:
@@ -17,6 +17,7 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -34,6 +35,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import pw.binom.viewmate.core.GlassesMode
|
||||
import pw.binom.viewmate.core.MediaCommand
|
||||
@@ -59,9 +61,13 @@ fun GlassesScreen(
|
||||
val playing by app.state.playingFlow.collectAsState()
|
||||
val mode by app.state.modeFlow.collectAsState()
|
||||
val glassesDownloads by app.server.hub.glassesDownloads.collectAsState()
|
||||
val durationMs = currentItem?.RunTimeTicks?.div(10_000)
|
||||
|
||||
var showDownloads by remember { mutableStateOf(false) }
|
||||
|
||||
// трекбар: пока тянем — слайдер следует за пальцем; отпустили — абсолютный SEEK
|
||||
var sliderValue by remember { mutableStateOf<Float?>(null) }
|
||||
|
||||
val actions = app.actions
|
||||
|
||||
// активное скачивание на очки: первое с незавершённым файлом
|
||||
@@ -81,6 +87,7 @@ fun GlassesScreen(
|
||||
MediaCommand.PAUSE -> audio.pause()
|
||||
MediaCommand.SEEK_FORWARD -> audio.seekTo(audio.currentPositionMs() + seekMs)
|
||||
MediaCommand.SEEK_BACKWARD -> audio.seekTo((audio.currentPositionMs() - seekMs).coerceAtLeast(0L))
|
||||
MediaCommand.SEEK -> audio.seekTo(seekMs)
|
||||
MediaCommand.CLOSE -> audio.stop()
|
||||
else -> Unit
|
||||
}
|
||||
@@ -106,11 +113,48 @@ fun GlassesScreen(
|
||||
currentItemName = currentItem?.Name,
|
||||
positionMs = positionMs,
|
||||
audioPositionMs = audioPos,
|
||||
durationMs = currentItem?.RunTimeTicks?.div(10_000),
|
||||
durationMs = durationMs,
|
||||
playing = playing,
|
||||
onClick = onOpenInfo,
|
||||
)
|
||||
|
||||
// трекбар: перетаскивание на любую позицию фильма (абсолютный SEEK)
|
||||
if (durationMs != null && durationMs > 0) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp),
|
||||
) {
|
||||
Slider(
|
||||
value = sliderValue ?: (positionMs ?: 0L).toFloat().coerceIn(0f, durationMs.toFloat()),
|
||||
onValueChange = { sliderValue = it },
|
||||
valueRange = 0f..durationMs.toFloat(),
|
||||
onValueChangeFinished = {
|
||||
sliderValue?.let { sendCommand(MediaCommand.SEEK, it.toLong()) }
|
||||
},
|
||||
)
|
||||
Text(
|
||||
text = "${msToClock((sliderValue ?: positionMs ?: 0L).toLong())} / ${msToClock(durationMs)}",
|
||||
color = PhoneMuted,
|
||||
fontSize = 11.sp,
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
)
|
||||
|
||||
// очки прислали позицию ≈ цели — seek подтверждён, ползунок снова «живой»
|
||||
LaunchedEffect(positionMs) {
|
||||
val sv = sliderValue ?: return@LaunchedEffect
|
||||
val real = positionMs ?: 0L
|
||||
if (kotlin.math.abs(real - sv) < 2500) sliderValue = null
|
||||
}
|
||||
// страховка от зависания (seek не прошёл/очки отключились) — сброс к реальной позиции через 10с
|
||||
LaunchedEffect(sliderValue) {
|
||||
val sv = sliderValue ?: return@LaunchedEffect
|
||||
delay(10_000)
|
||||
sliderValue = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// активная загрузка контента на очки
|
||||
if (activeGlassesItem != null) {
|
||||
Text(
|
||||
|
||||
Reference in New Issue
Block a user