fix(media): zone-based video controls — play always visible, controls on hover

- Center play button always visible when paused
- Pause button appears on center hover when playing
- Bottom controls bar only appears when hovering the bottom area
- Controls also show when paused for discoverability

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
nrobi144
2026-03-17 16:32:08 +02:00
parent e9249c924c
commit 248f38cc47
@@ -51,7 +51,6 @@ import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -62,7 +61,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
@Composable @Composable
fun VideoControls( fun VideoControls(
@@ -83,50 +81,44 @@ fun VideoControls(
onViewModeChange: ((ViewMode) -> Unit)? = null, onViewModeChange: ((ViewMode) -> Unit)? = null,
trailingControls: @Composable (() -> Unit)? = null, trailingControls: @Composable (() -> Unit)? = null,
) { ) {
var hovering by remember { mutableStateOf(false) } var hoveringCenter by remember { mutableStateOf(false) }
var showControls by remember { mutableStateOf(false) } var hoveringBottom by remember { mutableStateOf(false) }
// Show controls on hover, auto-hide 2s after mouse leaves
LaunchedEffect(hovering) {
if (hovering) {
showControls = true
} else {
delay(2000)
showControls = false
}
}
Box( Box(
modifier = modifier =
modifier modifier
.fillMaxSize() .fillMaxSize()
.clickable { onPlayPause() } .clickable { onPlayPause() },
.pointerInput(Unit) { ) {
awaitPointerEventScope { // Center hover zone — top 70% of the video
while (true) { Box(
val event = awaitPointerEvent() modifier =
when (event.type) { Modifier
PointerEventType.Enter -> { .fillMaxWidth()
hovering = true .fillMaxSize(0.7f)
showControls = true .align(Alignment.TopCenter)
} .pointerInput(Unit) {
awaitPointerEventScope {
PointerEventType.Exit -> { while (true) {
hovering = false val event = awaitPointerEvent()
when (event.type) {
PointerEventType.Enter -> hoveringCenter = true
PointerEventType.Exit -> hoveringCenter = false
} }
} }
} }
} },
}, )
) {
// Center play/buffering indicator — only on hover // Center play/buffering indicator
if (isBuffering) { if (isBuffering) {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center).size(48.dp), modifier = Modifier.align(Alignment.Center).size(48.dp),
color = Color.White, color = Color.White,
strokeWidth = 3.dp, strokeWidth = 3.dp,
) )
} else if (!isPlaying && showControls) { } else if (!isPlaying) {
// Always show play button when paused
IconButton( IconButton(
onClick = onPlayPause, onClick = onPlayPause,
modifier = Modifier.align(Alignment.Center).size(64.dp), modifier = Modifier.align(Alignment.Center).size(64.dp),
@@ -138,14 +130,40 @@ fun VideoControls(
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
) )
} }
} else if (hoveringCenter) {
// Show pause button on center hover when playing
IconButton(
onClick = onPlayPause,
modifier = Modifier.align(Alignment.Center).size(64.dp),
) {
Icon(
Icons.Default.Pause,
contentDescription = "Pause",
tint = Color.White,
modifier = Modifier.size(48.dp),
)
}
} }
// Bottom controls — two rows: seek slider on top, buttons below // Bottom controls — show on hover over bottom area
AnimatedVisibility( AnimatedVisibility(
visible = showControls, visible = hoveringBottom || !isPlaying,
enter = fadeIn(), enter = fadeIn(),
exit = fadeOut(), exit = fadeOut(),
modifier = Modifier.align(Alignment.BottomCenter), modifier =
Modifier
.align(Alignment.BottomCenter)
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
when (event.type) {
PointerEventType.Enter -> hoveringBottom = true
PointerEventType.Exit -> hoveringBottom = false
}
}
}
},
) { ) {
Column( Column(
modifier = modifier =