With the player beside the map, like, shuffle, repeat and lyrics move into the nav bar

The nav bar's pill carries them while the player is docked, with its dock button in the
same place to close it, so the player itself is just the cover, the song and play/skip.
The player's own close button, which sat over the corner of the cover, is gone.
This commit is contained in:
Seb3thehacker committed 2026-11-11 12:21:55 -08:00
1 parent fa33e7c650
commit 4aeaaee875
3 files changed
+50 -24

No files matched your search

@@ -275,31 +275,15 @@ private fun DockedPlayer(now: NowPlaying, modifier: Modifier) {
// Sized from the column's width rather than fixed: fixed sizes filled a narrow
// column edge to edge, so the buttons sat squeezed against each other. Each row
// keeps a clear gap between buttons, and none grows past a comfortable size.
// Like, shuffle, repeat and lyrics are in the nav bar's pill while the player is
// here (NavNowPlaying), which leaves the cover more room.
BoxWithConstraints(Modifier.fillMaxWidth()) {
val w = maxWidth
// Two skips, play at 1.25 times a skip, and two gaps of at least 20dp.
val skip = ((w - 40.dp) / 3.25f).coerceIn(36.dp, 48.dp)
// Four buttons and three gaps of at least 12dp.
val mode = ((w - 36.dp) / 4).coerceIn(34.dp, 42.dp)
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
TransportRow(now, media, skip = skip, modifier = Modifier.fillMaxWidth())
Spacer(Modifier.height(10.dp))
PlayModeRow(now, media, size = mode, modifier = Modifier.fillMaxWidth()) {
GsIconButton(
ImageVector.vectorResource(R.drawable.lyrics_24), stringResource(R.string.storage_lyrics), navigator::showSideLyrics,
size = mode, iconSize = mode * 0.52f,
)
}
}
val skip = ((maxWidth - 40.dp) / 3.25f).coerceIn(36.dp, 48.dp)
TransportRow(now, media, skip = skip, modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp))
}
}
// Round like the player's other buttons, and tucked into the card's corner so it
// covers as little of the cover as it can.
GsIconButton(
Icons.Filled.Close, stringResource(R.string.home_back_nav), navigator::closeSidePanel,
Modifier.align(Alignment.TopEnd).offset(x = 6.dp, y = (-6).dp), size = 32.dp, iconSize = 18.dp,
shape = CircleShape,
)
// No close button here: the nav bar's dock button closes the player (NavNowPlaying).
}
}
}
@@ -556,8 +556,13 @@ private fun NavNowPlaying(navigator: CarNavigator, selected: Boolean, modifier:
val phase by media.phase.collectAsState()
val now by media.now.collectAsState()
if (phase != CarMedia.Phase.READY || !now.isActive) return
// Docked beside the map, the player is already on screen - the pill steps aside for it.
if (navigator.sidePanel == SidePanel.CONTROLS && navigator.current == CarScreen.Home) return
// Docked beside the map, the player is already on screen, so the pill carries what the
// player leaves out - like, shuffle, repeat and lyrics - and its dock button, filled in, in
// the same place: tapped again, it closes the player.
if (navigator.sidePanel == SidePanel.CONTROLS && navigator.current == CarScreen.Home) {
DockedPill(navigator, now, selected, modifier)
return
}
// The media screen is the full player already; the pill would only repeat its controls.
if (navigator.current == CarScreen.Media) return
val context = LocalContext.current
@@ -670,6 +675,42 @@ private fun NavNowPlaying(navigator: CarNavigator, selected: Boolean, modifier:
}
}
/**
* The pill while the player is docked: like, shuffle, repeat and lyrics, then the dock button,
* shown open, which closes it. Sits on the player's side of the bar, under the player.
*/
@Composable
private fun DockedPill(navigator: CarNavigator, now: app.seb3thehacker.gearslip.media.NowPlaying, selected: Boolean, modifier: Modifier) {
val scheme = MaterialTheme.colorScheme
val playerLeft by CarSettings.playerLeft.collectAsState()
Box(modifier.fillMaxWidth(), contentAlignment = if (playerLeft) Alignment.CenterStart else Alignment.CenterEnd) {
Surface(
modifier = Modifier.height(44.dp),
shape = RoundedCornerShape(14.dp),
color = if (selected) scheme.primaryContainer else scheme.secondaryContainer,
contentColor = if (selected) scheme.onPrimaryContainer else scheme.onSecondaryContainer,
) {
PlayModeRow(
now, CarServices.media, size = 36.dp,
modifier = Modifier.fillMaxHeight().padding(horizontal = 4.dp),
arrangement = Arrangement.spacedBy(6.dp),
) {
GsIconButton(
androidx.compose.ui.graphics.vector.ImageVector.vectorResource(R.drawable.lyrics_24),
stringResource(R.string.storage_lyrics), navigator::showSideLyrics,
size = 36.dp, iconSize = 20.dp,
)
GsIconButton(
if (playerLeft) MediaIcons.DockLeftOpen else MediaIcons.DockRightOpen,
stringResource(R.string.navbar_close_panel),
navigator::togglePlayerDock,
size = 36.dp, iconSize = 22.dp,
)
}
}
}
}
/**
* A filled lightning bolt, next to the battery percentage instead of the word "charging" - the
* core Material icon set this project ships (no extended pack, to keep the app small) has no
@@ -130,6 +130,7 @@ internal fun PlayModeRow(
size: Dp,
modifier: Modifier = Modifier,
maxOther: Int = 0,
arrangement: Arrangement.Horizontal = Arrangement.SpaceEvenly,
trailing: @Composable RowScope.() -> Unit = {},
) {
val modes by media.modes.collectAsState()
@@ -139,7 +140,7 @@ internal fun PlayModeRow(
val shuffleOn = shuffleKnown && modes.shuffle != PlaybackStateCompat.SHUFFLE_MODE_NONE
val repeatOn = repeatKnown && modes.repeat != PlaybackStateCompat.REPEAT_MODE_NONE
val icon = size * 0.52f
Row(modifier, horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically) {
Row(modifier, horizontalArrangement = arrangement, verticalAlignment = Alignment.CenterVertically) {
// An app's own like, shuffle or repeat button shows whether it's on only through its icon,
// so that icon is drawn, as Android Auto does, and lit when its name says it's on.
// Gearslip's own stand in for apps that report a heart rating or shuffle and repeat modes.