diff --git a/app/src/test/java/org/justinvisser/wolfskin/tv/FirstSignInNavigationTest.kt b/app/src/test/java/org/justinvisser/wolfskin/tv/FirstSignInNavigationTest.kt index b0102a5b..63442159 100644 --- a/app/src/test/java/org/justinvisser/wolfskin/tv/FirstSignInNavigationTest.kt +++ b/app/src/test/java/org/justinvisser/wolfskin/tv/FirstSignInNavigationTest.kt @@ -105,9 +105,17 @@ class FirstSignInNavigationTest { assertEquals(6, session.state!!.view.library!!.source.size) assertFalse(session.state!!.person.hasAnime) rule.onNodeWithText("Shows").performClick() - rule.waitUntil(10_000) { - Shadows.shadowOf(Looper.getMainLooper()).idle() - session.state!!.view.library?.source?.size == 29 + try { + rule.waitUntil(10_000) { + Shadows.shadowOf(Looper.getMainLooper()).idle() + session.state!!.view.library?.source?.size == 29 + } + } catch (timeout: ComposeTimeoutException) { + val shell = session.state!! + throw AssertionError("Active=${navigation.backStack.last()}, shell=${shell.route}, " + + "movies=${shell.rememberedView("movies").library?.source?.size}, " + + "shows=${shell.rememberedView("shows").library?.source?.size}/" + + "${shell.rememberedView("shows").library?.status}", timeout) } rule.onNodeWithTag("library-grid").assertIsDisplayed() rule.onAllNodesWithText("Mindel Shows title 0")[0].assertIsDisplayed() diff --git a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/WolfskinApp.kt b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/WolfskinApp.kt index cfc1617b..52a68beb 100644 --- a/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/WolfskinApp.kt +++ b/app/src/wolfskin/java/org/justinvisser/wolfskin/tv/WolfskinApp.kt @@ -285,11 +285,14 @@ fun WolfskinApp( is Destination.MediaItem -> if (destination.type == org.jellyfin.sdk.model.api.BaseItemKind.PERSON) personRoute(destination.itemId.toString()) else titleRoute(destination.itemId.toString()) else -> "pushed" } - LaunchedEffect(destination, state) { if (!destination.fullScreen) state.enterDestination(route) } + val isCurrent = navigationManager.backStack.lastOrNull() == destination + LaunchedEffect(destination, state, isCurrent) { + if (isCurrent && !destination.fullScreen) state.enterDestination(route) + } if (destination.fullScreen) { DestinationContent(destination, preferences, viewModel::clearBackdrop, Modifier.fillMaxSize()) } else { - BackHandler { state.key(org.justinvisser.wolfskin.design.ShellKey.Back) } + BackHandler(enabled = isCurrent) { state.key(org.justinvisser.wolfskin.design.ShellKey.Back) } val upstream = resolveUpstreamDestination(destination, WolfskinScreenPreference.enabled, viewModel.libraryDestinations) if (upstream == null && (route.startsWith("title/") || route.startsWith("person/"))) { val jellyfinIdentity = destination is Destination.MediaItem || android.net.Uri.decode(route.substringAfter('/')).toUUIDOrNull() != null diff --git a/docker/README.md b/docker/README.md index 5e2b488e..bd1667dc 100644 --- a/docker/README.md +++ b/docker/README.md @@ -485,6 +485,8 @@ Cached `NavDisplay` entries read the current shell and Discover composition loca not the temporary launch shell captured when the entry was first created. Otherwise the screen can retain skeleton people, disconnected tab actions and an open picker even after the real session is ready. +Only the current navigation entry may set the shared route or handle Back; +outgoing entries can remain composed during a transition. Artwork uses a fresh bounded Wolfskin loader, not the upstream loader's authenticated fetcher. The app's network interceptor removes Authorization from