From 1ad7bc4e23c4b9e8a8d26a8b4be27052a3b9f850 Mon Sep 17 00:00:00 2001 From: Barnabas Balogh Date: Thu, 4 Jun 2026 20:57:21 +0200 Subject: [PATCH] feat: add exit confirmation dialog on back navigation --- .../java/hu/bbara/purefin/tv/TvActivity.kt | 36 +++++++++++++++- .../hu/bbara/purefin/ui/screen/TvAppScreen.kt | 5 +++ .../java/hu/bbara/purefin/PurefinActivity.kt | 41 ++++++++++++++++--- .../hu/bbara/purefin/ui/screen/AppScreen.kt | 5 +++ .../purefin/ui/common/dialog/ExitAppDialog.kt | 28 +++++++++++++ 5 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 core-ui/src/main/java/hu/bbara/purefin/ui/common/dialog/ExitAppDialog.kt diff --git a/app-tv/src/main/java/hu/bbara/purefin/tv/TvActivity.kt b/app-tv/src/main/java/hu/bbara/purefin/tv/TvActivity.kt index f3f6fb7a..89af9497 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/tv/TvActivity.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/tv/TvActivity.kt @@ -3,6 +3,7 @@ package hu.bbara.purefin.tv import android.content.pm.ApplicationInfo import android.os.Bundle import android.widget.Toast +import androidx.activity.compose.BackHandler import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge @@ -50,6 +51,7 @@ import hu.bbara.purefin.core.jellyfin.JellyfinAuthInterceptor import hu.bbara.purefin.core.navigation.NavigationCommand import hu.bbara.purefin.core.navigation.NavigationManager import hu.bbara.purefin.core.navigation.Route +import hu.bbara.purefin.ui.common.dialog.ExitAppDialog import hu.bbara.purefin.navigation.LocalNavigationBackStack import hu.bbara.purefin.navigation.LocalNavigationManager import hu.bbara.purefin.ui.screen.login.TvLoginScreen @@ -147,6 +149,7 @@ class TvActivity : ComponentActivity() { updateViewModel: AppUpdateViewModel = hiltViewModel() ) { var sessionLoaded by remember { mutableStateOf(false) } + var showExitDialog by remember { mutableStateOf(false) } val isLoggedIn by userSessionRepository.isLoggedIn.collectAsStateWithLifecycle(initialValue = false) val availableUpdate by updateViewModel.availableUpdate.collectAsStateWithLifecycle() val context = LocalContext.current @@ -168,6 +171,9 @@ class TvActivity : ComponentActivity() { } if (!sessionLoaded) { + BackHandler { + showExitDialog = true + } PurefinWaitingScreen(modifier = Modifier.fillMaxSize()) } else if (isLoggedIn) { @Suppress("UNCHECKED_CAST") @@ -179,7 +185,13 @@ class TvActivity : ComponentActivity() { LaunchedEffect(navigationManager, backStack) { navigationManager.commands.collect { command -> when (command) { - NavigationCommand.Pop -> if (backStack.size > 1) backStack.removeLastOrNull() + NavigationCommand.Pop -> { + if (backStack.size > 1) { + backStack.removeLastOrNull() + } else { + showExitDialog = true + } + } is NavigationCommand.Navigate -> backStack.add(command.route) is NavigationCommand.ReplaceAll -> { backStack.clear() @@ -189,13 +201,23 @@ class TvActivity : ComponentActivity() { } } + BackHandler(enabled = backStack.size == 1) { + showExitDialog = true + } + CompositionLocalProvider( LocalNavigationManager provides navigationManager, LocalNavigationBackStack provides backStack.toList() ) { NavDisplay( backStack = backStack, - onBack = { navigationManager.pop() }, + onBack = { + if (backStack.size > 1) { + navigationManager.pop() + } else { + showExitDialog = true + } + }, modifier = Modifier.fillMaxSize().background(backgroundDark), transitionSpec = { fadeIn( @@ -229,6 +251,9 @@ class TvActivity : ComponentActivity() { ) } } else { + BackHandler { + showExitDialog = true + } TvLoginScreen() } @@ -239,6 +264,13 @@ class TvActivity : ComponentActivity() { onDecline = updateViewModel::declineUpdate ) } + + if (showExitDialog) { + ExitAppDialog( + onCloseConfirmed = { finishAndRemoveTask() }, + onDismiss = { showExitDialog = false } + ) + } } @Composable diff --git a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/TvAppScreen.kt b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/TvAppScreen.kt index d9be787f..e56a29cd 100644 --- a/app-tv/src/main/java/hu/bbara/purefin/ui/screen/TvAppScreen.kt +++ b/app-tv/src/main/java/hu/bbara/purefin/ui/screen/TvAppScreen.kt @@ -1,5 +1,6 @@ package hu.bbara.purefin.ui.screen +import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Home @@ -102,6 +103,10 @@ fun TvAppScreen( } } + BackHandler(enabled = backStack.size == 1) { + navigationManager.pop() + } + TvNavigationDrawer( destinations = destinations, selectedDestination = selectedDestination, diff --git a/app/src/main/java/hu/bbara/purefin/PurefinActivity.kt b/app/src/main/java/hu/bbara/purefin/PurefinActivity.kt index a453b0db..3cd83ce0 100644 --- a/app/src/main/java/hu/bbara/purefin/PurefinActivity.kt +++ b/app/src/main/java/hu/bbara/purefin/PurefinActivity.kt @@ -2,6 +2,7 @@ package hu.bbara.purefin import android.content.pm.ApplicationInfo import android.os.Bundle +import androidx.activity.compose.BackHandler import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge @@ -51,6 +52,7 @@ import hu.bbara.purefin.navigation.LocalSharedTransitionScope import hu.bbara.purefin.core.navigation.NavigationCommand import hu.bbara.purefin.core.navigation.NavigationManager import hu.bbara.purefin.core.navigation.Route +import hu.bbara.purefin.ui.common.dialog.ExitAppDialog import hu.bbara.purefin.ui.screen.waiting.PurefinWaitingScreen import hu.bbara.purefin.ui.screen.login.LoginScreen import hu.bbara.purefin.ui.theme.AppTheme @@ -146,6 +148,7 @@ class PurefinActivity : ComponentActivity() { navigationManager: NavigationManager ) { var sessionLoaded by remember { mutableStateOf(false) } + var showExitDialog by remember { mutableStateOf(false) } val isLoggedIn by userSessionRepository.isLoggedIn.collectAsStateWithLifecycle(initialValue = false) LaunchedEffect(Unit) { @@ -155,11 +158,11 @@ class PurefinActivity : ComponentActivity() { } if (!sessionLoaded) { + BackHandler { + showExitDialog = true + } PurefinWaitingScreen(modifier = Modifier.fillMaxSize()) - return - } - - if (isLoggedIn) { + } else if (isLoggedIn) { @Suppress("UNCHECKED_CAST") val backStack = rememberNavBackStack(Route.Home) as NavBackStack var homeMediaSharedBoundsKey by remember { mutableStateOf(null) } @@ -171,7 +174,13 @@ class PurefinActivity : ComponentActivity() { LaunchedEffect(navigationManager, backStack) { navigationManager.commands.collect { command -> when (command) { - NavigationCommand.Pop -> if (backStack.size > 1) backStack.removeLastOrNull() + NavigationCommand.Pop -> { + if (backStack.size > 1) { + backStack.removeLastOrNull() + } else { + showExitDialog = true + } + } is NavigationCommand.Navigate -> backStack.add(command.route) is NavigationCommand.ReplaceAll -> { backStack.clear() @@ -181,6 +190,10 @@ class PurefinActivity : ComponentActivity() { } } + BackHandler(enabled = backStack.size == 1) { + showExitDialog = true + } + SharedTransitionLayout { CompositionLocalProvider( LocalNavigationManager provides navigationManager, @@ -193,7 +206,13 @@ class PurefinActivity : ComponentActivity() { ) { NavDisplay( backStack = backStack, - onBack = { navigationManager.pop() }, + onBack = { + if (backStack.size > 1) { + navigationManager.pop() + } else { + showExitDialog = true + } + }, modifier = Modifier .fillMaxSize() .background(backgroundDark) @@ -232,9 +251,19 @@ class PurefinActivity : ComponentActivity() { } } } else { + BackHandler { + showExitDialog = true + } LoginScreen( modifier = Modifier.semantics { testTagsAsResourceId = true } ) } + + if (showExitDialog) { + ExitAppDialog( + onCloseConfirmed = { finishAndRemoveTask() }, + onDismiss = { showExitDialog = false } + ) + } } } diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt index 948b5661..805c17ff 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/AppScreen.kt @@ -1,5 +1,6 @@ package hu.bbara.purefin.ui.screen +import androidx.activity.compose.BackHandler import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -138,6 +139,10 @@ fun AppScreen( } } + BackHandler(enabled = backStack.size == 1) { + navigationManager.pop() + } + NavDisplay( backStack = backStack, onBack = { diff --git a/core-ui/src/main/java/hu/bbara/purefin/ui/common/dialog/ExitAppDialog.kt b/core-ui/src/main/java/hu/bbara/purefin/ui/common/dialog/ExitAppDialog.kt new file mode 100644 index 00000000..0b1197c5 --- /dev/null +++ b/core-ui/src/main/java/hu/bbara/purefin/ui/common/dialog/ExitAppDialog.kt @@ -0,0 +1,28 @@ +package hu.bbara.purefin.ui.common.dialog + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable + +@Composable +fun ExitAppDialog( + onCloseConfirmed: () -> Unit, + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Close app?") }, + text = { Text("Are you sure?") }, + confirmButton = { + TextButton(onClick = onCloseConfirmed) { + Text("Close") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + } + ) +}