Android v1 polish + Web UI flavor pass #65

Merged
bvandeusen merged 13 commits from dev into main 2026-06-01 20:17:49 -04:00
Showing only changes of commit 2b9b4c1db6 - Show all commits
@@ -22,7 +22,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.foundation.gestures.detectVerticalDragGestures import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.Velocity
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
@@ -30,7 +34,6 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.material3.SliderDefaults import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
@@ -122,8 +125,11 @@ fun NowPlayingScreen(
return return
} }
val dominant = rememberDominantColor(track.coverUrl) val dominant = rememberDominantColor(track.coverUrl)
val dismissConnection = rememberDragDismissConnection(
onDismiss = { navController.popBackStack() },
)
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize().nowPlayingDragDismiss(navController), modifier = Modifier.fillMaxSize().nestedScroll(dismissConnection),
topBar = { NowPlayingTopBar(onClose = { navController.popBackStack() }) }, topBar = { NowPlayingTopBar(onClose = { navController.popBackStack() }) },
snackbarHost = { SnackbarHost(snackbarHostState) }, snackbarHost = { SnackbarHost(snackbarHostState) },
containerColor = Color.Transparent, containerColor = Color.Transparent,
@@ -161,26 +167,49 @@ private const val DOMINANT_TINT_MID_STOP = 0.45f
/** /**
* Drag-down to dismiss. Mirrors Flutter's modal-page gesture: a * Drag-down to dismiss. Mirrors Flutter's modal-page gesture: a
* downward drag past the threshold pops the screen, returning the * downward over-scroll past the threshold pops the screen.
* user to whichever shell route launched NowPlaying. Tap, swipe-up, *
* and horizontal drags pass through to the underlying content * Implementation note: the body Column uses verticalScroll, which
* (slider scrub, etc.). * eats every vertical drag delta before a top-level
* detectVerticalDragGestures can see it. NestedScroll is the
* right primitive here — verticalScroll offers its over-scroll
* deltas to the nearest ancestor connection, so we accumulate
* downward over-scroll (available.y > 0 means the scrollable
* couldn't consume it because it's at the top) and pop on
* threshold. The Slider keeps its own pointerInput and is
* unaffected.
*/ */
@Composable @Composable
private fun Modifier.nowPlayingDragDismiss(navController: NavHostController): Modifier = private fun rememberDragDismissConnection(
this.pointerInput(Unit) { onDismiss: () -> Unit,
var totalDragY = 0f thresholdPx: Float = DRAG_DISMISS_THRESHOLD_PX,
detectVerticalDragGestures( ): NestedScrollConnection = remember(onDismiss, thresholdPx) {
onDragStart = { totalDragY = 0f }, object : NestedScrollConnection {
onDragEnd = { private var accumulated = 0f
if (totalDragY > DRAG_DISMISS_THRESHOLD_PX) {
navController.popBackStack() override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource,
): Offset {
if (source != NestedScrollSource.UserInput) return Offset.Zero
if (available.y > 0f) {
accumulated += available.y
if (accumulated >= thresholdPx) {
accumulated = 0f
onDismiss()
}
return Offset(0f, available.y)
}
if (consumed.y != 0f || available.y < 0f) accumulated = 0f
return Offset.Zero
}
override suspend fun onPreFling(available: Velocity): Velocity {
accumulated = 0f
return Velocity.Zero
}
} }
totalDragY = 0f
},
onDragCancel = { totalDragY = 0f },
onVerticalDrag = { _, delta -> totalDragY += delta },
)
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)