feat: add fullscreen lock

This commit is contained in:
tunbmoon
2026-03-02 14:07:50 +08:00
parent d90c9bc870
commit 29bcdda51c
5 changed files with 152 additions and 95 deletions
@@ -275,6 +275,7 @@ fun ChannelItem(
Card(
modifier = Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = 96.dp)
.clickable(onClick = onClick),
colors = CardDefaults.cardColors(
containerColor = if (isPlaying)
@@ -57,7 +57,9 @@ fun PlayerControlsOverlay(
onPlayPauseClick: () -> Unit,
onBackClick: () -> Unit,
onFullscreenClick: () -> Unit,
onSettingsClick: () -> Unit
onSettingsClick: () -> Unit,
isLocked: Boolean,
onLockChange: (Boolean) -> Unit
) {
var currentPosition by remember { mutableLongStateOf(media3Player?.currentPosition ?: 0L) }
var bufferedPosition by remember { mutableLongStateOf(media3Player?.bufferedPosition ?: 0L) }
@@ -111,112 +113,146 @@ fun PlayerControlsOverlay(
)
)
IconButton(
onClick = onSettingsClick,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(8.dp)
) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = stringResource(R.string.player_settings),
tint = Color.White,
modifier = Modifier.size(26.dp)
)
if (isFullscreen) {
IconButton(
onClick = { onLockChange(!isLocked) },
modifier = Modifier
.align(Alignment.TopEnd)
.padding(8.dp)
) {
Icon(
imageVector = if (isLocked) Icons.Default.Lock else Icons.Default.LockOpen,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(26.dp)
)
}
}
IconButton(
onClick = onBackClick,
modifier = Modifier
.align(Alignment.TopStart)
.padding(8.dp)
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back),
tint = Color.White,
modifier = Modifier.size(28.dp)
)
}
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.background(
Brush.verticalGradient(
colors = listOf(
Color.Transparent,
Color.Black.copy(alpha = 0.7f)
)
if (!isLocked) {
if (isFullscreen) {
IconButton(
onClick = onSettingsClick,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 8.dp, end = 56.dp)
) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = stringResource(R.string.player_settings),
tint = Color.White,
modifier = Modifier.size(26.dp)
)
)
) {
Column(
}
} else {
IconButton(
onClick = onSettingsClick,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(8.dp)
) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = stringResource(R.string.player_settings),
tint = Color.White,
modifier = Modifier.size(26.dp)
)
}
}
IconButton(
onClick = onBackClick,
modifier = Modifier
.align(Alignment.TopStart)
.padding(8.dp)
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back),
tint = Color.White,
modifier = Modifier.size(28.dp)
)
}
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 8.dp)
){
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
IconButton(onClick = onPlayPauseClick) {
Icon(
imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
contentDescription = if (isPlaying) stringResource(R.string.paused) else stringResource(
R.string.play
),
tint = Color.White,
modifier = Modifier.size(28.dp)
.background(
Brush.verticalGradient(
colors = listOf(
Color.Transparent,
Color.Black.copy(alpha = 0.7f)
)
)
}
Spacer(modifier = Modifier.width(8.dp))
ProgressBar(
modifier = Modifier.weight(1f),
isBuffering = isBuffering,
estimatedProgress = estimatedProgress
)
Spacer(modifier = Modifier.width(8.dp))
if (enablePip && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
IconButton(
onClick = {
activity?.let { act ->
if (act.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
val aspectRatio = Rational(16, 9)
val builder = PictureInPictureParams.Builder()
.setAspectRatio(aspectRatio)
act.enterPictureInPictureMode(builder.build())
}
}
},
modifier = Modifier.offset(x = 2.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 8.dp)
){
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
IconButton(onClick = onPlayPauseClick) {
Icon(
imageVector = Icons.Default.PictureInPicture,
contentDescription = stringResource(R.string.pip),
imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
contentDescription = if (isPlaying) stringResource(R.string.paused) else stringResource(
R.string.play
),
tint = Color.White,
modifier = Modifier.size(22.dp)
modifier = Modifier.size(28.dp)
)
}
}
IconButton(
onClick = onFullscreenClick,
modifier = Modifier.offset(x = if (enablePip && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) (-2).dp else 0.dp)
) {
Icon(
imageVector = if (isFullscreen) Icons.Default.FullscreenExit else Icons.Default.Fullscreen,
contentDescription = if (isFullscreen) stringResource(R.string.exit_fullscreen) else stringResource(
R.string.fullscreen
),
tint = Color.White,
modifier = Modifier.size(28.dp)
Spacer(modifier = Modifier.width(8.dp))
ProgressBar(
modifier = Modifier.weight(1f),
isBuffering = isBuffering,
estimatedProgress = estimatedProgress
)
Spacer(modifier = Modifier.width(8.dp))
if (enablePip && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
IconButton(
onClick = {
activity?.let { act ->
if (act.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
val aspectRatio = Rational(16, 9)
val builder = PictureInPictureParams.Builder()
.setAspectRatio(aspectRatio)
act.enterPictureInPictureMode(builder.build())
}
}
},
modifier = Modifier.offset(x = 2.dp)
) {
Icon(
imageVector = Icons.Default.PictureInPicture,
contentDescription = stringResource(R.string.pip),
tint = Color.White,
modifier = Modifier.size(22.dp)
)
}
}
IconButton(
onClick = onFullscreenClick,
modifier = Modifier.offset(x = if (enablePip && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) (-2).dp else 0.dp)
) {
Icon(
imageVector = if (isFullscreen) Icons.Default.FullscreenExit else Icons.Default.Fullscreen,
contentDescription = if (isFullscreen) stringResource(R.string.exit_fullscreen) else stringResource(
R.string.fullscreen
),
tint = Color.White,
modifier = Modifier.size(28.dp)
)
}
}
}
}
@@ -421,7 +421,9 @@ private fun PlayerPageContent(
onPlayPauseClick = onPlayPauseClick,
onBackClick = onBackClick,
onFullscreenClick = onFullscreenClick,
onSettingsClick = onSettingsClick
onSettingsClick = onSettingsClick,
isLocked = uiState.isLocked,
onLockChange = { viewModel.setLocked(it) }
)
}
@@ -206,6 +206,14 @@ class OrientationHelper(
isEnable = true
orientationEventListener.enable()
}
fun pause() {
isPause = true
}
fun resume() {
isPause = false
}
fun release() {
orientationEventListener.disable()
@@ -45,7 +45,8 @@ data class PlayerUiState(
val shouldScrollToChannel: Boolean = false,
val isFullscreen: Boolean = false,
val availableQualities: List<VideoQuality> = emptyList(),
val currentQuality: VideoQuality? = null
val currentQuality: VideoQuality? = null,
val isLocked: Boolean = false
)
data class PlayerProgressState(
@@ -266,6 +267,15 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
_uiState.value = _uiState.value.copy(showControls = false)
}
fun setLocked(locked: Boolean) {
_uiState.value = _uiState.value.copy(isLocked = locked)
if (locked) {
orientationHelper?.pause()
} else {
orientationHelper?.resume()
}
}
fun switchChannel(channel: M3U8Channel) {
if (_uiState.value.currentChannelUrl == channel.url) {
return