UI: disable triggering drawer via gesture; enable alert dialog on back navigation inside conversation and benchmark

This commit is contained in:
Han Yin 2025-04-11 21:00:02 -07:00
parent a7ae8b7ce0
commit 648b97818e
3 changed files with 229 additions and 134 deletions

View File

@ -2,30 +2,40 @@ package com.example.llama.revamp
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerValue import androidx.compose.material3.DrawerValue
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.example.llama.revamp.engine.InferenceEngine import com.example.llama.revamp.engine.InferenceEngine
import com.example.llama.revamp.navigation.AppDestinations import com.example.llama.revamp.navigation.AppDestinations
import com.example.llama.revamp.navigation.NavigationActions import com.example.llama.revamp.navigation.NavigationActions
import com.example.llama.revamp.ui.components.AppNavigationDrawer
import com.example.llama.revamp.ui.components.UnloadModelConfirmationDialog import com.example.llama.revamp.ui.components.UnloadModelConfirmationDialog
import com.example.llama.revamp.ui.screens.BenchmarkScreen import com.example.llama.revamp.ui.screens.BenchmarkScreen
import com.example.llama.revamp.ui.screens.ConversationScreen import com.example.llama.revamp.ui.screens.ConversationScreen
@ -71,6 +81,11 @@ fun AppContent() {
val engineState by viewModel.engineState.collectAsState() val engineState by viewModel.engineState.collectAsState()
// Track if model is loaded for gesture handling
val isModelLoaded = remember(engineState) {
viewModel.isModelLoaded()
}
val navigationActions = remember(navController) { val navigationActions = remember(navController) {
NavigationActions(navController) NavigationActions(navController)
} }
@ -79,6 +94,65 @@ fun AppContent() {
var showUnloadDialog by remember { mutableStateOf(false) } var showUnloadDialog by remember { mutableStateOf(false) }
var pendingNavigation by remember { mutableStateOf<(() -> Unit)?>(null) } var pendingNavigation by remember { mutableStateOf<(() -> Unit)?>(null) }
// Get current route
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute by remember {
derivedStateOf { navBackStackEntry?.destination?.route }
}
// Determine if current route requires model unloading
val routeNeedsModelUnloading by remember(currentRoute) {
derivedStateOf {
currentRoute == AppDestinations.CONVERSATION_ROUTE ||
currentRoute == AppDestinations.BENCHMARK_ROUTE ||
currentRoute == AppDestinations.MODE_SELECTION_ROUTE
}
}
// Get local back dispatcher
val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
val lifecycleOwner = LocalLifecycleOwner.current
// Helper function to handle back press with model unloading check
val handleBackWithModelCheck = {
if (viewModel.isModelLoaded()) {
showUnloadDialog = true
pendingNavigation = { navController.popBackStack() }
true // Mark as handled
} else {
navController.popBackStack()
true // Mark as handled
}
}
// Register a system back handler for screens that need unload confirmation
DisposableEffect(lifecycleOwner, backDispatcher, currentRoute, isModelLoaded) {
val callback = object : OnBackPressedCallback(
// Only enable for screens that need model unloading confirmation
routeNeedsModelUnloading && isModelLoaded
) {
override fun handleOnBackPressed() {
handleBackWithModelCheck()
}
}
backDispatcher?.addCallback(lifecycleOwner, callback)
// Remove the callback when the effect leaves the composition
onDispose {
callback.remove()
}
}
// Compose BackHandler for added protection (this handles Compose-based back navigation)
BackHandler(
enabled = routeNeedsModelUnloading &&
isModelLoaded &&
drawerState.currentValue == DrawerValue.Closed
) {
handleBackWithModelCheck()
}
// Observe back button // Observe back button
LaunchedEffect(navController) { LaunchedEffect(navController) {
navController.addOnDestinationChangedListener { _, destination, _ -> navController.addOnDestinationChangedListener { _, destination, _ ->
@ -94,7 +168,12 @@ fun AppContent() {
} }
} }
// Main Content // Main Content with navigation drawer wrapper
AppNavigationDrawer(
drawerState = drawerState,
navigationActions = navigationActions,
modelLoaded = isModelLoaded
) {
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = AppDestinations.MODEL_SELECTION_ROUTE startDestination = AppDestinations.MODEL_SELECTION_ROUTE
@ -129,12 +208,7 @@ fun AppContent() {
}, },
onBackPressed = { onBackPressed = {
// Need to unload model before going back // Need to unload model before going back
if (viewModel.isModelLoaded()) { handleBackWithModelCheck()
showUnloadDialog = true
pendingNavigation = { navController.popBackStack() }
} else {
navController.popBackStack()
}
}, },
drawerState = drawerState, drawerState = drawerState,
navigationActions = navigationActions navigationActions = navigationActions
@ -146,12 +220,7 @@ fun AppContent() {
ConversationScreen( ConversationScreen(
onBackPressed = { onBackPressed = {
// Need to unload model before going back // Need to unload model before going back
if (viewModel.isModelLoaded()) { handleBackWithModelCheck()
showUnloadDialog = true
pendingNavigation = { navController.popBackStack() }
} else {
navController.popBackStack()
}
}, },
drawerState = drawerState, drawerState = drawerState,
navigationActions = navigationActions, navigationActions = navigationActions,
@ -164,12 +233,7 @@ fun AppContent() {
BenchmarkScreen( BenchmarkScreen(
onBackPressed = { onBackPressed = {
// Need to unload model before going back // Need to unload model before going back
if (viewModel.isModelLoaded()) { handleBackWithModelCheck()
showUnloadDialog = true
pendingNavigation = { navController.popBackStack() }
} else {
navController.popBackStack()
}
}, },
onRerunPressed = { onRerunPressed = {
viewModel.rerunBenchmark() viewModel.rerunBenchmark()
@ -208,6 +272,7 @@ fun AppContent() {
) )
} }
} }
}
// Model unload confirmation dialog // Model unload confirmation dialog
if (showUnloadDialog) { if (showUnloadDialog) {

View File

@ -62,10 +62,6 @@ fun AppScaffold(
// Formatted memory usage // Formatted memory usage
val memoryText = "${memoryUsage.availableGb}GB available" val memoryText = "${memoryUsage.availableGb}GB available"
AppNavigationDrawer(
drawerState = drawerState,
navigationActions = navigationActions
) {
Scaffold( Scaffold(
topBar = { topBar = {
SystemStatusTopBar( SystemStatusTopBar(
@ -85,5 +81,4 @@ fun AppScaffold(
}, },
content = content content = content
) )
}
} }

View File

@ -1,5 +1,6 @@
package com.example.llama.revamp.ui.components package com.example.llama.revamp.ui.components
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -7,10 +8,13 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Divider
import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerState
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -19,9 +23,14 @@ import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.example.llama.revamp.navigation.NavigationActions import com.example.llama.revamp.navigation.NavigationActions
@ -29,19 +38,45 @@ import kotlinx.coroutines.launch
/** /**
* App navigation drawer that provides access to different sections of the app. * App navigation drawer that provides access to different sections of the app.
* Gesture opening is disabled when a model is loaded to prevent accidental navigation,
* but gesture dismissal is always enabled.
*/ */
@Composable @Composable
fun AppNavigationDrawer( fun AppNavigationDrawer(
drawerState: DrawerState, drawerState: DrawerState,
navigationActions: NavigationActions, navigationActions: NavigationActions,
modelLoaded: Boolean = false,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val configuration = LocalConfiguration.current
// Calculate drawer width (60% of screen width)
val drawerWidth = (configuration.screenWidthDp * 0.6).dp
// Determine if gestures should be enabled
// Always enable when drawer is open (to allow dismissal)
// Only enable when model is not loaded (to prevent accidental opening)
val gesturesEnabled by remember(drawerState.currentValue, modelLoaded) {
derivedStateOf {
drawerState.currentValue == DrawerValue.Open || !modelLoaded
}
}
// Handle back button to close drawer if open
BackHandler(enabled = drawerState.currentValue == DrawerValue.Open) {
coroutineScope.launch {
drawerState.close()
}
}
ModalNavigationDrawer( ModalNavigationDrawer(
drawerState = drawerState, drawerState = drawerState,
gesturesEnabled = gesturesEnabled,
drawerContent = { drawerContent = {
ModalDrawerSheet { ModalDrawerSheet(
modifier = Modifier.width(drawerWidth)
) {
DrawerContent( DrawerContent(
onHomeClicked = { onHomeClicked = {
coroutineScope.launch { coroutineScope.launch {