修改优化播放器界面
This commit is contained in:
1 parent
129a2fb028
commit
156a6fe067
9 files changed
+327
-97
No files matched your search
@@ -12,12 +12,14 @@ import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -40,6 +42,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -49,6 +52,11 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -59,6 +67,9 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
@@ -70,18 +81,11 @@ import androidx.media3.common.TrackSelectionOverride
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.BatteryManager
|
||||
import android.os.SystemClock
|
||||
import androidx.compose.foundation.Canvas
|
||||
import android.os.BatteryManager
|
||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.gestures.awaitLongPressOrCancellation
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.media3.exoplayer.DefaultLoadControl
|
||||
import androidx.media3.exoplayer.analytics.AnalyticsListener
|
||||
@@ -118,6 +122,24 @@ private fun formatNetworkSpeed(bps: Long): String {
|
||||
else -> "$bps B/s"
|
||||
}
|
||||
}
|
||||
private fun formatVideoCodec(mimeType: String?): String = when (mimeType) {
|
||||
"video/avc" -> "H.264"
|
||||
"video/hevc" -> "H.265"
|
||||
"video/av01" -> "AV1"
|
||||
"video/x-vnd.on2.vp9" -> "VP9"
|
||||
"video/x-vnd.on2.vp8" -> "VP8"
|
||||
"video/mp4v-es" -> "MPEG-4"
|
||||
"video/mpeg2" -> "MPEG-2"
|
||||
"video/3gpp" -> "H.263"
|
||||
"video/x-ms-wmv" -> "WMV"
|
||||
else -> mimeType?.substringAfterLast('/') ?: ""
|
||||
}
|
||||
|
||||
private fun formatBitrate(bps: Int): String = when {
|
||||
bps >= 1_000_000 -> String.format("%.1f Mbps", bps / 1_000_000f)
|
||||
bps >= 1_000 -> String.format("%.0f Kbps", bps / 1_000f)
|
||||
else -> "$bps bps"
|
||||
}
|
||||
|
||||
private class SpeedMonitor {
|
||||
private var lastTotalBytes = 0L
|
||||
@@ -216,14 +238,18 @@ fun VideoPlayerScreen(
|
||||
var hasNetworkSample by remember { mutableStateOf(false) }
|
||||
var batteryPercent by remember { mutableIntStateOf(-1) }
|
||||
var isCharging by remember { mutableStateOf(false) }
|
||||
var isWifi by remember { mutableStateOf(false) }
|
||||
var topTimeText by remember { mutableStateOf("") }
|
||||
var isWifi by remember { mutableStateOf(false) }
|
||||
var videoCodecText by remember { mutableStateOf("") }
|
||||
var videoBitrateText by remember { mutableStateOf("") }
|
||||
var videoFrameRateText by remember { mutableStateOf("") }
|
||||
var speedUpVisible by remember { mutableStateOf(false) }
|
||||
var speedUpValue by remember { mutableFloatStateOf(2f) }
|
||||
|
||||
var controlsVisible by remember { mutableStateOf(true) }
|
||||
LaunchedEffect(controlsVisible, isPlaying) {
|
||||
if (controlsVisible && isPlaying) {
|
||||
var controlsLocked by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(controlsVisible, isPlaying, controlsLocked) {
|
||||
if (controlsVisible && isPlaying && !controlsLocked) {
|
||||
delay(5000)
|
||||
controlsVisible = false
|
||||
}
|
||||
@@ -241,18 +267,18 @@ fun VideoPlayerScreen(
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
topTimeText = java.text.SimpleDateFormat("HH:mm", java.util.Locale.getDefault()).format(java.util.Date())
|
||||
delay(1000)
|
||||
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
|
||||
isWifi = cm?.activeNetwork?.let { net ->
|
||||
cm.getNetworkCapabilities(net)?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
|
||||
} ?: false
|
||||
delay(2000)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
|
||||
isWifi = cm?.allNetworks?.any { cap ->
|
||||
cm.getNetworkCapabilities(cap)?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
|
||||
} ?: false
|
||||
delay(2000)
|
||||
topTimeText = java.text.SimpleDateFormat("HH:mm", java.util.Locale.getDefault()).format(java.util.Date())
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,6 +297,18 @@ fun VideoPlayerScreen(
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(player) {
|
||||
while (true) {
|
||||
val vf = player.videoFormat
|
||||
if (vf != null) {
|
||||
videoCodecText = formatVideoCodec(vf.sampleMimeType)
|
||||
videoBitrateText = formatBitrate(vf.bitrate)
|
||||
videoFrameRateText = if (vf.frameRate > 0f) String.format("%.0f fps", vf.frameRate) else ""
|
||||
}
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
val speed = speedMonitor.currentBytesPerSec()
|
||||
@@ -292,8 +330,19 @@ fun VideoPlayerScreen(
|
||||
DisposableEffect(Unit) {
|
||||
val activity = context.findActivity()
|
||||
val prev = activity?.requestedOrientation ?: ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
val window = activity?.window
|
||||
val insetsController = window?.let { WindowCompat.getInsetsController(it, it.decorView) }
|
||||
val prevBehavior = insetsController?.systemBarsBehavior
|
||||
insetsController?.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
insetsController?.hide(WindowInsetsCompat.Type.systemBars())
|
||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
onDispose { activity?.requestedOrientation = prev }
|
||||
onDispose {
|
||||
insetsController?.let { controller ->
|
||||
controller.show(WindowInsetsCompat.Type.systemBars())
|
||||
controller.systemBarsBehavior = prevBehavior ?: WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
|
||||
}
|
||||
activity?.requestedOrientation = prev
|
||||
}
|
||||
}
|
||||
DisposableEffect(playbackUrl) {
|
||||
playerError = null
|
||||
@@ -367,7 +416,7 @@ fun VideoPlayerScreen(
|
||||
.background(Color.Black)
|
||||
.pointerInput(player) {
|
||||
awaitEachGesture {
|
||||
val down = awaitFirstDown(requireUnconsumed = false)
|
||||
val down = awaitFirstDown(requireUnconsumed = true)
|
||||
val longPress = awaitLongPressOrCancellation(down.id)
|
||||
if (longPress != null) {
|
||||
// 长按:2倍速播放,并显示倍速胶囊
|
||||
@@ -435,7 +484,8 @@ fun VideoPlayerScreen(
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
BoxWithConstraints(Modifier.fillMaxSize()) {
|
||||
val capsuleInset = maxWidth * 0.075f + 20.dp
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
@@ -459,7 +509,7 @@ fun VideoPlayerScreen(
|
||||
icon = LumaIcon.Replay,
|
||||
contentDescription = stringResource(Res.string.player_seek_backward),
|
||||
active = false,
|
||||
size = 48.dp,
|
||||
size = 32.dp,
|
||||
) {
|
||||
val pos = player.currentPosition - 10_000L
|
||||
player.seekTo(maxOf(0L, pos))
|
||||
@@ -469,7 +519,7 @@ fun VideoPlayerScreen(
|
||||
icon = LumaIcon.Previous,
|
||||
contentDescription = stringResource(Res.string.player_previous_episode),
|
||||
active = false,
|
||||
size = 56.dp,
|
||||
size = 40.dp,
|
||||
) {
|
||||
val idx = episodes.indexOfFirst { it.id == currentEpisodeId }
|
||||
if (idx > 0) onSelectEpisode(episodes[idx - 1])
|
||||
@@ -478,8 +528,8 @@ fun VideoPlayerScreen(
|
||||
icon = if (isPlaying) LumaIcon.Pause else LumaIcon.Play,
|
||||
contentDescription = stringResource(if (isPlaying) Res.string.player_pause else Res.string.player_play),
|
||||
active = true,
|
||||
size = 72.dp,
|
||||
iconSize = 40.dp,
|
||||
size = 48.dp,
|
||||
iconSize = 28.dp,
|
||||
) {
|
||||
if (player.isPlaying) {
|
||||
player.pause()
|
||||
@@ -491,7 +541,7 @@ fun VideoPlayerScreen(
|
||||
icon = LumaIcon.Next,
|
||||
contentDescription = stringResource(Res.string.player_next_episode),
|
||||
active = false,
|
||||
size = 56.dp,
|
||||
size = 40.dp,
|
||||
) {
|
||||
val idx = episodes.indexOfFirst { it.id == currentEpisodeId }
|
||||
if (idx >= 0 && idx < episodes.lastIndex) onSelectEpisode(episodes[idx + 1])
|
||||
@@ -500,24 +550,25 @@ fun VideoPlayerScreen(
|
||||
icon = LumaIcon.Speed,
|
||||
contentDescription = stringResource(Res.string.player_seek_forward),
|
||||
active = false,
|
||||
size = 48.dp,
|
||||
size = 32.dp,
|
||||
) {
|
||||
val pos = player.currentPosition + 10_000L
|
||||
player.seekTo(minOf(pos, player.duration))
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxWidth().statusBarsPadding().padding(16.dp)) {
|
||||
Column(Modifier.fillMaxWidth().statusBarsPadding().padding(top = 16.dp)) {
|
||||
// 第一排:当前时间(左)|网速 + 电量(右)
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
Modifier.fillMaxWidth().padding(horizontal = capsuleInset),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(topTimeText, color = Color.White, fontSize = 13.sp)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
if (playbackUrl != null) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
LumaIconView(LumaIcon.Network, contentDescription = null, tint = Color.White, modifier = Modifier.size(16.dp))
|
||||
LumaIconView(if (isWifi) LumaIcon.Wifi else LumaIcon.Signal, contentDescription = null, tint = Color.White, modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
if (hasNetworkSample) Text(formatNetworkSpeed(networkSpeed), color = Color.White, fontSize = 13.sp)
|
||||
}
|
||||
@@ -531,28 +582,32 @@ fun VideoPlayerScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Spacer(Modifier.height(8.dp))
|
||||
// 第二排:关闭按钮(左)|Emby 艺术字(紧跟关闭按钮右侧)|字幕/剧集按钮(右)
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
Modifier.fillMaxWidth().padding(horizontal = capsuleInset),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(Modifier.weight(1f).padding(end = 12.dp), verticalArrangement = Arrangement.Center) {
|
||||
if (titleLogoUrl.isNotBlank()) {
|
||||
AsyncImage(
|
||||
model = titleLogoUrl,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.height(28.dp).widthIn(max = 220.dp),
|
||||
)
|
||||
} else {
|
||||
Text(title, color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
if (serverName.isNotBlank()) {
|
||||
Text(serverName, color = Color.White.copy(alpha = 0.8f), fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
LiquidSurfaceButton(
|
||||
icon = LumaIcon.Close,
|
||||
contentDescription = stringResource(Res.string.player_close),
|
||||
active = false,
|
||||
size = 40.dp,
|
||||
) { onClose() }
|
||||
if (titleLogoUrl.isNotBlank()) {
|
||||
AsyncImage(
|
||||
model = titleLogoUrl,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.height(28.dp).widthIn(max = 220.dp),
|
||||
)
|
||||
} else {
|
||||
Text(title, color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
LiquidSurfaceButton(
|
||||
icon = LumaIcon.Subtitle,
|
||||
contentDescription = stringResource(Res.string.player_subtitle),
|
||||
@@ -571,6 +626,26 @@ fun VideoPlayerScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (serverName.isNotBlank()) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
// 第三排:服务器图标 + 服务器名字
|
||||
Row(Modifier.fillMaxWidth().padding(start = capsuleInset + 52.dp, end = capsuleInset), verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(Modifier.size(30.dp).clip(CircleShape), contentAlignment = Alignment.Center) {
|
||||
if (serverIconUrl.isNotBlank()) {
|
||||
AsyncImage(
|
||||
model = serverIconUrl,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
} else {
|
||||
LumaIconView(LumaIcon.Server, contentDescription = null, tint = Color.White, modifier = Modifier.size(24.dp))
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(serverName, color = Color.White.copy(alpha = 0.8f), fontSize = 15.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (playbackUrl != null) {
|
||||
@@ -578,56 +653,73 @@ fun VideoPlayerScreen(
|
||||
Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth(0.85f)
|
||||
.padding(bottom = 32.dp)
|
||||
.lumaGlass(
|
||||
backdrop = null,
|
||||
radius = 24.dp,
|
||||
level = LumaGlassLevel.Strong,
|
||||
tint = Color.Black.copy(alpha = 0.40f),
|
||||
borderColor = Color.White.copy(alpha = 0.16f),
|
||||
shadowElevation = 4.dp,
|
||||
)
|
||||
.padding(horizontal = 20.dp, vertical = 12.dp),
|
||||
.padding(bottom = 32.dp),
|
||||
) {
|
||||
var totalDurationMs by remember { mutableLongStateOf(0L) }
|
||||
var currentPositionMs by remember { mutableLongStateOf(0L) }
|
||||
var isSeeking by remember { mutableStateOf(false) }
|
||||
var seekPosition by remember { mutableFloatStateOf(0f) }
|
||||
var bufferProgress by remember { mutableFloatStateOf(0f) }
|
||||
|
||||
LaunchedEffect(player) {
|
||||
while (true) {
|
||||
if (player.duration > 0) {
|
||||
totalDurationMs = player.duration
|
||||
if (!isSeeking) {
|
||||
currentPositionMs = player.currentPosition
|
||||
progress = player.currentPosition.toFloat() / player.duration.toFloat()
|
||||
val b = (player.bufferedPosition.toFloat() / player.duration).coerceIn(0f, 1f)
|
||||
bufferProgress = maxOf(b, progress)
|
||||
}
|
||||
}
|
||||
delay(250)
|
||||
}
|
||||
// 技术信息行(进度胶囊上方、胶囊外部):内核|格式|码率|帧率
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Text("ExoPlayer", color = Color.White.copy(alpha = 0.75f), fontSize = 12.sp)
|
||||
if (videoCodecText.isNotBlank()) Text(videoCodecText, color = Color.White.copy(alpha = 0.75f), fontSize = 12.sp)
|
||||
if (videoBitrateText.isNotBlank()) Text(videoBitrateText, color = Color.White.copy(alpha = 0.75f), fontSize = 12.sp)
|
||||
if (videoFrameRateText.isNotBlank()) Text(videoFrameRateText, color = Color.White.copy(alpha = 0.75f), fontSize = 12.sp)
|
||||
}
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.lumaGlass(
|
||||
backdrop = null,
|
||||
radius = 24.dp,
|
||||
level = LumaGlassLevel.Strong,
|
||||
tint = Color.Black.copy(alpha = 0.40f),
|
||||
borderColor = Color.White.copy(alpha = 0.22f),
|
||||
shadowElevation = 4.dp,
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 6.dp),
|
||||
) {
|
||||
var totalDurationMs by remember { mutableLongStateOf(0L) }
|
||||
var currentPositionMs by remember { mutableLongStateOf(0L) }
|
||||
var isSeeking by remember { mutableStateOf(false) }
|
||||
var seekPosition by remember { mutableFloatStateOf(0f) }
|
||||
var bufferProgress by remember { mutableFloatStateOf(0f) }
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(formatMs(currentPositionMs), color = Color.White, fontSize = 12.sp)
|
||||
LumaSeekBar(
|
||||
value = if (isSeeking) seekPosition else progress,
|
||||
onValueChange = { v -> isSeeking = true; seekPosition = v },
|
||||
onValueChangeFinished = {
|
||||
player.seekTo((seekPosition * player.duration).toLong())
|
||||
isSeeking = false
|
||||
latestOnPlaybackProgress(player.currentPosition, !player.isPlaying, "TimeUpdate")
|
||||
},
|
||||
modifier = Modifier.weight(1f).padding(horizontal = 12.dp),
|
||||
activeColor = Color.White,
|
||||
trackColor = Color.White.copy(alpha = 0.12f),
|
||||
bufferColor = Color.White.copy(alpha = 0.28f),
|
||||
bufferedValue = bufferProgress,
|
||||
thumbColor = Color.White,
|
||||
)
|
||||
Text(formatMs(totalDurationMs), color = Color.White, fontSize = 12.sp)
|
||||
LaunchedEffect(player) {
|
||||
while (true) {
|
||||
if (player.duration > 0) {
|
||||
totalDurationMs = player.duration
|
||||
if (!isSeeking) {
|
||||
currentPositionMs = player.currentPosition
|
||||
progress = player.currentPosition.toFloat() / player.duration.toFloat()
|
||||
val b = (player.bufferedPosition.toFloat() / player.duration).coerceIn(0f, 1f)
|
||||
bufferProgress = maxOf(b, progress)
|
||||
}
|
||||
}
|
||||
delay(250)
|
||||
}
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(formatMs(currentPositionMs), color = Color.White, fontSize = 12.sp)
|
||||
LumaSeekBar(
|
||||
value = if (isSeeking) seekPosition else progress,
|
||||
onValueChange = { v -> isSeeking = true; seekPosition = v; controlsLocked = true },
|
||||
onValueChangeFinished = {
|
||||
player.seekTo((seekPosition * player.duration).toLong())
|
||||
isSeeking = false
|
||||
controlsLocked = false
|
||||
latestOnPlaybackProgress(player.currentPosition, !player.isPlaying, "TimeUpdate")
|
||||
},
|
||||
modifier = Modifier.weight(1f).padding(horizontal = 12.dp),
|
||||
activeColor = Color.White,
|
||||
trackColor = Color.White.copy(alpha = 0.12f),
|
||||
bufferColor = Color.White.copy(alpha = 0.45f),
|
||||
bufferedValue = bufferProgress,
|
||||
thumbColor = Color.White,
|
||||
)
|
||||
Text(formatMs(totalDurationMs), color = Color.White, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -750,6 +842,7 @@ private fun LiquidGlassPanel(
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.padding(top = 16.dp, bottom = 16.dp, end = 16.dp)
|
||||
.fillMaxHeight()
|
||||
.width(320.dp)
|
||||
.lumaGlass(
|
||||
|
||||
@@ -176,7 +176,7 @@ fun LumaSeekBar(
|
||||
|
||||
Canvas(
|
||||
modifier
|
||||
.heightIn(min = 48.dp)
|
||||
.heightIn(min = 30.dp)
|
||||
.onSizeChanged { trackWidthPx = it.width.toFloat() }
|
||||
.semantics {
|
||||
progressBarRangeInfo = ProgressBarRangeInfo(currentValue, 0f..1f)
|
||||
@@ -189,6 +189,7 @@ fun LumaSeekBar(
|
||||
.pointerInput(trackWidthPx) {
|
||||
awaitEachGesture {
|
||||
val down = awaitFirstDown(requireUnconsumed = false)
|
||||
down.consume()
|
||||
dragging = true
|
||||
updateFromPosition(down.position.x)
|
||||
var lastX = down.position.x
|
||||
|
||||
@@ -54,6 +54,8 @@ enum class LumaIcon(
|
||||
Database(R.drawable.aurora_database, R.drawable.aurora_database_selected),
|
||||
Cloud(R.drawable.aurora_cloud, R.drawable.aurora_cloud_selected),
|
||||
Network(R.drawable.aurora_network, R.drawable.aurora_network_selected),
|
||||
Wifi(R.drawable.aurora_wifi, R.drawable.aurora_wifi_selected),
|
||||
Signal(R.drawable.aurora_signal, R.drawable.aurora_signal_selected),
|
||||
Globe(R.drawable.aurora_globe, R.drawable.aurora_globe_selected),
|
||||
Shield(R.drawable.aurora_shield, R.drawable.aurora_shield_selected),
|
||||
Key(R.drawable.aurora_key, R.drawable.aurora_key_selected),
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="100"
|
||||
android:viewportHeight="100">
|
||||
<path
|
||||
android:pathData="M22 62 V80"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#1C1C1E"
|
||||
android:strokeWidth="6.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:pathData="M40 48 V80"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#1C1C1E"
|
||||
android:strokeWidth="6.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:pathData="M58 34 V80"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#1C1C1E"
|
||||
android:strokeWidth="6.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:pathData="M76 20 V80"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#1C1C1E"
|
||||
android:strokeWidth="6.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
</vector>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="100"
|
||||
android:viewportHeight="100">
|
||||
<path
|
||||
android:pathData="M22 62 V80"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#0A84FF"
|
||||
android:strokeWidth="6.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:pathData="M40 48 V80"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#0A84FF"
|
||||
android:strokeWidth="6.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:pathData="M58 34 V80"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#0A84FF"
|
||||
android:strokeWidth="6.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:pathData="M76 20 V80"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#0A84FF"
|
||||
android:strokeWidth="6.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
</vector>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="100"
|
||||
android:viewportHeight="100">
|
||||
<path
|
||||
android:pathData="M14 36 Q50 4 86 36"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#1C1C1E"
|
||||
android:strokeWidth="6.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:pathData="M29 51 Q50 27 71 51"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#1C1C1E"
|
||||
android:strokeWidth="6.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:pathData="M43 63 Q50 52 57 63"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#1C1C1E"
|
||||
android:strokeWidth="6.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:pathData="M45 78 a5 5 0 1 0 10 0 a5 5 0 1 0 -10 0 Z"
|
||||
android:fillColor="#1C1C1E" />
|
||||
</vector>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="100"
|
||||
android:viewportHeight="100">
|
||||
<path
|
||||
android:pathData="M14 36 Q50 4 86 36"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#0A84FF"
|
||||
android:strokeWidth="6.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:pathData="M29 51 Q50 27 71 51"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#0A84FF"
|
||||
android:strokeWidth="6.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:pathData="M43 63 Q50 52 57 63"
|
||||
android:fillColor="@android:color/transparent"
|
||||
android:strokeColor="#0A84FF"
|
||||
android:strokeWidth="6.0"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:pathData="M45 78 a5 5 0 1 0 10 0 a5 5 0 1 0 -10 0 Z"
|
||||
android:fillColor="#0A84FF" />
|
||||
</vector>
|
||||
@@ -137,6 +137,7 @@
|
||||
<string name="player_failed">Playback failed</string>
|
||||
<string name="player_subtitle">Subtitles</string>
|
||||
<string name="player_episode_list">Episode list</string>
|
||||
<string name="player_close">Close</string>
|
||||
<string name="player_seek_backward">Seek back 10 seconds</string>
|
||||
<string name="player_previous_episode">Previous episode</string>
|
||||
<string name="player_play">Play</string>
|
||||
|
||||
@@ -137,6 +137,7 @@
|
||||
<string name="player_failed">播放失败</string>
|
||||
<string name="player_subtitle">字幕</string>
|
||||
<string name="player_episode_list">剧集列表</string>
|
||||
<string name="player_close">关闭</string>
|
||||
<string name="player_seek_backward">后退 10 秒</string>
|
||||
<string name="player_previous_episode">上一集</string>
|
||||
<string name="player_play">播放</string>
|
||||
|
||||
Reference in new issue
Block a user