From e3e8120e8b3de003d7b9e72d895ab66c6a6ff011 Mon Sep 17 00:00:00 2001
From: beyond
Date: Sun, 6 Sep 2026 19:47:26 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=92=AD=E6=94=BE=E5=99=A8?=
=?UTF-8?q?=E7=95=8C=E9=9D=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../screens/VideoPlayerScreen.kt | 68 +++++++++++--------
.../myapplication/ui/design/LumaComponents.kt | 3 +-
.../screens/VideoPlayerTimeFormatTest.kt | 28 ++++++++
3 files changed, 69 insertions(+), 30 deletions(-)
create mode 100644 app/src/test/kotlin/com/example/myapplication/screens/VideoPlayerTimeFormatTest.kt
diff --git a/app/src/main/kotlin/com/example/myapplication/screens/VideoPlayerScreen.kt b/app/src/main/kotlin/com/example/myapplication/screens/VideoPlayerScreen.kt
index d867234..6769c82 100644
--- a/app/src/main/kotlin/com/example/myapplication/screens/VideoPlayerScreen.kt
+++ b/app/src/main/kotlin/com/example/myapplication/screens/VideoPlayerScreen.kt
@@ -64,9 +64,12 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -126,12 +129,12 @@ import top.yukonga.miuix.kmp.basic.Text
import kotlin.math.abs
import kotlin.math.roundToInt
-private fun formatMs(ms: Long): String {
- val totalSec = ms / 1000
+internal fun formatMs(ms: Long): String {
+ val totalSec = ms.coerceAtLeast(0L) / 1000
val h = totalSec / 3600
val m = (totalSec % 3600) / 60
val s = totalSec % 60
- return if (h > 0) String.format("%d:%02d:%02d", h, m, s) else String.format("%02d:%02d", m, s)
+ return String.format(java.util.Locale.ROOT, "%02d:%02d:%02d", h, m, s)
}
private fun formatNetworkSpeed(bps: Long): String {
@@ -338,6 +341,7 @@ fun VideoPlayerScreen(
var stopReported by remember(playbackUrl) { mutableStateOf(false) }
var progress by remember { mutableFloatStateOf(0f) }
+ var isSeeking by remember { mutableStateOf(false) }
var isPlaying by remember { mutableStateOf(false) }
var playerError by remember(playbackUrl) { mutableStateOf(null) }
var networkSpeed by remember { mutableLongStateOf(0L) }
@@ -392,7 +396,7 @@ fun VideoPlayerScreen(
LaunchedEffect(Unit) {
while (true) {
- if (player.duration > 0 && seekPreviewPositionMs == null) {
+ if (player.duration > 0 && !isSeeking && seekPreviewPositionMs == null) {
progress = player.currentPosition.toFloat() / player.duration.toFloat()
}
isPlaying = player.isPlaying
@@ -821,6 +825,8 @@ fun VideoPlayerScreen(
) {
BoxWithConstraints(Modifier.fillMaxSize()) {
val capsuleInset = maxWidth * 0.075f + 20.dp
+ // The 40dp button surface is centered in a 48dp touch target.
+ val buttonInset = maxWidth * 0.075f - 4.dp
Box(
Modifier
.fillMaxSize()
@@ -842,7 +848,7 @@ fun VideoPlayerScreen(
),
active = isScreenLocked,
size = 40.dp,
- modifier = Modifier.align(Alignment.CenterStart).padding(start = capsuleInset),
+ modifier = Modifier.align(Alignment.CenterStart).padding(start = buttonInset),
) {
isScreenLocked = !isScreenLocked
controlsVisible = true
@@ -855,7 +861,7 @@ fun VideoPlayerScreen(
contentDescription = stringResource(Res.string.player_rotate_screen),
active = false,
size = 40.dp,
- modifier = Modifier.align(Alignment.CenterEnd).padding(end = capsuleInset),
+ modifier = Modifier.align(Alignment.CenterEnd).padding(end = buttonInset),
) {
activity?.requestedOrientation = if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
@@ -865,7 +871,7 @@ fun VideoPlayerScreen(
}
Row(
- Modifier.align(Alignment.BottomStart).padding(start = capsuleInset, bottom = 14.dp),
+ Modifier.align(Alignment.BottomStart).padding(start = buttonInset, bottom = 14.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
@@ -923,7 +929,7 @@ fun VideoPlayerScreen(
Spacer(Modifier.height(2.dp))
// 第二排:关闭按钮(左)|Emby 艺术字(紧跟关闭按钮右侧)|字幕/剧集按钮(右)
Row(
- Modifier.fillMaxWidth().padding(horizontal = capsuleInset),
+ Modifier.fillMaxWidth().padding(horizontal = buttonInset),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
@@ -1030,7 +1036,7 @@ fun VideoPlayerScreen(
borderColor = Color.White.copy(alpha = 0.22f),
shadowElevation = 4.dp,
)
- .padding(horizontal = 12.dp, vertical = 0.dp),
+ .padding(horizontal = 8.dp, vertical = 0.dp),
) {
var totalDurationMs by remember(player) {
mutableLongStateOf(player.duration.coerceAtLeast(0L))
@@ -1038,21 +1044,8 @@ fun VideoPlayerScreen(
var currentPositionMs by remember(player) {
mutableLongStateOf(player.currentPosition.coerceAtLeast(0L))
}
- var isSeeking by remember { mutableStateOf(false) }
var seekPosition by remember { mutableFloatStateOf(0f) }
var bufferProgress by remember { mutableFloatStateOf(0f) }
- var seekCompletedVersion by remember { mutableIntStateOf(0) }
-
- // Reset isSeeking after seek completes to prevent jitter
- LaunchedEffect(seekCompletedVersion) {
- if (seekCompletedVersion > 0) {
- delay(300)
- // Sync currentPositionMs to the seek target before resuming polling
- // to prevent a visual jump when player position hasn't caught up yet
- currentPositionMs = (progress * totalDurationMs).toLong()
- isSeeking = false
- }
- }
LaunchedEffect(player) {
while (true) {
@@ -1068,9 +1061,20 @@ fun VideoPlayerScreen(
}
}
- Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 2.dp)) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
val swipePositionMs = seekPreviewPositionMs
- Text(formatMs(swipePositionMs ?: currentPositionMs), color = Color.White, fontSize = 11.sp)
+ // Reserve space independently of the digits: device fonts may ignore monospace.
+ val timeWidth = with(LocalDensity.current) {
+ (formatMs(totalDurationMs).length * 7).sp.toDp()
+ }
+ Text(
+ formatMs(swipePositionMs ?: currentPositionMs),
+ modifier = Modifier.width(timeWidth),
+ color = Color.White,
+ fontSize = 11.sp,
+ fontFamily = FontFamily.Monospace,
+ maxLines = 1,
+ )
LumaSeekBar(
value = when {
isSeeking -> seekPosition
@@ -1088,13 +1092,11 @@ fun VideoPlayerScreen(
player.seekTo(targetPosition)
progress = finalValue
currentPositionMs = targetPosition
+ isSeeking = false
controlsInteractionLocked = false
latestOnPlaybackProgress(targetPosition, !player.isPlaying, "TimeUpdate", true)
- // Keep isSeeking true briefly to prevent polling loop from
- // overwriting currentPositionMs before player catches up
- seekCompletedVersion++
},
- modifier = Modifier.weight(1f).padding(horizontal = 12.dp),
+ modifier = Modifier.weight(1f).height(28.dp),
progressContentDescription = stringResource(Res.string.player_progress),
activeColor = Color.White,
trackColor = Color.White.copy(alpha = 0.12f),
@@ -1102,7 +1104,15 @@ fun VideoPlayerScreen(
bufferedValue = bufferProgress,
thumbColor = Color.White,
)
- Text(formatMs(totalDurationMs), color = Color.White, fontSize = 11.sp)
+ Text(
+ formatMs(totalDurationMs),
+ modifier = Modifier.width(timeWidth),
+ color = Color.White,
+ fontSize = 11.sp,
+ fontFamily = FontFamily.Monospace,
+ textAlign = TextAlign.End,
+ maxLines = 1,
+ )
}
}
}
diff --git a/app/src/main/kotlin/com/example/myapplication/ui/design/LumaComponents.kt b/app/src/main/kotlin/com/example/myapplication/ui/design/LumaComponents.kt
index 440383a..451c003 100644
--- a/app/src/main/kotlin/com/example/myapplication/ui/design/LumaComponents.kt
+++ b/app/src/main/kotlin/com/example/myapplication/ui/design/LumaComponents.kt
@@ -509,6 +509,7 @@ fun LumaSeekBar(
.pointerInput(Unit) {
awaitEachGesture {
val down = awaitFirstDown()
+ down.consume()
val horizontalInset = 7.dp.toPx()
val usableWidth = (size.width - horizontalInset * 2f).coerceAtLeast(1f)
var gestureValue = ((down.position.x - horizontalInset) / usableWidth).coerceIn(0f, 1f)
@@ -521,8 +522,8 @@ fun LumaSeekBar(
if (change.pressed) {
gestureValue = ((change.position.x - horizontalInset) / usableWidth).coerceIn(0f, 1f)
currentOnValueChange(gestureValue)
- change.consume()
}
+ change.consume()
}
dragging = false
currentOnValueChangeFinished(gestureValue)
diff --git a/app/src/test/kotlin/com/example/myapplication/screens/VideoPlayerTimeFormatTest.kt b/app/src/test/kotlin/com/example/myapplication/screens/VideoPlayerTimeFormatTest.kt
new file mode 100644
index 0000000..eeaf580
--- /dev/null
+++ b/app/src/test/kotlin/com/example/myapplication/screens/VideoPlayerTimeFormatTest.kt
@@ -0,0 +1,28 @@
+package com.example.myapplication.screens
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class VideoPlayerTimeFormatTest {
+ @Test
+ fun alwaysDisplaysHoursMinutesAndSeconds() {
+ assertEquals("00:00:00", formatMs(0L))
+ assertEquals("00:00:09", formatMs(9_999L))
+ assertEquals("00:01:00", formatMs(60_000L))
+ assertEquals("00:59:59", formatMs(3_599_999L))
+ assertEquals("01:00:00", formatMs(3_600_000L))
+ assertEquals("12:34:56", formatMs(45_296_000L))
+ }
+
+ @Test
+ fun unknownDurationDisplaysZero() {
+ assertEquals("00:00:00", formatMs(-1L))
+ assertEquals("00:00:00", formatMs(Long.MIN_VALUE + 1L))
+ }
+
+ @Test
+ fun hoursDoNotWrapAfterOneDay() {
+ assertEquals("25:00:00", formatMs(90_000_000L))
+ assertEquals("100:00:00", formatMs(360_000_000L))
+ }
+}