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.IconButton
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.Slider
import androidx.compose.material3.TopAppBar
@@ -30,7 +34,6 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
@@ -122,8 +125,11 @@ fun NowPlayingScreen(
return
}
val dominant = rememberDominantColor(track.coverUrl)
val dismissConnection = rememberDragDismissConnection(
onDismiss = { navController.popBackStack() },
)
Scaffold(
modifier = Modifier.fillMaxSize().nowPlayingDragDismiss(navController),
modifier = Modifier.fillMaxSize().nestedScroll(dismissConnection),
topBar = { NowPlayingTopBar(onClose = { navController.popBackStack() }) },
snackbarHost = { SnackbarHost(snackbarHostState) },
containerColor = Color.Transparent,
@@ -161,27 +167,50 @@ private const val DOMINANT_TINT_MID_STOP = 0.45f
/**
* Drag-down to dismiss. Mirrors Flutter's modal-page gesture: a
* downward drag past the threshold pops the screen, returning the
* user to whichever shell route launched NowPlaying. Tap, swipe-up,
* and horizontal drags pass through to the underlying content
* (slider scrub, etc.).
* downward over-scroll past the threshold pops the screen.
*
* Implementation note: the body Column uses verticalScroll, which
* 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
private fun Modifier.nowPlayingDragDismiss(navController: NavHostController): Modifier =
this.pointerInput(Unit) {
var totalDragY = 0f
detectVerticalDragGestures(
onDragStart = { totalDragY = 0f },
onDragEnd = {
if (totalDragY > DRAG_DISMISS_THRESHOLD_PX) {
navController.popBackStack()
private fun rememberDragDismissConnection(
onDismiss: () -> Unit,
thresholdPx: Float = DRAG_DISMISS_THRESHOLD_PX,
): NestedScrollConnection = remember(onDismiss, thresholdPx) {
object : NestedScrollConnection {
private var accumulated = 0f
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()
}
totalDragY = 0f
},
onDragCancel = { totalDragY = 0f },
onVerticalDrag = { _, delta -> totalDragY += delta },
)
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
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable