From 3d7b6bcf139850b07c18f639bc62af4b0f9dd882 Mon Sep 17 00:00:00 2001 From: Barnabas Balogh Date: Tue, 27 Jan 2026 18:18:09 +0100 Subject: [PATCH] refactor: rename player gesture handlers for clarity - Rename `PlayerGesturesLayer` parameters to be more descriptive of the gesture type (e.g., `onDoubleTapLeft`, `onDoubleTapCenter`, `onDoubleTapRight`). - Update the `PlayerScreen` to use the new, more specific gesture handler names. --- .../bbara/purefin/player/ui/PlayerScreen.kt | 64 +++++++++++++++++++ .../components/HorizontalSeekGestureHelper.kt | 21 ++++++ .../ui/components/PlayerGesturesLayer.kt | 59 ++++++++++++++++- 3 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/hu/bbara/purefin/player/ui/components/HorizontalSeekGestureHelper.kt 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 4d2b521..719dd15 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 @@ -13,7 +13,10 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -23,7 +26,9 @@ 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.platform.LocalContext +import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.media3.common.util.UnstableApi import androidx.media3.ui.AspectRatioFrameLayout @@ -35,6 +40,8 @@ import hu.bbara.purefin.player.ui.components.PlayerQueuePanel import hu.bbara.purefin.player.ui.components.PlayerSettingsSheet import hu.bbara.purefin.player.ui.components.PlayerSideSliders import hu.bbara.purefin.player.viewmodel.PlayerViewModel +import kotlinx.coroutines.delay +import kotlin.math.abs import kotlin.math.roundToInt @OptIn(UnstableApi::class) @@ -57,6 +64,15 @@ fun PlayerScreen( var brightnessOverlayVisible by remember { mutableStateOf(false) } var volumeOverlayVisible by remember { mutableStateOf(false) } var showQueuePanel by remember { mutableStateOf(false) } + var horizontalSeekFeedback by remember { mutableStateOf(null) } + var showFeedbackPreview by remember { mutableStateOf(false) } + + LaunchedEffect(showFeedbackPreview) { + if (!showFeedbackPreview) { + delay(1000) + horizontalSeekFeedback = null + } + } LaunchedEffect(uiState.isPlaying) { if (uiState.isPlaying) { @@ -107,9 +123,27 @@ fun PlayerScreen( (volume * maxVolume).roundToInt(), 0 ) + }, + setFeedBackPreview = { + showFeedbackPreview = it + }, + onHorizontalDragPreview = { + horizontalSeekFeedback = it + }, + onHorizontalDrag = { + viewModel.seekBy(it) + horizontalSeekFeedback = it } ) + horizontalSeekFeedback?.let { delta -> + SeekAmountIndicator( + deltaMs = delta, + modifier = Modifier + .align(Alignment.Center) + ) + } + AnimatedVisibility( visible = volumeOverlayVisible || brightnessOverlayVisible, enter = fadeIn(), @@ -197,6 +231,36 @@ fun PlayerScreen( } } +@Composable +private fun SeekAmountIndicator(deltaMs: Long, modifier: Modifier = Modifier) { + val scheme = MaterialTheme.colorScheme + val prefix = if (deltaMs >= 0) "+" else "-" + val formatted = formatSeekDelta(abs(deltaMs)) + Box( + modifier = modifier + .clip(RoundedCornerShape(16.dp)) + .background(scheme.surface.copy(alpha = 0.9f)) + .padding(horizontal = 20.dp, vertical = 12.dp) + ) { + Text( + text = "$prefix$formatted", + color = scheme.onSurface, + style = MaterialTheme.typography.titleMedium + ) + } +} + +private fun formatSeekDelta(deltaMs: Long): String { + val totalSeconds = (deltaMs / 1000).toInt() + val seconds = totalSeconds % 60 + val minutes = totalSeconds / 60 + return if (minutes > 0) { + "%d:%02d".format(minutes, seconds) + } else { + "%02d s".format(seconds) + } +} + private fun readCurrentBrightness(activity: Activity?): Float { val current = activity?.window?.attributes?.screenBrightness return if (current != null && current >= 0) current else 0.5f diff --git a/app/src/main/java/hu/bbara/purefin/player/ui/components/HorizontalSeekGestureHelper.kt b/app/src/main/java/hu/bbara/purefin/player/ui/components/HorizontalSeekGestureHelper.kt new file mode 100644 index 0000000..0efa5d7 --- /dev/null +++ b/app/src/main/java/hu/bbara/purefin/player/ui/components/HorizontalSeekGestureHelper.kt @@ -0,0 +1,21 @@ +package hu.bbara.purefin.player.ui.components + +import androidx.compose.ui.unit.dp +import kotlin.math.abs +import kotlin.math.pow + +internal object HorizontalSeekGestureHelper { + val START_THRESHOLD = 24.dp + private const val COEFFICIENT = 1.3f + const val EXPONENT = 1.8f + private const val MAX_DELTA_MS = 300_000L + + fun deltaMs(rawDelta: Float): Long { + val magnitude = abs(rawDelta) + if (magnitude == 0f) return 0L + val magnitudePow = magnitude.toDouble().pow(EXPONENT.toDouble()).toFloat() + val scaled = COEFFICIENT * magnitudePow + val signed = if (rawDelta > 0f) scaled else -scaled + return signed.toLong().coerceIn(-MAX_DELTA_MS, MAX_DELTA_MS) + } +} diff --git a/app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerGesturesLayer.kt b/app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerGesturesLayer.kt index d334e3c..f484691 100644 --- a/app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerGesturesLayer.kt +++ b/app/src/main/java/hu/bbara/purefin/player/ui/components/PlayerGesturesLayer.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity @Composable fun PlayerGesturesLayer( @@ -17,8 +18,14 @@ fun PlayerGesturesLayer( onDoubleTapRight: () -> Unit, onDoubleTapLeft: () -> Unit, onVerticalDragLeft: (delta: Float) -> Unit, - onVerticalDragRight: (delta: Float) -> Unit + onVerticalDragRight: (delta: Float) -> Unit, + onHorizontalDragPreview: (deltaMs: Long?) -> Unit = {}, + onHorizontalDrag: (deltaMs: Long) -> Unit, + setFeedBackPreview: (show: Boolean) -> Unit ) { + val density = LocalDensity.current + val horizontalThresholdPx = with(density) { HorizontalSeekGestureHelper.START_THRESHOLD.toPx() } + Box( modifier = modifier .fillMaxSize() @@ -50,5 +57,55 @@ fun PlayerGesturesLayer( } } } + .pointerInput(Unit) { + var accumulatedHorizontalDrag = 0f + var isHorizontalDragActive = false + var lastPreviewDelta: Long? = null + detectHorizontalDragGestures( + onDragStart = { + accumulatedHorizontalDrag = 0f + isHorizontalDragActive = false + lastPreviewDelta = null + setFeedBackPreview(false) + onHorizontalDragPreview(null) + }, + onHorizontalDrag = { change, dragAmount -> + accumulatedHorizontalDrag += dragAmount + if (!isHorizontalDragActive && kotlin.math.abs(accumulatedHorizontalDrag) >= horizontalThresholdPx) { + isHorizontalDragActive = true + setFeedBackPreview(true) + } + if (isHorizontalDragActive) { + change.consume() + val deltaMs = HorizontalSeekGestureHelper.deltaMs(accumulatedHorizontalDrag) + if (deltaMs != 0L && deltaMs != lastPreviewDelta) { + lastPreviewDelta = deltaMs + onHorizontalDragPreview(deltaMs) + } + } + }, + onDragEnd = { + if (isHorizontalDragActive) { + val deltaMs = HorizontalSeekGestureHelper.deltaMs(accumulatedHorizontalDrag) + if (deltaMs != 0L) { + onHorizontalDrag(deltaMs) + onHorizontalDragPreview(deltaMs) + } + } + accumulatedHorizontalDrag = 0f + isHorizontalDragActive = false + lastPreviewDelta = null + setFeedBackPreview(false) + onHorizontalDragPreview(null) + }, + onDragCancel = { + accumulatedHorizontalDrag = 0f + isHorizontalDragActive = false + lastPreviewDelta = null + setFeedBackPreview(false) + onHorizontalDragPreview(null) + } + ) + } ) }