Merge remote-tracking branch 'origin/main' into claude/audio-rooms-android-ui-PIpFN

This commit is contained in:
Claude
2026-04-27 19:30:18 +00:00
2 changed files with 27 additions and 21 deletions
@@ -34,19 +34,22 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.toRoute import androidx.navigation.toRoute
// Per-entry hint stored on NavBackStackEntry.savedStateHandle by // Per-entry hint stamped by Nav.navBottomBar marking that the entry was
// Nav.navBottomBar. When present, composableFromEnd skips the horizontal // reached via a bottom-nav tab. Used in two places:
// slide so bottom-bar taps fall back to the NavHost-level fade. // - composableFromEnd skips the horizontal slide on tab entries so
const val SKIP_SLIDE_ANIMATION_KEY = "skipSlideAnimation" // bottom-bar taps fall back to the NavHost-level fade.
// - Nav.canPop hides the back arrow on tab roots even though Home
// sits below them in the stack.
const val BOTTOM_NAV_ROOT_KEY = "bottomNavRoot"
fun NavBackStackEntry.skipsSlideAnimation(): Boolean = savedStateHandle.get<Boolean>(SKIP_SLIDE_ANIMATION_KEY) == true fun NavBackStackEntry.isBottomNavRoot(): Boolean = savedStateHandle.get<Boolean>(BOTTOM_NAV_ROOT_KEY) == true
inline fun <reified T : Any> NavGraphBuilder.composableFromEnd(noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit) { inline fun <reified T : Any> NavGraphBuilder.composableFromEnd(noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit) {
composable<T>( composable<T>(
enterTransition = { if (targetState.skipsSlideAnimation()) null else slideInHorizontallyFromEnd }, enterTransition = { if (targetState.isBottomNavRoot()) null else slideInHorizontallyFromEnd },
exitTransition = { if (targetState.skipsSlideAnimation()) null else scaleOut }, exitTransition = { if (targetState.isBottomNavRoot()) null else scaleOut },
popEnterTransition = { if (initialState.skipsSlideAnimation()) null else scaleIn }, popEnterTransition = { if (initialState.isBottomNavRoot()) null else scaleIn },
popExitTransition = { if (initialState.skipsSlideAnimation()) null else slideOutHorizontallyToEnd }, popExitTransition = { if (initialState.isBottomNavRoot()) null else slideOutHorizontallyToEnd },
content = content, content = content,
) )
} }
@@ -24,9 +24,9 @@ import android.annotation.SuppressLint
import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerValue import androidx.compose.material3.DrawerValue
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.vitorpamplona.amethyst.ui.navigation.SKIP_SLIDE_ANIMATION_KEY import com.vitorpamplona.amethyst.ui.navigation.BOTTOM_NAV_ROOT_KEY
import com.vitorpamplona.amethyst.ui.navigation.isBottomNavRoot
import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.navigation.routes.getRouteWithArguments import com.vitorpamplona.amethyst.ui.navigation.routes.getRouteWithArguments
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -79,22 +79,25 @@ class Nav(
override fun navBottomBar(route: Route) { override fun navBottomBar(route: Route) {
navigationScope.launch { navigationScope.launch {
controller.navigate(route) { controller.navigate(route) {
// Clear the back stack down to and including the graph's start // Clear sibling bottom-nav entries but keep Home (the start
// destination so a bottom-nav tap leaves only the new route in // destination) below, so back-swipe from any tab returns to
// the stack. Without inclusive=true, Home would remain below // Home and back-swipe from Home leaves the app.
// the new tab and canPop() would wrongly show a back arrow. popUpTo(Route.Home) {
popUpTo(controller.graph.findStartDestination().id) { inclusive = false
inclusive = true
} }
launchSingleTop = true launchSingleTop = true
} }
// Stamp the entry so composableFromEnd's transition lambdas can // Mark this entry as a tab root: hides the back arrow in canPop
// skip the slide animation when this entry enters or exits. // and skips the horizontal slide in composableFromEnd.
controller.getBackStackEntry(route).savedStateHandle[SKIP_SLIDE_ANIMATION_KEY] = true controller.getBackStackEntry(route).savedStateHandle[BOTTOM_NAV_ROOT_KEY] = true
} }
} }
override fun canPop(): Boolean = controller.previousBackStackEntry != null override fun canPop(): Boolean {
val current = controller.currentBackStackEntry ?: return false
if (current.isBottomNavRoot()) return false
return controller.previousBackStackEntry != null
}
override fun popBack() { override fun popBack() {
navigationScope.launch { navigationScope.launch {