mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
fix: prevent track selection overlay from auto-hiding on tap
Separated track selection UI from player controls overlay to prevent the gesture layer from dismissing the track selection panel when selecting options. Created PersistentOverlayContainer that manages its own visibility state independent of player controls.
This commit is contained in:
@@ -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<Long?>(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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TrackOption>,
|
||||
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 = {
|
||||
overlayController.show {
|
||||
TrackSelectionPanel(
|
||||
title = "Quality",
|
||||
options = options,
|
||||
selectedId = selectedId,
|
||||
onSelect = { option ->
|
||||
onSelect(option)
|
||||
expanded = false
|
||||
overlayController.hide()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -88,51 +64,27 @@ fun AudioSelectionButton(
|
||||
options: List<TrackOption>,
|
||||
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 = {
|
||||
overlayController.show {
|
||||
TrackSelectionPanel(
|
||||
title = "Audio",
|
||||
options = options,
|
||||
selectedId = selectedId,
|
||||
onSelect = { option ->
|
||||
onSelect(option)
|
||||
expanded = false
|
||||
overlayController.hide()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -140,34 +92,70 @@ fun SubtitlesSelectionButton(
|
||||
options: List<TrackOption>,
|
||||
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.ClosedCaption,
|
||||
contentDescription = "Subtitles",
|
||||
onClick = { expanded = true }
|
||||
onClick = {
|
||||
overlayController.show {
|
||||
TrackSelectionPanel(
|
||||
title = "Subtitles",
|
||||
options = options,
|
||||
selectedId = selectedId,
|
||||
onSelect = { option ->
|
||||
onSelect(option)
|
||||
overlayController.hide()
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
modifier = Modifier
|
||||
.widthIn(min = 160.dp, max = 280.dp)
|
||||
@Composable
|
||||
private fun TrackSelectionPanel(
|
||||
title: String,
|
||||
options: List<TrackOption>,
|
||||
selectedId: String?,
|
||||
onSelect: (TrackOption) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(scheme.surface.copy(alpha = 0.98f))
|
||||
.widthIn(min = 160.dp, max = 280.dp)
|
||||
.heightIn(max = 280.dp)
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = { /* Prevent clicks from bubbling */ }
|
||||
),
|
||||
contentAlignment = Alignment.BottomEnd
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(vertical = 8.dp, horizontal = 8.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)
|
||||
) {
|
||||
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
|
||||
.heightIn(max = 400.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
options.forEach { option ->
|
||||
@@ -175,17 +163,13 @@ fun SubtitlesSelectionButton(
|
||||
TrackOptionItem(
|
||||
label = option.label,
|
||||
selected = selected,
|
||||
onClick = {
|
||||
onSelect(option)
|
||||
expanded = false
|
||||
}
|
||||
onClick = { onSelect(option) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TrackOptionItem(
|
||||
|
||||
Reference in New Issue
Block a user