mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +01:00
fix back popping the route under now playing; disc header spacing; dial back the fab glyph
- register the now playing and queue back callback from a LaunchedEffect instead of BackHandler: the NavHost registers its own callback from a LaunchedEffect, so a BackHandler composed before it loses the back and pops the route underneath - NowPlayingBackTest registers the overlay handler before the NavHost (the losing order) and asserts back closes the overlay without popping the route - more top padding on disc headers, except the first which sits flush - LIST_OPTIONS_WEIGHT 0.15 -> 0.11
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
package com.subtracks.ui
|
package com.subtracks.ui
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.animate
|
import androidx.compose.animation.core.animate
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
@@ -32,6 +33,7 @@ import androidx.compose.foundation.layout.navigationBars
|
|||||||
import androidx.compose.foundation.layout.statusBars
|
import androidx.compose.foundation.layout.statusBars
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -72,6 +74,7 @@ import com.subtracks.ui.settings.SettingsRoute
|
|||||||
import com.subtracks.ui.theme.ArtworkTheme
|
import com.subtracks.ui.theme.ArtworkTheme
|
||||||
import com.subtracks.ui.theme.rememberArtworkColors
|
import com.subtracks.ui.theme.rememberArtworkColors
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.awaitCancellation
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
@@ -157,6 +160,30 @@ private fun MainNavigation() {
|
|||||||
if (!open) nowPlayingOpen = false
|
if (!open) nowPlayingOpen = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The NavHost registers its back callback from a LaunchedEffect, so a BackHandler composed
|
||||||
|
// before it can lose the back to the NavHost (which would pop the route under the overlay).
|
||||||
|
// Register ours from a LaunchedEffect after the NavHost so it lands later and wins.
|
||||||
|
val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
|
||||||
|
val overlayBack =
|
||||||
|
remember {
|
||||||
|
object : OnBackPressedCallback(false) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
when {
|
||||||
|
showingQueue -> showingQueue = false
|
||||||
|
nowPlayingOpen -> settleNowPlaying(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SideEffect { overlayBack.isEnabled = showingQueue || nowPlayingOpen }
|
||||||
|
LaunchedEffect(backDispatcher, overlayBack) {
|
||||||
|
backDispatcher?.addCallback(overlayBack)
|
||||||
|
try {
|
||||||
|
awaitCancellation()
|
||||||
|
} finally {
|
||||||
|
overlayBack.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
LaunchedEffect(Unit) { playbackController.connect() }
|
LaunchedEffect(Unit) { playbackController.connect() }
|
||||||
LaunchedEffect(Unit) { if (nowPlayingOpen) nowPlayingProgress = 1f }
|
LaunchedEffect(Unit) { if (nowPlayingOpen) nowPlayingProgress = 1f }
|
||||||
|
|
||||||
@@ -370,9 +397,4 @@ private fun MainNavigation() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BackHandler(enabled = showingQueue) { showingQueue = false }
|
|
||||||
BackHandler(enabled = nowPlayingOpen && !showingQueue) {
|
|
||||||
settleNowPlaying(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ fun AlbumDetailScreen(
|
|||||||
val disc = song.disc ?: 1L
|
val disc = song.disc ?: 1L
|
||||||
val discLabel = discLabels[disc]
|
val discLabel = discLabels[disc]
|
||||||
if (disc != lastDisc && (multiDisc || discLabel != null)) {
|
if (disc != lastDisc && (multiDisc || discLabel != null)) {
|
||||||
item(key = "disc:$disc") { DiscHeader(discLabel ?: "Disc $disc") }
|
item(key = "disc:$disc") { DiscHeader(discLabel ?: "Disc $disc", first = lastDisc == null) }
|
||||||
}
|
}
|
||||||
lastDisc = disc
|
lastDisc = disc
|
||||||
item(key = song.id) {
|
item(key = song.id) {
|
||||||
@@ -146,10 +146,13 @@ fun AlbumDetailScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DiscHeader(text: String) {
|
private fun DiscHeader(
|
||||||
|
text: String,
|
||||||
|
first: Boolean,
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 28.dp, bottom = 4.dp),
|
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = if (first) 0.dp else 40.dp, bottom = 4.dp),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.FiberSmartRecord,
|
imageVector = Icons.Outlined.FiberSmartRecord,
|
||||||
|
|||||||
@@ -653,7 +653,7 @@ private val TAB_BAR_CONTENT_HEIGHT = 52.dp
|
|||||||
private val TAB_ICON_SIZE = 24.dp
|
private val TAB_ICON_SIZE = 24.dp
|
||||||
private val TAB_VERTICAL_PADDING = 6.dp
|
private val TAB_VERTICAL_PADDING = 6.dp
|
||||||
private val TOUCH_TARGET = 48.dp
|
private val TOUCH_TARGET = 48.dp
|
||||||
private const val LIST_OPTIONS_WEIGHT = 0.15f
|
private const val LIST_OPTIONS_WEIGHT = 0.11f
|
||||||
private val LIST_OPTIONS_BAR_WIDTHS = listOf(1f, 0.62f, 0.34f)
|
private val LIST_OPTIONS_BAR_WIDTHS = listOf(1f, 0.62f, 0.34f)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
package com.subtracks.ui
|
package com.subtracks.ui
|
||||||
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.OnBackPressedCallback
|
||||||
|
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
|
||||||
@@ -17,6 +26,7 @@ import androidx.navigation.compose.composable
|
|||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
|
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
|
||||||
|
import kotlinx.coroutines.awaitCancellation
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -38,8 +48,37 @@ class NowPlayingBackTest {
|
|||||||
composeRule.setContent {
|
composeRule.setContent {
|
||||||
val nav = rememberNavController()
|
val nav = rememberNavController()
|
||||||
navController = nav
|
navController = nav
|
||||||
|
val dispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
|
||||||
|
val overlayBackCallback =
|
||||||
|
remember {
|
||||||
|
object : OnBackPressedCallback(false) {
|
||||||
|
override fun handleOnBackPressed() {
|
||||||
|
overlayOpen = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SideEffect { overlayBackCallback.isEnabled = overlayOpen }
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
NavHost(navController = nav, startDestination = "a") {
|
NavHost(
|
||||||
|
navController = nav,
|
||||||
|
startDestination = "a",
|
||||||
|
enterTransition = {
|
||||||
|
slideInHorizontally(initialOffsetX = { it / 4 }, animationSpec = tween(260)) +
|
||||||
|
fadeIn(tween(260))
|
||||||
|
},
|
||||||
|
exitTransition = {
|
||||||
|
slideOutHorizontally(targetOffsetX = { -it / 4 }, animationSpec = tween(260)) +
|
||||||
|
fadeOut(tween(260))
|
||||||
|
},
|
||||||
|
popEnterTransition = {
|
||||||
|
slideInHorizontally(initialOffsetX = { -it / 4 }, animationSpec = tween(260)) +
|
||||||
|
fadeIn(tween(260))
|
||||||
|
},
|
||||||
|
popExitTransition = {
|
||||||
|
slideOutHorizontally(targetOffsetX = { it / 4 }, animationSpec = tween(260)) +
|
||||||
|
fadeOut(tween(260))
|
||||||
|
},
|
||||||
|
) {
|
||||||
composable("a") { Text("A") }
|
composable("a") { Text("A") }
|
||||||
composable("b") { Text("B") }
|
composable("b") { Text("B") }
|
||||||
}
|
}
|
||||||
@@ -47,10 +86,15 @@ class NowPlayingBackTest {
|
|||||||
Text("OVERLAY", modifier = Modifier.fillMaxSize())
|
Text("OVERLAY", modifier = Modifier.fillMaxSize())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Same ordering as MainNavigation: the overlay's back handler is registered after the
|
// The NavHost registers its back callback from a LaunchedEffect, so the overlay must
|
||||||
// NavHost, so it must consume back and leave the destination in place.
|
// register its own from a LaunchedEffect after it to land later in the dispatcher.
|
||||||
BackHandler(enabled = overlayOpen) {
|
LaunchedEffect(dispatcher, overlayBackCallback) {
|
||||||
overlayOpen = false
|
dispatcher.addCallback(overlayBackCallback)
|
||||||
|
try {
|
||||||
|
awaitCancellation()
|
||||||
|
} finally {
|
||||||
|
overlayBackCallback.remove()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user