diff --git a/app/src/main/java/hu/bbara/purefin/player/ui/PlayerScreen.kt b/app/src/main/java/hu/bbara/purefin/player/ui/PlayerScreen.kt index 1129b63..a23b3b9 100644 --- a/app/src/main/java/hu/bbara/purefin/player/ui/PlayerScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/player/ui/PlayerScreen.kt @@ -44,6 +44,8 @@ import hu.bbara.purefin.player.ui.components.PlayerControlsOverlay import hu.bbara.purefin.player.ui.components.PlayerGesturesLayer import hu.bbara.purefin.player.ui.components.PlayerLoadingErrorEndCard import hu.bbara.purefin.player.ui.components.PlayerQueuePanel +import hu.bbara.purefin.player.ui.components.PersistentOverlayContainer +import hu.bbara.purefin.player.ui.components.rememberPersistentOverlayController import hu.bbara.purefin.player.viewmodel.PlayerViewModel import kotlin.math.abs import kotlin.math.roundToInt @@ -66,6 +68,7 @@ fun PlayerScreen( var brightness by remember { mutableStateOf(readCurrentBrightness(activity)) } var showQueuePanel by remember { mutableStateOf(false) } var horizontalSeekFeedback by remember { mutableStateOf(null) } + val overlayController = rememberPersistentOverlayController() LaunchedEffect(uiState.isPlaying) { if (uiState.isPlaying) { @@ -175,6 +178,7 @@ fun PlayerScreen( modifier = Modifier.fillMaxSize(), uiState = uiState, showControls = controlsVisible, + overlayController = overlayController, onBack = onBack, onPlayPause = { viewModel.togglePlayPause() }, onSeek = { viewModel.seekTo(it) }, @@ -215,6 +219,11 @@ fun PlayerScreen( .fillMaxSize() ) } + + PersistentOverlayContainer( + controller = overlayController, + modifier = Modifier.fillMaxSize() + ) } } diff --git a/app/src/main/java/hu/bbara/purefin/player/ui/components/PersistentOverlayContainer.kt b/app/src/main/java/hu/bbara/purefin/player/ui/components/PersistentOverlayContainer.kt new file mode 100644 index 0000000..d00c31f --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/player/ui/components/PersistentOverlayContainer.kt @@ -0,0 +1,78 @@ +package hu.bbara.purefin.player.ui.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +/** + * Controller for the persistent overlay that allows components to show content + * independently of the player controls visibility. + */ +class PersistentOverlayController { + private var _content by mutableStateOf<(@Composable () -> Unit)?>(null) + + val isVisible: Boolean + get() = _content != null + + val content: (@Composable () -> Unit)? + get() = _content + + fun show(content: @Composable () -> Unit) { + _content = content + } + + fun hide() { + _content = null + } +} + +/** + * A persistent overlay container that sits above the player controls and gesture layer. + * This allows components to display content (like track selection panels) without being + * affected by the player controls visibility state. + * + * @param controller The controller that manages the overlay's visibility and content + * @param modifier Modifier to be applied to the container + */ +@Composable +fun PersistentOverlayContainer( + controller: PersistentOverlayController, + modifier: Modifier = Modifier +) { + AnimatedVisibility( + visible = controller.isVisible, + enter = fadeIn(), + exit = fadeOut(), + modifier = modifier + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = 0.5f)) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { controller.hide() } + ) + ) { + controller.content?.invoke() + } + } +} + +@Composable +fun rememberPersistentOverlayController(): PersistentOverlayController { + return remember { PersistentOverlayController() } +} diff --git a/app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerControlsOverlay.kt b/app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerControlsOverlay.kt index 9292fbc..f9ffd31 100644 --- a/app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerControlsOverlay.kt +++ b/app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerControlsOverlay.kt @@ -44,6 +44,7 @@ import hu.bbara.purefin.player.model.TrackOption fun PlayerControlsOverlay( uiState: PlayerUiState, showControls: Boolean, + overlayController: PersistentOverlayController, onBack: () -> Unit, onPlayPause: () -> Unit, onSeek: (Long) -> Unit, @@ -89,6 +90,7 @@ fun PlayerControlsOverlay( BottomSection( uiState = uiState, scrubbing = scrubbing, + overlayController = overlayController, onScrubStart = { scrubbing = true }, onScrub = onSeek, onScrubFinished = { scrubbing = false }, @@ -151,6 +153,7 @@ private fun TopBar( private fun BottomSection( uiState: PlayerUiState, scrubbing: Boolean, + overlayController: PersistentOverlayController, onScrubStart: () -> Unit, onScrub: (Long) -> Unit, onScrubFinished: () -> Unit, @@ -258,17 +261,20 @@ private fun BottomSection( QualitySelectionButton( options = uiState.qualityTracks, selectedId = uiState.selectedQualityTrackId, - onSelect = onSelectTrack + onSelect = onSelectTrack, + overlayController = overlayController ) AudioSelectionButton( options = uiState.audioTracks, selectedId = uiState.selectedAudioTrackId, - onSelect = onSelectTrack + onSelect = onSelectTrack, + overlayController = overlayController ) SubtitlesSelectionButton( options = uiState.textTracks, selectedId = uiState.selectedTextTrackId, - onSelect = onSelectTrack + onSelect = onSelectTrack, + overlayController = overlayController ) } } diff --git a/app/src/main/java/hu/bbara/purefin/player/ui/components/TrackSelectionButtons.kt b/app/src/main/java/hu/bbara/purefin/player/ui/components/TrackSelectionButtons.kt index 3d31ae5..7a267f1 100644 --- a/app/src/main/java/hu/bbara/purefin/player/ui/components/TrackSelectionButtons.kt +++ b/app/src/main/java/hu/bbara/purefin/player/ui/components/TrackSelectionButtons.kt @@ -2,6 +2,7 @@ package hu.bbara.purefin.player.ui.components import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -16,14 +17,11 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ClosedCaption import androidx.compose.material.icons.outlined.HighQuality import androidx.compose.material.icons.outlined.Language -import androidx.compose.material3.DropdownMenu import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight @@ -36,51 +34,29 @@ fun QualitySelectionButton( options: List, selectedId: String?, onSelect: (TrackOption) -> Unit, + overlayController: PersistentOverlayController, modifier: Modifier = Modifier ) { - var expanded by remember { mutableStateOf(false) } val scheme = MaterialTheme.colorScheme - Box(modifier = modifier) { - PurefinIconButton( - icon = Icons.Outlined.HighQuality, - contentDescription = "Quality", - onClick = { expanded = true } - ) - - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - modifier = Modifier - .widthIn(min = 160.dp, max = 280.dp) - ) { - Box( - modifier = Modifier - .background(scheme.surface.copy(alpha = 0.98f)) - .widthIn(min = 160.dp, max = 280.dp) - .heightIn(max = 280.dp) - ) { - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .padding(vertical = 8.dp, horizontal = 8.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - options.forEach { option -> - val selected = option.id == selectedId - TrackOptionItem( - label = option.label, - selected = selected, - onClick = { - onSelect(option) - expanded = false - } - ) + PurefinIconButton( + icon = Icons.Outlined.HighQuality, + contentDescription = "Quality", + onClick = { + overlayController.show { + TrackSelectionPanel( + title = "Quality", + options = options, + selectedId = selectedId, + onSelect = { option -> + onSelect(option) + overlayController.hide() } - } + ) } - } - } + }, + modifier = modifier + ) } @Composable @@ -88,99 +64,107 @@ fun AudioSelectionButton( options: List, selectedId: String?, onSelect: (TrackOption) -> Unit, + overlayController: PersistentOverlayController, modifier: Modifier = Modifier ) { - var expanded by remember { mutableStateOf(false) } - val scheme = MaterialTheme.colorScheme - - Box(modifier = modifier) { - PurefinIconButton( - icon = Icons.Outlined.Language, - contentDescription = "Audio", - onClick = { expanded = true } - ) - - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - modifier = Modifier - .widthIn(min = 160.dp, max = 280.dp) - ) { - Box( - modifier = Modifier - .background(scheme.surface.copy(alpha = 0.98f)) - .widthIn(min = 160.dp, max = 280.dp) - .heightIn(max = 280.dp) - ) { - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .padding(vertical = 8.dp, horizontal = 8.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - options.forEach { option -> - val selected = option.id == selectedId - TrackOptionItem( - label = option.label, - selected = selected, - onClick = { - onSelect(option) - expanded = false - } - ) + PurefinIconButton( + icon = Icons.Outlined.Language, + contentDescription = "Audio", + onClick = { + overlayController.show { + TrackSelectionPanel( + title = "Audio", + options = options, + selectedId = selectedId, + onSelect = { option -> + onSelect(option) + overlayController.hide() } - } + ) } - } - } + }, + modifier = modifier + ) } @Composable fun SubtitlesSelectionButton( + options: List, + selectedId: String?, + onSelect: (TrackOption) -> Unit, + overlayController: PersistentOverlayController, + modifier: Modifier = Modifier +) { + PurefinIconButton( + icon = Icons.Outlined.ClosedCaption, + contentDescription = "Subtitles", + onClick = { + overlayController.show { + TrackSelectionPanel( + title = "Subtitles", + options = options, + selectedId = selectedId, + onSelect = { option -> + onSelect(option) + overlayController.hide() + } + ) + } + }, + modifier = modifier + ) +} + +@Composable +private fun TrackSelectionPanel( + title: String, options: List, selectedId: String?, onSelect: (TrackOption) -> Unit, modifier: Modifier = Modifier ) { - var expanded by remember { mutableStateOf(false) } val scheme = MaterialTheme.colorScheme - Box(modifier = modifier) { - PurefinIconButton( - icon = Icons.Outlined.ClosedCaption, - contentDescription = "Subtitles", - onClick = { expanded = true } - ) - - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, + Box( + modifier = modifier + .fillMaxWidth() + .padding(16.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { /* Prevent clicks from bubbling */ } + ), + contentAlignment = Alignment.BottomEnd + ) { + Column( modifier = Modifier - .widthIn(min = 160.dp, max = 280.dp) + .widthIn(min = 200.dp, max = 320.dp) + .clip(RoundedCornerShape(12.dp)) + .background(scheme.surface.copy(alpha = 0.98f)) + .padding(vertical = 12.dp, horizontal = 8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) ) { - Box( + Text( + text = title, + color = scheme.onSurface, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + + Column( modifier = Modifier - .background(scheme.surface.copy(alpha = 0.98f)) - .widthIn(min = 160.dp, max = 280.dp) - .heightIn(max = 280.dp) + .heightIn(max = 400.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(4.dp) ) { - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .padding(vertical = 8.dp, horizontal = 8.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - options.forEach { option -> - val selected = option.id == selectedId - TrackOptionItem( - label = option.label, - selected = selected, - onClick = { - onSelect(option) - expanded = false - } - ) - } + options.forEach { option -> + val selected = option.id == selectedId + TrackOptionItem( + label = option.label, + selected = selected, + onClick = { onSelect(option) } + ) } } }