From 1ef5cd5b7a68e4c97ead558de5fb11584bdbc48a Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 28 May 2026 13:33:43 -0400 Subject: [PATCH] feat(android): swipe-up-to-expand gesture on MiniPlayer (#34) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports player_bar.dart's onVerticalDragEnd — an upward flick anywhere on the bar expands into NowPlaying, alongside the existing tap-to-expand. A vertical draggable claims only vertical-dominant drags, so the inline seek slider keeps its horizontal scrub gesture. The 200 px/s Flutter threshold is expressed in dp and density-converted so the flick feels the same across screen densities. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../minstrel/player/ui/MiniPlayer.kt | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/ui/MiniPlayer.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/MiniPlayer.kt index 806030bf..23e70d28 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/ui/MiniPlayer.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/MiniPlayer.kt @@ -4,6 +4,9 @@ import androidx.compose.animation.Crossfade import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -27,6 +30,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -52,6 +56,11 @@ private const val MINI_BAR_TONAL_ELEVATION_DP = 4 private const val COVER_SIZE_DP = 48 private const val SCRUBBER_ROW_HEIGHT_DP = 4 +// Upward flick speed (dp/s) that expands the bar into NowPlaying. +// Mirrors player_bar.dart's 200 px/s threshold; expressed in dp and +// converted via density so the gesture feels the same across screens. +private const val SWIPE_UP_VELOCITY_DP = 200 + @OptIn(ExperimentalSharedTransitionApi::class) @Composable private fun MiniCover(coverUrl: String, contentDescription: String) { @@ -126,10 +135,22 @@ fun MiniPlayer( val isLiked by trackActionsViewModel.isLikedFlow(track.id) .collectAsStateWithLifecycle(initialValue = false) + // Swipe up anywhere on the bar to expand into the full player — + // mirrors player_bar.dart. We only act on a clear upward flick + // (negative velocity past the threshold) so a slow tap-with-jitter + // doesn't accidentally open the screen. The horizontal seek slider + // keeps its own gestures; a vertical draggable only claims + // vertical-dominant drags. + val flickThresholdPx = with(LocalDensity.current) { SWIPE_UP_VELOCITY_DP.dp.toPx() } Surface( modifier = modifier .fillMaxWidth() - .height(MINI_BAR_HEIGHT_DP.dp), + .height(MINI_BAR_HEIGHT_DP.dp) + .draggable( + orientation = Orientation.Vertical, + state = rememberDraggableState { }, + onDragStopped = { velocity -> if (velocity < -flickThresholdPx) onExpandClick() }, + ), color = MaterialTheme.colorScheme.surface, tonalElevation = MINI_BAR_TONAL_ELEVATION_DP.dp, ) {