fix(nav): prevent back-stack crash on rapid presses

- Replace raw popBackStack() with rememberGuardedPopBackStack()
on detail, person, browse, stream, player, catalog, collection, and folder routes
- Add PlatformBackHandler to affected full-screen routes
- Enable android:enableOnBackInvokedCallback in the manifest
This commit is contained in:
WeshBg 2026-05-01 23:46:12 -04:00
parent 8a58fabfdd
commit 280f6c2aae
2 changed files with 77 additions and 26 deletions

View file

@ -6,6 +6,7 @@
<application <application
android:allowBackup="true" android:allowBackup="true"
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"

View file

@ -1005,15 +1005,21 @@ private fun MainAppContent(
} }
composable<DetailRoute> { backStackEntry -> composable<DetailRoute> { backStackEntry ->
val route = backStackEntry.toRoute<DetailRoute>() val route = backStackEntry.toRoute<DetailRoute>()
val onBack = rememberGuardedPopBackStack(
navController = navController,
backStackEntry = backStackEntry,
)
PlatformBackHandler(
enabled = true,
onBack = onBack,
)
val directorRole = stringResource(Res.string.person_role_director) val directorRole = stringResource(Res.string.person_role_director)
val writerRole = stringResource(Res.string.person_role_writer) val writerRole = stringResource(Res.string.person_role_writer)
val creatorRole = stringResource(Res.string.person_role_creator) val creatorRole = stringResource(Res.string.person_role_creator)
MetaDetailsScreen( MetaDetailsScreen(
type = route.type, type = route.type,
id = route.id, id = route.id,
onBack = { onBack = onBack,
navController.popBackStack()
},
onPlay = onPlay, onPlay = onPlay,
onPlayManually = onPlayManually, onPlayManually = onPlayManually,
onOpenMeta = { preview -> onOpenMeta = { preview ->
@ -1078,13 +1084,21 @@ private fun MainAppContent(
} }
composable<PersonDetailRoute> { backStackEntry -> composable<PersonDetailRoute> { backStackEntry ->
val route = backStackEntry.toRoute<PersonDetailRoute>() val route = backStackEntry.toRoute<PersonDetailRoute>()
val onBack = rememberGuardedPopBackStack(
navController = navController,
backStackEntry = backStackEntry,
)
PlatformBackHandler(
enabled = true,
onBack = onBack,
)
PersonDetailScreen( PersonDetailScreen(
personId = route.personId, personId = route.personId,
personName = route.personName, personName = route.personName,
initialProfilePhoto = route.personPhoto, initialProfilePhoto = route.personPhoto,
avatarTransitionKey = route.castAvatarTransitionKey, avatarTransitionKey = route.castAvatarTransitionKey,
preferCrew = route.preferCrew, preferCrew = route.preferCrew,
onBack = { navController.popBackStack() }, onBack = onBack,
onOpenMeta = { preview -> onOpenMeta = { preview ->
coroutineScope.launch { coroutineScope.launch {
val resolvedId = if (preview.id.startsWith("tmdb:")) { val resolvedId = if (preview.id.startsWith("tmdb:")) {
@ -1113,12 +1127,20 @@ private fun MainAppContent(
} }
composable<EntityBrowseRoute> { backStackEntry -> composable<EntityBrowseRoute> { backStackEntry ->
val route = backStackEntry.toRoute<EntityBrowseRoute>() val route = backStackEntry.toRoute<EntityBrowseRoute>()
val onBack = rememberGuardedPopBackStack(
navController = navController,
backStackEntry = backStackEntry,
)
PlatformBackHandler(
enabled = true,
onBack = onBack,
)
TmdbEntityBrowseScreen( TmdbEntityBrowseScreen(
entityKind = TmdbEntityKind.fromRouteValue(route.entityKind), entityKind = TmdbEntityKind.fromRouteValue(route.entityKind),
entityId = route.entityId, entityId = route.entityId,
entityName = route.entityName, entityName = route.entityName,
sourceType = route.sourceType, sourceType = route.sourceType,
onBack = { navController.popBackStack() }, onBack = onBack,
onOpenMeta = { preview -> onOpenMeta = { preview ->
coroutineScope.launch { coroutineScope.launch {
val resolvedId = if (preview.id.startsWith("tmdb:")) { val resolvedId = if (preview.id.startsWith("tmdb:")) {
@ -1145,6 +1167,15 @@ private fun MainAppContent(
} }
composable<StreamRoute> { backStackEntry -> composable<StreamRoute> { backStackEntry ->
val route = backStackEntry.toRoute<StreamRoute>() val route = backStackEntry.toRoute<StreamRoute>()
val onBack = rememberGuardedPopBackStack(
navController = navController,
backStackEntry = backStackEntry,
beforePop = { StreamsRepository.clear() },
)
PlatformBackHandler(
enabled = true,
onBack = onBack,
)
val launch = remember(route.launchId) { val launch = remember(route.launchId) {
StreamLaunchStore.get(route.launchId) StreamLaunchStore.get(route.launchId)
} }
@ -1410,10 +1441,7 @@ private fun MainAppContent(
) )
} }
}, },
onBack = { onBack = onBack,
StreamsRepository.clear()
navController.popBackStack()
},
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) )
} }
@ -1432,6 +1460,18 @@ private fun MainAppContent(
}, },
) { backStackEntry -> ) { backStackEntry ->
val route = backStackEntry.toRoute<PlayerRoute>() val route = backStackEntry.toRoute<PlayerRoute>()
val onBack = rememberGuardedPopBackStack(
navController = navController,
backStackEntry = backStackEntry,
beforePop = {
ResumePromptRepository.markPlayerExitedNormally()
PlayerLaunchStore.remove(route.launchId)
},
)
PlatformBackHandler(
enabled = true,
onBack = onBack,
)
val launch = remember(route.launchId) { PlayerLaunchStore.get(route.launchId) } val launch = remember(route.launchId) { PlayerLaunchStore.get(route.launchId) }
if (launch == null) { if (launch == null) {
LaunchedEffect(route.launchId) { LaunchedEffect(route.launchId) {
@ -1468,16 +1508,21 @@ private fun MainAppContent(
parentMetaType = launch.parentMetaType, parentMetaType = launch.parentMetaType,
initialPositionMs = launch.initialPositionMs, initialPositionMs = launch.initialPositionMs,
initialProgressFraction = launch.initialProgressFraction, initialProgressFraction = launch.initialProgressFraction,
onBack = { onBack = onBack,
ResumePromptRepository.markPlayerExitedNormally()
PlayerLaunchStore.remove(route.launchId)
navController.popBackStack()
},
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) )
} }
composable<CatalogRoute> { backStackEntry -> composable<CatalogRoute> { backStackEntry ->
val route = backStackEntry.toRoute<CatalogRoute>() val route = backStackEntry.toRoute<CatalogRoute>()
val onBack = rememberGuardedPopBackStack(
navController = navController,
backStackEntry = backStackEntry,
beforePop = { CatalogRepository.clear() },
)
PlatformBackHandler(
enabled = true,
onBack = onBack,
)
CatalogScreen( CatalogScreen(
title = route.title, title = route.title,
subtitle = route.subtitle, subtitle = route.subtitle,
@ -1486,10 +1531,7 @@ private fun MainAppContent(
catalogId = route.catalogId, catalogId = route.catalogId,
supportsPagination = route.supportsPagination, supportsPagination = route.supportsPagination,
genre = route.genre, genre = route.genre,
onBack = { onBack = onBack,
CatalogRepository.clear()
navController.popBackStack()
},
onPosterClick = { meta -> onPosterClick = { meta ->
navController.navigate(DetailRoute(type = meta.type, id = meta.id)) navController.navigate(DetailRoute(type = meta.type, id = meta.id))
}, },
@ -1618,24 +1660,32 @@ private fun MainAppContent(
} }
composable<CollectionEditorRoute> { backStackEntry -> composable<CollectionEditorRoute> { backStackEntry ->
val route = backStackEntry.toRoute<CollectionEditorRoute>() val route = backStackEntry.toRoute<CollectionEditorRoute>()
val onBack = rememberGuardedPopBackStack(
navController = navController,
backStackEntry = backStackEntry,
beforePop = { CollectionEditorRepository.clear() },
)
CollectionEditorScreen( CollectionEditorScreen(
collectionId = route.collectionId, collectionId = route.collectionId,
onBack = { onBack = onBack,
CollectionEditorRepository.clear()
navController.popBackStack()
},
) )
} }
composable<FolderDetailRoute> { backStackEntry -> composable<FolderDetailRoute> { backStackEntry ->
val route = backStackEntry.toRoute<FolderDetailRoute>() val route = backStackEntry.toRoute<FolderDetailRoute>()
val onBack = rememberGuardedPopBackStack(
navController = navController,
backStackEntry = backStackEntry,
beforePop = { FolderDetailRepository.clear() },
)
PlatformBackHandler(
enabled = true,
onBack = onBack,
)
LaunchedEffect(route.collectionId, route.folderId) { LaunchedEffect(route.collectionId, route.folderId) {
FolderDetailRepository.initialize(route.collectionId, route.folderId) FolderDetailRepository.initialize(route.collectionId, route.folderId)
} }
FolderDetailScreen( FolderDetailScreen(
onBack = { onBack = onBack,
FolderDetailRepository.clear()
navController.popBackStack()
},
onCatalogClick = onCatalogClick, onCatalogClick = onCatalogClick,
onPosterClick = { meta -> onPosterClick = { meta ->
navController.navigate(DetailRoute(type = meta.type, id = meta.id)) navController.navigate(DetailRoute(type = meta.type, id = meta.id))