feat: add exit confirmation dialog on back navigation

This commit is contained in:
2026-06-04 20:57:21 +02:00
parent e72c421294
commit 1ad7bc4e23
5 changed files with 107 additions and 8 deletions

View File

@@ -3,6 +3,7 @@ package hu.bbara.purefin.tv
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge 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.NavigationCommand
import hu.bbara.purefin.core.navigation.NavigationManager import hu.bbara.purefin.core.navigation.NavigationManager
import hu.bbara.purefin.core.navigation.Route 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.LocalNavigationBackStack
import hu.bbara.purefin.navigation.LocalNavigationManager import hu.bbara.purefin.navigation.LocalNavigationManager
import hu.bbara.purefin.ui.screen.login.TvLoginScreen import hu.bbara.purefin.ui.screen.login.TvLoginScreen
@@ -147,6 +149,7 @@ class TvActivity : ComponentActivity() {
updateViewModel: AppUpdateViewModel = hiltViewModel() updateViewModel: AppUpdateViewModel = hiltViewModel()
) { ) {
var sessionLoaded by remember { mutableStateOf(false) } var sessionLoaded by remember { mutableStateOf(false) }
var showExitDialog by remember { mutableStateOf(false) }
val isLoggedIn by userSessionRepository.isLoggedIn.collectAsStateWithLifecycle(initialValue = false) val isLoggedIn by userSessionRepository.isLoggedIn.collectAsStateWithLifecycle(initialValue = false)
val availableUpdate by updateViewModel.availableUpdate.collectAsStateWithLifecycle() val availableUpdate by updateViewModel.availableUpdate.collectAsStateWithLifecycle()
val context = LocalContext.current val context = LocalContext.current
@@ -168,6 +171,9 @@ class TvActivity : ComponentActivity() {
} }
if (!sessionLoaded) { if (!sessionLoaded) {
BackHandler {
showExitDialog = true
}
PurefinWaitingScreen(modifier = Modifier.fillMaxSize()) PurefinWaitingScreen(modifier = Modifier.fillMaxSize())
} else if (isLoggedIn) { } else if (isLoggedIn) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@@ -179,7 +185,13 @@ class TvActivity : ComponentActivity() {
LaunchedEffect(navigationManager, backStack) { LaunchedEffect(navigationManager, backStack) {
navigationManager.commands.collect { command -> navigationManager.commands.collect { command ->
when (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.Navigate -> backStack.add(command.route)
is NavigationCommand.ReplaceAll -> { is NavigationCommand.ReplaceAll -> {
backStack.clear() backStack.clear()
@@ -189,13 +201,23 @@ class TvActivity : ComponentActivity() {
} }
} }
BackHandler(enabled = backStack.size == 1) {
showExitDialog = true
}
CompositionLocalProvider( CompositionLocalProvider(
LocalNavigationManager provides navigationManager, LocalNavigationManager provides navigationManager,
LocalNavigationBackStack provides backStack.toList() LocalNavigationBackStack provides backStack.toList()
) { ) {
NavDisplay( NavDisplay(
backStack = backStack, backStack = backStack,
onBack = { navigationManager.pop() }, onBack = {
if (backStack.size > 1) {
navigationManager.pop()
} else {
showExitDialog = true
}
},
modifier = Modifier.fillMaxSize().background(backgroundDark), modifier = Modifier.fillMaxSize().background(backgroundDark),
transitionSpec = { transitionSpec = {
fadeIn( fadeIn(
@@ -229,6 +251,9 @@ class TvActivity : ComponentActivity() {
) )
} }
} else { } else {
BackHandler {
showExitDialog = true
}
TvLoginScreen() TvLoginScreen()
} }
@@ -239,6 +264,13 @@ class TvActivity : ComponentActivity() {
onDecline = updateViewModel::declineUpdate onDecline = updateViewModel::declineUpdate
) )
} }
if (showExitDialog) {
ExitAppDialog(
onCloseConfirmed = { finishAndRemoveTask() },
onDismiss = { showExitDialog = false }
)
}
} }
@Composable @Composable

View File

@@ -1,5 +1,6 @@
package hu.bbara.purefin.ui.screen package hu.bbara.purefin.ui.screen
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Home import androidx.compose.material.icons.outlined.Home
@@ -102,6 +103,10 @@ fun TvAppScreen(
} }
} }
BackHandler(enabled = backStack.size == 1) {
navigationManager.pop()
}
TvNavigationDrawer( TvNavigationDrawer(
destinations = destinations, destinations = destinations,
selectedDestination = selectedDestination, selectedDestination = selectedDestination,

View File

@@ -2,6 +2,7 @@ package hu.bbara.purefin
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.os.Bundle import android.os.Bundle
import androidx.activity.compose.BackHandler
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge 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.NavigationCommand
import hu.bbara.purefin.core.navigation.NavigationManager import hu.bbara.purefin.core.navigation.NavigationManager
import hu.bbara.purefin.core.navigation.Route 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.waiting.PurefinWaitingScreen
import hu.bbara.purefin.ui.screen.login.LoginScreen import hu.bbara.purefin.ui.screen.login.LoginScreen
import hu.bbara.purefin.ui.theme.AppTheme import hu.bbara.purefin.ui.theme.AppTheme
@@ -146,6 +148,7 @@ class PurefinActivity : ComponentActivity() {
navigationManager: NavigationManager navigationManager: NavigationManager
) { ) {
var sessionLoaded by remember { mutableStateOf(false) } var sessionLoaded by remember { mutableStateOf(false) }
var showExitDialog by remember { mutableStateOf(false) }
val isLoggedIn by userSessionRepository.isLoggedIn.collectAsStateWithLifecycle(initialValue = false) val isLoggedIn by userSessionRepository.isLoggedIn.collectAsStateWithLifecycle(initialValue = false)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@@ -155,11 +158,11 @@ class PurefinActivity : ComponentActivity() {
} }
if (!sessionLoaded) { if (!sessionLoaded) {
BackHandler {
showExitDialog = true
}
PurefinWaitingScreen(modifier = Modifier.fillMaxSize()) PurefinWaitingScreen(modifier = Modifier.fillMaxSize())
return } else if (isLoggedIn) {
}
if (isLoggedIn) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val backStack = rememberNavBackStack(Route.Home) as NavBackStack<Route> val backStack = rememberNavBackStack(Route.Home) as NavBackStack<Route>
var homeMediaSharedBoundsKey by remember { mutableStateOf<String?>(null) } var homeMediaSharedBoundsKey by remember { mutableStateOf<String?>(null) }
@@ -171,7 +174,13 @@ class PurefinActivity : ComponentActivity() {
LaunchedEffect(navigationManager, backStack) { LaunchedEffect(navigationManager, backStack) {
navigationManager.commands.collect { command -> navigationManager.commands.collect { command ->
when (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.Navigate -> backStack.add(command.route)
is NavigationCommand.ReplaceAll -> { is NavigationCommand.ReplaceAll -> {
backStack.clear() backStack.clear()
@@ -181,6 +190,10 @@ class PurefinActivity : ComponentActivity() {
} }
} }
BackHandler(enabled = backStack.size == 1) {
showExitDialog = true
}
SharedTransitionLayout { SharedTransitionLayout {
CompositionLocalProvider( CompositionLocalProvider(
LocalNavigationManager provides navigationManager, LocalNavigationManager provides navigationManager,
@@ -193,7 +206,13 @@ class PurefinActivity : ComponentActivity() {
) { ) {
NavDisplay( NavDisplay(
backStack = backStack, backStack = backStack,
onBack = { navigationManager.pop() }, onBack = {
if (backStack.size > 1) {
navigationManager.pop()
} else {
showExitDialog = true
}
},
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(backgroundDark) .background(backgroundDark)
@@ -232,9 +251,19 @@ class PurefinActivity : ComponentActivity() {
} }
} }
} else { } else {
BackHandler {
showExitDialog = true
}
LoginScreen( LoginScreen(
modifier = Modifier.semantics { testTagsAsResourceId = true } modifier = Modifier.semantics { testTagsAsResourceId = true }
) )
} }
if (showExitDialog) {
ExitAppDialog(
onCloseConfirmed = { finishAndRemoveTask() },
onDismiss = { showExitDialog = false }
)
}
} }
} }

View File

@@ -1,5 +1,6 @@
package hu.bbara.purefin.ui.screen package hu.bbara.purefin.ui.screen
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
@@ -138,6 +139,10 @@ fun AppScreen(
} }
} }
BackHandler(enabled = backStack.size == 1) {
navigationManager.pop()
}
NavDisplay( NavDisplay(
backStack = backStack, backStack = backStack,
onBack = { onBack = {

View File

@@ -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")
}
}
)
}