播放器优化: 菜单状态合并、连播偏好持久化、间距统一

- 新增PlayerMenu枚举,替代5个独立boolean状态(showSubtitleMenu等)
- 菜单切换逻辑统一为activeMenu.toggle模式
- BackHandler简化为activeMenu != PlayerMenu.None
- 菜单onDismiss统一关闭activeMenu
- confirmBeforeContinuousPlayback持久化到SharedPreferences
- 顶部间距16.dp→LumaSpacing.md
- 清理sed替换产生的多余空行

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
beyondandClaude Opus 5 committed 2026-09-06 18:02:00 +08:00
1 parent ebd6963bf5
commit 34e2ef6bd7
1 file changed
+50 -62
@@ -114,6 +114,7 @@ import com.example.myapplication.ui.design.LumaGlassLevel
import com.example.myapplication.ui.design.lumaGlass
import com.example.myapplication.ui.design.LumaShapes
import com.example.myapplication.ui.design.LumaColors
import com.example.myapplication.ui.design.LumaSpacing
import com.example.myapplication.ui.LocalGlassBackdrop
import kotlinx.coroutines.delay
import kotlinx.coroutines.CoroutineScope
@@ -250,6 +251,11 @@ private enum class PlayerDoubleTapFeedback {
SeekForward,
}
/** Consolidated menu state — replaces 5 separate boolean flags. */
private enum class PlayerMenu {
None, Subtitle, Episode, Audio, PlaybackTools, NextEpisodeConfirmation,
}
private tailrec fun Context.findActivity(): Activity? = when (this) {
is Activity -> this
is ContextWrapper -> baseContext.findActivity()
@@ -281,12 +287,12 @@ fun VideoPlayerScreen(
val configuration = LocalConfiguration.current
val cannotPlayMessage = stringResource(Res.string.player_cannot_play)
val lifecycleOwner = LocalLifecycleOwner.current
var showSubtitleMenu by remember { mutableStateOf(false) }
var showEpisodeMenu by remember { mutableStateOf(false) }
var showAudioMenu by remember { mutableStateOf(false) }
var showPlaybackToolsMenu by remember { mutableStateOf(false) }
var activeMenu by remember { mutableStateOf(PlayerMenu.None) }
var showNextEpisodeConfirmation by remember { mutableStateOf(false) }
var confirmBeforeContinuousPlayback by remember { mutableStateOf(true) }
var confirmBeforeContinuousPlayback by remember {
val prefs = context.getSharedPreferences("player_settings", Context.MODE_PRIVATE)
mutableStateOf(prefs.getBoolean("confirm_before_continuous", true))
}
var audioDisabledByUser by remember { mutableStateOf(false) }
val speedMonitor = remember { SpeedMonitor() }
val transferListener = remember {
@@ -366,14 +372,10 @@ fun VideoPlayerScreen(
controlsVisible,
isPlaying,
controlsInteractionLocked,
showSubtitleMenu,
showEpisodeMenu,
showAudioMenu,
showPlaybackToolsMenu,
activeMenu,
showNextEpisodeConfirmation,
) {
val menuVisible = showSubtitleMenu || showEpisodeMenu || showAudioMenu ||
showPlaybackToolsMenu || showNextEpisodeConfirmation
val menuVisible = activeMenu != PlayerMenu.None || showNextEpisodeConfirmation
if (controlsVisible && isPlaying && !controlsInteractionLocked && !menuVisible) {
delay(5000)
controlsVisible = false
@@ -496,10 +498,10 @@ fun VideoPlayerScreen(
val nextEpisode = latestEpisodes.getOrNull(currentIndex + 1) ?: return
if (latestConfirmBeforeContinuousPlayback) {
controlsVisible = true
showSubtitleMenu = false
showEpisodeMenu = false
showAudioMenu = false
showPlaybackToolsMenu = false
showNextEpisodeConfirmation = true
} else {
latestOnSelectEpisode(nextEpisode)
@@ -543,16 +545,13 @@ fun VideoPlayerScreen(
}
BackHandler(enabled = true) {
when {
showSubtitleMenu || showEpisodeMenu || showAudioMenu || showPlaybackToolsMenu || showNextEpisodeConfirmation -> {
showSubtitleMenu = false
showEpisodeMenu = false
showAudioMenu = false
showPlaybackToolsMenu = false
activeMenu != PlayerMenu.None || showNextEpisodeConfirmation -> {
activeMenu = PlayerMenu.None
showNextEpisodeConfirmation = false
}
isScreenLocked -> {
isScreenLocked = false
controlsVisible = true
isScreenLocked = false
controlsVisible = true
}
else -> onClose()
}
@@ -849,10 +848,7 @@ fun VideoPlayerScreen(
) {
isScreenLocked = !isScreenLocked
controlsVisible = true
showSubtitleMenu = false
showEpisodeMenu = false
showAudioMenu = false
showPlaybackToolsMenu = false
activeMenu = PlayerMenu.None
}
if (!isScreenLocked) {
@@ -910,7 +906,7 @@ fun VideoPlayerScreen(
}
}
Column(Modifier.fillMaxWidth().statusBarsPadding().padding(top = 16.dp)) {
Column(Modifier.fillMaxWidth().statusBarsPadding().padding(top = LumaSpacing.md)) {
// P0: Delegated to extracted composable — only this component polls and recomposes
PlayerStatusBar(
playbackUrl = playbackUrl,
@@ -948,46 +944,34 @@ fun VideoPlayerScreen(
LiquidSurfaceButton(
icon = LumaIcon.Subtitle,
contentDescription = stringResource(Res.string.player_subtitle),
active = showSubtitleMenu,
active = activeMenu == PlayerMenu.Subtitle,
size = 40.dp,
) {
showSubtitleMenu = !showSubtitleMenu
showEpisodeMenu = false
showAudioMenu = false
showPlaybackToolsMenu = false
activeMenu = if (activeMenu == PlayerMenu.Subtitle) PlayerMenu.None else PlayerMenu.Subtitle
}
LiquidSurfaceButton(
icon = LumaIcon.Watch,
contentDescription = stringResource(Res.string.player_episode_list),
active = showEpisodeMenu,
active = activeMenu == PlayerMenu.Episode,
size = 40.dp,
) {
showEpisodeMenu = !showEpisodeMenu
showSubtitleMenu = false
showAudioMenu = false
showPlaybackToolsMenu = false
activeMenu = if (activeMenu == PlayerMenu.Episode) PlayerMenu.None else PlayerMenu.Episode
}
LiquidSurfaceButton(
icon = LumaIcon.Headphones,
contentDescription = stringResource(Res.string.player_audio_tracks),
active = showAudioMenu,
active = activeMenu == PlayerMenu.Audio,
size = 40.dp,
) {
showAudioMenu = !showAudioMenu
showSubtitleMenu = false
showEpisodeMenu = false
showPlaybackToolsMenu = false
activeMenu = if (activeMenu == PlayerMenu.Audio) PlayerMenu.None else PlayerMenu.Audio
}
LiquidSurfaceButton(
icon = LumaIcon.More,
contentDescription = stringResource(Res.string.player_playback_tools),
active = showPlaybackToolsMenu,
active = activeMenu == PlayerMenu.PlaybackTools,
size = 40.dp,
) {
showPlaybackToolsMenu = !showPlaybackToolsMenu
showSubtitleMenu = false
showEpisodeMenu = false
showAudioMenu = false
activeMenu = if (activeMenu == PlayerMenu.PlaybackTools) PlayerMenu.None else PlayerMenu.PlaybackTools
}
}
}
@@ -1155,25 +1139,25 @@ fun VideoPlayerScreen(
)
}
if (showSubtitleMenu) {
if (activeMenu == PlayerMenu.Subtitle) {
LiquidGlassPanel(
title = stringResource(Res.string.player_subtitle),
onDismiss = { showSubtitleMenu = false },
onDismiss = { activeMenu = PlayerMenu.None },
) {
SubtitleMenuContent(
player = player,
onTrackChanged = { eventName ->
latestOnPlaybackProgress(player.currentPosition, !player.isPlaying, eventName, true)
showSubtitleMenu = false
},
)
}
}
if (showEpisodeMenu) {
if (activeMenu == PlayerMenu.Episode) {
LiquidGlassPanel(
title = title.ifBlank { stringResource(Res.string.player_episode_list) },
titleLogoUrl = titleLogoUrl,
onDismiss = { showEpisodeMenu = false },
onDismiss = { activeMenu = PlayerMenu.None },
) {
if (episodes.isEmpty()) {
Text(
@@ -1189,52 +1173,56 @@ fun VideoPlayerScreen(
episode = ep,
serverUrl = serverUrl,
selected = ep.id == currentEpisodeId,
) { showEpisodeMenu = false; onSelectEpisode(ep) }
) { ; onSelectEpisode(ep) }
}
}
}
}
}
if (showAudioMenu) {
if (activeMenu == PlayerMenu.Audio) {
LiquidGlassPanel(
title = stringResource(Res.string.player_audio_tracks),
onDismiss = { showAudioMenu = false },
onDismiss = { activeMenu = PlayerMenu.None },
) {
AudioMenuContent(
player = player,
onAudioDisabledChange = { audioDisabledByUser = it },
onTrackChanged = { eventName ->
latestOnPlaybackProgress(player.currentPosition, !player.isPlaying, eventName, true)
showAudioMenu = false
},
)
}
}
if (showPlaybackToolsMenu) {
if (activeMenu == PlayerMenu.PlaybackTools) {
LiquidGlassPanel(
title = stringResource(Res.string.player_playback_tools),
onDismiss = { showPlaybackToolsMenu = false },
onDismiss = { activeMenu = PlayerMenu.None },
) {
PlaybackToolsMenuContent(
chapters = chapters,
confirmBeforeContinuousPlayback = confirmBeforeContinuousPlayback,
onConfirmBeforeContinuousPlaybackChange = { confirmBeforeContinuousPlayback = it },
onConfirmBeforeContinuousPlaybackChange = { newValue ->
confirmBeforeContinuousPlayback = newValue
context.getSharedPreferences("player_settings", Context.MODE_PRIVATE)
.edit().putBoolean("confirm_before_continuous", newValue).apply()
},
onSeek = { targetPositionMs ->
player.seekTo(targetPositionMs)
latestOnPlaybackProgress(targetPositionMs, !player.isPlaying, "TimeUpdate", true)
showPlaybackToolsMenu = false
},
onSkipOutro = {
val currentIndex = episodes.indexOfFirst { it.id == currentEpisodeId }
val nextEpisode = episodes.getOrNull(currentIndex + 1)
if (nextEpisode != null) {
showPlaybackToolsMenu = false
onSelectEpisode(nextEpisode)
} else {
val targetPositionMs = (player.duration - 500L).coerceAtLeast(0L)
player.seekTo(targetPositionMs)
latestOnPlaybackProgress(targetPositionMs, !player.isPlaying, "TimeUpdate", true)
showPlaybackToolsMenu = false
}
},
)