From 22850632ec1e355cf096f35b56709e70af833c23 Mon Sep 17 00:00:00 2001 From: justinzeus Date: Fri, 6 Nov 2026 12:03:20 +0100 Subject: [PATCH] Restrict shell route and Back handling to the current entry --- .../wolfskin/tv/FirstSignInNavigationTest.kt | 14 +++++++++++--- .../org/justinvisser/wolfskin/tv/WolfskinApp.kt | 7 +++++-- docker/README.md | 2 ++ 3 files changed, 18 insertions(+), 5 deletions(-) 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