From 87c814501e8bbb0c8fb7ff9d807efc987224c0af Mon Sep 17 00:00:00 2001 From: JojoIV <jonas.broeckmann@gmx.de> Date: Tue, 14 May 2024 19:37:02 +0200 Subject: [PATCH] Moved user search to top app bar --- .../kaffeekasse/ui/kaffeekasse/ManualBill.kt | 53 +++--- .../ui/kaffeekasse/components/ItemSearch.kt | 42 ----- .../kaffeekasse/ui/login/Login.kt | 130 +++----------- .../kaffeekasse/ui/login/UserSelection.kt | 162 +++++++++++++++--- .../kaffeekasse/ui/navigation/AppScreens.kt | 8 +- .../kaffeekasse/ui/util/TopBarSearchField.kt | 91 ++++++++++ 6 files changed, 281 insertions(+), 205 deletions(-) delete mode 100644 app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/components/ItemSearch.kt create mode 100644 app/src/main/java/net/novagamestudios/kaffeekasse/ui/util/TopBarSearchField.kt diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/ManualBill.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/ManualBill.kt index 1da6dce..9007ea9 100644 --- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/ManualBill.kt +++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/ManualBill.kt @@ -14,7 +14,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Receipt -import androidx.compose.material.icons.filled.Search import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -41,6 +40,7 @@ import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.currentOrThrow import net.novagamestudios.common_utils.Logger import net.novagamestudios.common_utils.compose.components.CircularProgressIndicator +import net.novagamestudios.common_utils.compose.components.RowCenter import net.novagamestudios.common_utils.compose.tabIndicatorOffset import net.novagamestudios.kaffeekasse.KaffeekasseModule.Companion.kaffeekasseCartProvider import net.novagamestudios.kaffeekasse.model.kaffeekasse.Item @@ -57,12 +57,14 @@ import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.Checkout import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.CheckoutState import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.CustomItems import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.CustomItemsState -import net.novagamestudios.kaffeekasse.ui.kaffeekasse.components.ItemSearchField import net.novagamestudios.kaffeekasse.ui.login.LogoutTopBarAction import net.novagamestudios.kaffeekasse.ui.navigation.AppSubpageTitle import net.novagamestudios.kaffeekasse.ui.navigation.KaffeekasseNavigation import net.novagamestudios.kaffeekasse.ui.util.FailureRetryScreen import net.novagamestudios.kaffeekasse.ui.util.RichDataContent +import net.novagamestudios.kaffeekasse.ui.util.TopBarSearchAction +import net.novagamestudios.kaffeekasse.ui.util.TopBarSearchField +import net.novagamestudios.kaffeekasse.ui.util.TopBarSearchFieldState import net.novagamestudios.kaffeekasse.ui.util.navigation.BackNavigationHandler import net.novagamestudios.kaffeekasse.ui.util.navigation.BackNavigationHandler.Companion.then import net.novagamestudios.kaffeekasse.ui.util.screenmodel.ScreenModelFactory @@ -80,28 +82,29 @@ class ManualBillScreenModel private constructor( else -> null } - private var _showSearch by mutableStateOf(false) - var showSearch - get() = _showSearch - set(value) { - _showSearch = value - if (!value) searchQuery = "" - } - var searchQuery by mutableStateOf("") +// private var _showSearch by mutableStateOf(false) +// var showSearch +// get() = _showSearch +// set(value) { +// _showSearch = value +// if (!value) searchQuery = "" +// } +// var searchQuery by mutableStateOf("") + val searchState = TopBarSearchFieldState() val stock = kaffeekasse.stock.collectAsRichStateHere() val itemGroups get() = stock.dataOrNull?.itemGroups ?: emptyList() fun Sequence<Item>.filtered(): Sequence<Item> { - val query = searchQuery.takeIf { showSearch }?.takeUnless { it.isBlank() } + val query = searchState.queryOrNull return if (query != null) filter { item -> item.cleanFullName.contains(query, ignoreCase = true) } else this } val searchItems by derivedStateOf { - if (showSearch) CategorizedItemsState( + if (searchState.show) CategorizedItemsState( itemGroups .asSequence() .flatMap { it.items } @@ -168,21 +171,11 @@ class ManualBillScreenModel private constructor( categorizedItemsByGroup.getOrNull(currentGroupIndex) } return remember(categorizedItems, cart) { - categorizedItems?.backNavigation then SearchBackNavigationHandler() then CartBackNavigationHandler(cart) + categorizedItems?.backNavigation then searchState.backNavigation then CartBackNavigationHandler(cart) } } } - private inner class SearchBackNavigationHandler : BackNavigationHandler { - override fun canNavigateBack() = showSearch - override fun onNavigateBack(): Boolean { - if (showSearch) { - showSearch = false - return true - } - return false - } - } private class CartBackNavigationHandler( private val cart: MutableCart @@ -341,26 +334,20 @@ private fun ItemGroupTab( fun ManualBillTopBarTitle( model: ManualBillScreenModel, navigator: Navigator -) { - if (model.showSearch) { - ItemSearchField( - query = model.searchQuery, - onQueryChange = { model.searchQuery = it }, - ) - } else { +) = RowCenter { + if (!model.searchState.show) { val name = model.accountName if (name != null) AppSubpageTitle("$name") else AppModuleSelection() } + TopBarSearchField(model.searchState) } @Composable fun ManualBillTopBarActions( model: ManualBillScreenModel ) { - if (!model.showSearch) IconButton(onClick = { model.showSearch = !model.showSearch }) { - Icon(Icons.Default.Search, "Suchen") - } + TopBarSearchAction(model.searchState) val navigator = LocalNavigator.currentOrThrow IconButton(onClick = { navigator.push(KaffeekasseNavigation.AccountScreen(model.session)) diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/components/ItemSearch.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/components/ItemSearch.kt deleted file mode 100644 index 5f769bc..0000000 --- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/kaffeekasse/components/ItemSearch.kt +++ /dev/null @@ -1,42 +0,0 @@ -package net.novagamestudios.kaffeekasse.ui.kaffeekasse.components - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Clear -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.SearchBarDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester - - -@Composable -fun ItemSearchField( - query: String, - onQueryChange: (String) -> Unit, - modifier: Modifier = Modifier -) { - val focusRequester = remember { FocusRequester() } - LaunchedEffect(Unit) { - focusRequester.requestFocus() - } - SearchBarDefaults.InputField( - query = query, - onQueryChange = onQueryChange, - onSearch = { }, - expanded = false, - onExpandedChange = { }, - modifier = modifier.focusRequester(focusRequester), - placeholder = { Text("Suchen") }, - trailingIcon = { - if (query.isNotEmpty()) IconButton(onClick = { onQueryChange("") }) { - Icon(Icons.Default.Clear, "Clear") - } - }, - ) -} - diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/Login.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/Login.kt index 75dc802..c257b54 100644 --- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/Login.kt +++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/Login.kt @@ -1,22 +1,14 @@ package net.novagamestudios.kaffeekasse.ui.login -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Logout -import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.Key -import androidx.compose.material.icons.filled.PhonelinkErase import androidx.compose.material.icons.filled.ScreenLockLandscape import androidx.compose.material.icons.filled.SmartScreen import androidx.compose.material3.AlertDialog @@ -25,7 +17,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -33,13 +24,11 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp @@ -49,22 +38,20 @@ import cafe.adriel.voyager.navigator.Navigator import kotlinx.coroutines.launch import net.novagamestudios.common_utils.Logger import net.novagamestudios.common_utils.compose.components.CircularLoadingBox -import net.novagamestudios.kaffeekasse.R import net.novagamestudios.kaffeekasse.app -import net.novagamestudios.kaffeekasse.data.cleanName import net.novagamestudios.kaffeekasse.model.credentials.DeviceCredentials import net.novagamestudios.kaffeekasse.model.credentials.isValid -import net.novagamestudios.kaffeekasse.model.session.Device import net.novagamestudios.kaffeekasse.model.session.Session -import net.novagamestudios.kaffeekasse.model.session.deviceOrNull import net.novagamestudios.kaffeekasse.repositories.LoginRepository import net.novagamestudios.kaffeekasse.repositories.RepositoryProvider import net.novagamestudios.kaffeekasse.repositories.i11.PortalRepository import net.novagamestudios.kaffeekasse.ui.AppInfoTopBarAction import net.novagamestudios.kaffeekasse.ui.FullscreenIconButton +import net.novagamestudios.kaffeekasse.ui.login.UserSelectionScreenModel.Companion.backNavigationHandler import net.novagamestudios.kaffeekasse.ui.navigation.LoginNavigation -import net.novagamestudios.kaffeekasse.ui.util.screenmodel.ScreenModelFactory +import net.novagamestudios.kaffeekasse.ui.util.navigation.BackNavigationHandler import net.novagamestudios.kaffeekasse.ui.util.rememberSerializableState +import net.novagamestudios.kaffeekasse.ui.util.screenmodel.ScreenModelFactory import net.novagamestudios.kaffeekasse.ui.util.screenmodel.collectAsStateHere @@ -87,12 +74,6 @@ class LoginScreenModel private constructor( } } - fun logoutDevice() { - screenModelScope.launch { - loginRepository.logoutDevice() - } - } - companion object : ScreenModelFactory<LoginNavigation.Companion, LoginScreenModel> { context (RepositoryProvider) override fun create(screen: LoginNavigation.Companion) = LoginScreenModel( @@ -205,107 +186,46 @@ private fun LoginDeviceForm( @Composable fun LoginTopBarTitle( - model: LoginScreenModel + model: LoginScreenModel, + navigator: Navigator ) { - model.session.deviceOrNull?.let { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Image( - painterResource(R.drawable.logo_edited), - "Kaffeekasse", - Modifier.size(32.dp) - ) - DeviceInfo(it, Modifier.padding(16.dp)) + when (val screen = navigator.lastItem) { + is LoginNavigation.UserSelectionScreen -> { + UserSelectionTopBarTitle(screen.model, navigator) } } } -context (RowScope) @Composable fun LoginTopBarActions( - model: LoginScreenModel + model: LoginScreenModel, + navigator: Navigator ) { - val session = model.session - if (session !is Session.WithRealUser) { - when (session) { - is Session.WithDevice -> { - var confirmLogout by remember { mutableStateOf(false) } - IconButton( - onClick = { confirmLogout = true }, - enabled = !model.isLoading - ) { - Icon(Icons.Default.PhonelinkErase, "Gerät ausloggen") - } - if (confirmLogout) ConfirmLogoutDeviceDialog( - onDismiss = { confirmLogout = false }, - onConfirm = { model.logoutDevice() } - ) - } - else -> { - IconButton( - onClick = { model.showLoginDeviceDialog = true }, - enabled = !model.isLoading - ) { - Icon(Icons.Default.ScreenLockLandscape, "Gerät einloggen") - } + when (val screen = navigator.lastItem) { + is LoginNavigation.FormScreen -> { + AppInfoTopBarAction() + IconButton( + onClick = { model.showLoginDeviceDialog = true }, + enabled = !model.isLoading + ) { + Icon(Icons.Default.ScreenLockLandscape, "Gerät einloggen") } } + is LoginNavigation.UserSelectionScreen -> { + UserSelectionTopBarActions(screen.model, navigator) + } } - AppInfoTopBarAction() FullscreenIconButton() } - - - @Composable -private fun DeviceInfo( - device: Device, - modifier: Modifier = Modifier -) = Row( - modifier, - verticalAlignment = Alignment.CenterVertically -) { - Text(device.name ?: "Unbekanntes Gerät") - device.knownItemGroup?.let { - ProvideTextStyle(MaterialTheme.typography.titleMedium) { - Spacer(Modifier.width(16.dp)) - Icon(Icons.Default.ChevronRight, null) - Text( - it.cleanName, - style = MaterialTheme.typography.titleMedium - ) - } +fun LoginScreenModel.backNavigationHandler(navigator: Navigator): BackNavigationHandler { + return when (val screen = navigator.lastItem) { + is LoginNavigation.UserSelectionScreen -> screen.model.backNavigationHandler(navigator) + else -> BackNavigationHandler.default() } } -@Composable -private fun ConfirmLogoutDeviceDialog( - onDismiss: () -> Unit, - onConfirm: () -> Unit, - modifier: Modifier = Modifier -) = AlertDialog( - onDismissRequest = onDismiss, - confirmButton = { - Button( - onClick = onConfirm - ) { - Text("Ausloggen") - } - }, - modifier, - dismissButton = { - TextButton( - onClick = onDismiss - ) { - Text("Abbrechen") - } - }, - text = { - Text("Möchtest du das Gerät wirklich ausloggen?") - } -) @Composable fun LogoutTopBarAction(session: Session) { diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/UserSelection.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/UserSelection.kt index 51b4243..ad48a2a 100644 --- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/UserSelection.kt +++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/login/UserSelection.kt @@ -1,6 +1,7 @@ package net.novagamestudios.kaffeekasse.ui.login import android.content.res.Configuration +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -11,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.grid.GridCells @@ -21,14 +23,13 @@ import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Password -import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.PhonelinkErase import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.DividerDefaults -import androidx.compose.material3.DockedSearchBar import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -36,7 +37,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.ProvideTextStyle -import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.pulltorefresh.PullToRefreshContainer @@ -56,6 +56,7 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -76,11 +77,15 @@ import net.novagamestudios.common_utils.compose.components.RowCenter import net.novagamestudios.common_utils.compose.state.rememberDerivedStateOf import net.novagamestudios.common_utils.warn import net.novagamestudios.kaffeekasse.App +import net.novagamestudios.kaffeekasse.R import net.novagamestudios.kaffeekasse.api.kaffeekasse.model.BasicUserInfo +import net.novagamestudios.kaffeekasse.data.cleanName import net.novagamestudios.kaffeekasse.model.kaffeekasse.UserAuthCredentials +import net.novagamestudios.kaffeekasse.model.session.Device import net.novagamestudios.kaffeekasse.repositories.LoginRepository import net.novagamestudios.kaffeekasse.repositories.RepositoryProvider import net.novagamestudios.kaffeekasse.repositories.i11.KaffeekasseRepository +import net.novagamestudios.kaffeekasse.ui.AppInfoTopBarAction import net.novagamestudios.kaffeekasse.ui.handleSession import net.novagamestudios.kaffeekasse.ui.navigation.LoginNavigation import net.novagamestudios.kaffeekasse.ui.util.AlphabetSelectionChar @@ -89,28 +94,34 @@ import net.novagamestudios.kaffeekasse.ui.util.HorizontalSelectionBar import net.novagamestudios.kaffeekasse.ui.util.RichDataContent import net.novagamestudios.kaffeekasse.ui.util.Toasts import net.novagamestudios.kaffeekasse.ui.util.ToastsState +import net.novagamestudios.kaffeekasse.ui.util.TopBarSearchAction +import net.novagamestudios.kaffeekasse.ui.util.TopBarSearchField +import net.novagamestudios.kaffeekasse.ui.util.TopBarSearchFieldState import net.novagamestudios.kaffeekasse.ui.util.VerticalSelectionBar +import net.novagamestudios.kaffeekasse.ui.util.navigation.BackNavigationHandler import net.novagamestudios.kaffeekasse.ui.util.rememberPullToRefreshState import net.novagamestudios.kaffeekasse.ui.util.screenmodel.ScreenModelFactory +import net.novagamestudios.kaffeekasse.ui.util.screenmodel.collectAsStateHere import net.novagamestudios.kaffeekasse.util.richdata.asRichDataFlow import net.novagamestudios.kaffeekasse.util.richdata.combineRich -import net.novagamestudios.kaffeekasse.util.richdata.dataOrNull import net.novagamestudios.kaffeekasse.util.richdata.stateIn import net.novagamestudios.kaffeekasse.util.richdata.withFunctions class UserSelectionScreenModel private constructor( - private val loginRepository: LoginRepository, + val device: Device, + val loginRepository: LoginRepository, kaffeekasse: KaffeekasseRepository ) : ScreenModel, Logger { + val isPerformingLoginAction by loginRepository.isPerformingAction.collectAsStateHere() val users = kaffeekasse.basicUserInfoList - var searchQuery by mutableStateOf("") + val searchState = TopBarSearchFieldState() val filteredUsers = run { val data = combineRich( - snapshotFlow { searchQuery.lowercase() }.asRichDataFlow(), + snapshotFlow { searchState.queryOrBlank.lowercase() }.asRichDataFlow(), users, loadDuringTransform = true ) { query, users -> @@ -160,12 +171,25 @@ class UserSelectionScreenModel private constructor( loginUser(dialog.user, dialog.auth) } + fun logoutDevice() { + screenModelScope.launch { + loginRepository.logoutDevice() + } + } + companion object : ScreenModelFactory<LoginNavigation.UserSelectionScreen, UserSelectionScreenModel> { context (RepositoryProvider) override fun create(screen: LoginNavigation.UserSelectionScreen) = UserSelectionScreenModel( + device = screen.device, loginRepository = loginRepository, kaffeekasse = kaffeekasseRepository, ) + + + @Composable + fun UserSelectionScreenModel.backNavigationHandler(navigator: Navigator): BackNavigationHandler { + return remember { searchState.backNavigation } + } } } @@ -186,19 +210,19 @@ fun UserSelection( .nestedScroll(pullToRefreshState.nestedScrollConnection), contentAlignment = Alignment.TopCenter ) { - UserSearchBar( - query = model.searchQuery, - onQueryChange = { model.searchQuery = it }, - onSearch = { - model.filteredUsers.value - .dataOrNull - ?.singleOrNull() - ?.let { model.loginUser(it) } - }, - Modifier - .padding(8.dp) - .align(Alignment.TopCenter) - ) +// UserSearchBar( +// query = model.searchQuery, +// onQueryChange = { model.searchQuery = it }, +// onSearch = { +// model.filteredUsers.value +// .dataOrNull +// ?.singleOrNull() +// ?.let { model.loginUser(it) } +// }, +// Modifier +// .padding(8.dp) +// .align(Alignment.TopCenter) +// ) RichDataContent( source = model.filteredUsers, @@ -240,6 +264,7 @@ fun UserSelection( Toasts(model.toasts) } +/* @Composable private fun UserSearchBar( query: String, @@ -274,6 +299,7 @@ private fun UserSearchBar( modifier, content = { } ) +*/ @Composable private fun UserGridWithSelectionBar( @@ -281,7 +307,8 @@ private fun UserGridWithSelectionBar( onClick: (BasicUserInfo) -> Unit, modifier: Modifier = Modifier ) { - val searchBarOffset = SearchBarDefaults.InputFieldHeight +// val searchBarOffset = SearchBarDefaults.InputFieldHeight + val searchBarOffset = 0.dp val gridState = rememberLazyGridState() val coroutineScope = rememberCoroutineScope() @@ -507,3 +534,94 @@ private fun UserAuthDialog( } } ) + + +@Composable +fun UserSelectionTopBarTitle( + model: UserSelectionScreenModel, + navigator: Navigator +) { + Row(verticalAlignment = Alignment.CenterVertically) { + if (model.searchState.show) { + TopBarSearchField(model.searchState) + } else { + Image( + painterResource(R.drawable.logo_edited), + "Kaffeekasse", + Modifier.size(32.dp) + ) + DeviceInfo(model.device, Modifier.padding(16.dp)) + } + } +} + + +@Composable +fun UserSelectionTopBarActions( + model: UserSelectionScreenModel, + navigator: Navigator +) { + TopBarSearchAction(model.searchState, alwaysShow = true) + AppInfoTopBarAction() + var confirmLogout by remember { mutableStateOf(false) } + IconButton( + onClick = { confirmLogout = true }, + enabled = !model.isPerformingLoginAction + ) { + Icon(Icons.Default.PhonelinkErase, "Gerät ausloggen") + } + if (confirmLogout) ConfirmLogoutDeviceDialog( + onDismiss = { confirmLogout = false }, + onConfirm = { model.logoutDevice() } + ) +} + + +@Composable +private fun DeviceInfo( + device: Device, + modifier: Modifier = Modifier +) = Row( + modifier, + verticalAlignment = Alignment.CenterVertically +) { + Text(device.name ?: "Unbekanntes Gerät") + device.knownItemGroup?.let { + ProvideTextStyle(MaterialTheme.typography.titleMedium) { + Spacer(Modifier.width(16.dp)) + Icon(Icons.Default.ChevronRight, null) + Text( + it.cleanName, + style = MaterialTheme.typography.titleMedium + ) + } + } +} + +@Composable +private fun ConfirmLogoutDeviceDialog( + onDismiss: () -> Unit, + onConfirm: () -> Unit, + modifier: Modifier = Modifier +) = AlertDialog( + onDismissRequest = onDismiss, + confirmButton = { + Button( + onClick = onConfirm + ) { + Text("Ausloggen") + } + }, + modifier, + dismissButton = { + TextButton( + onClick = onDismiss + ) { + Text("Abbrechen") + } + }, + text = { + Text("Möchtest du das Gerät wirklich ausloggen?") + } +) + diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/navigation/AppScreens.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/navigation/AppScreens.kt index ebbea3a..61b33ee 100644 --- a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/navigation/AppScreens.kt +++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/navigation/AppScreens.kt @@ -42,6 +42,7 @@ import net.novagamestudios.kaffeekasse.ui.login.LoginTopBarActions import net.novagamestudios.kaffeekasse.ui.login.LoginTopBarTitle import net.novagamestudios.kaffeekasse.ui.login.UserSelection import net.novagamestudios.kaffeekasse.ui.login.UserSelectionScreenModel +import net.novagamestudios.kaffeekasse.ui.login.backNavigationHandler import net.novagamestudios.kaffeekasse.ui.util.screenmodel.ScreenModelProvider import net.novagamestudios.kaffeekasse.ui.util.screenmodel.ScreenProvidingModel @@ -62,8 +63,8 @@ sealed interface LoginNavigation { @Composable override fun Content() = AppScaffoldNavigator( key = "login-navigator", initialRoute = listOf(initialScreen), - title = { LoginTopBarTitle(model) }, - actions = { LoginTopBarActions(model) }, + title = { LoginTopBarTitle(model, it) }, + actions = { LoginTopBarActions(model, it) }, topAppBarScrollBehavior = null, content = { if (model.isLoading) { @@ -74,7 +75,8 @@ sealed interface LoginNavigation { AppScaffoldNavigatorDefaultContent("login-navigator-content") } LoginAdditional(model, this) - } + }, + backNavigationHandlerProvider = { model.backNavigationHandler(it) } ) } diff --git a/app/src/main/java/net/novagamestudios/kaffeekasse/ui/util/TopBarSearchField.kt b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/util/TopBarSearchField.kt new file mode 100644 index 0000000..118993c --- /dev/null +++ b/app/src/main/java/net/novagamestudios/kaffeekasse/ui/util/TopBarSearchField.kt @@ -0,0 +1,91 @@ +package net.novagamestudios.kaffeekasse.ui.util + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import net.novagamestudios.kaffeekasse.ui.util.navigation.BackNavigationHandler + + +class TopBarSearchFieldState { + private var _show by mutableStateOf(false) + var show + get() = _show + set(value) { + _show = value + if (!value) query = "" + } + var query by mutableStateOf("") + val queryOrBlank get() = query.takeIf { show } ?: "" + val queryOrNull get() = query.takeIf { show }?.takeUnless { it.isBlank() } + + val backNavigation by lazy { + object : BackNavigationHandler { + override fun canNavigateBack() = show + override fun onNavigateBack(): Boolean { + if (show) { + show = false + return true + } + return false + } + } + } +} + + +@Composable +fun TopBarSearchField( + state: TopBarSearchFieldState, + modifier: Modifier = Modifier, + onSearch: () -> Unit = { } +) { + if (state.show) { + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + SearchBarDefaults.InputField( + query = state.query, + onQueryChange = { state.query = it }, + onSearch = { onSearch() }, + expanded = false, + onExpandedChange = { }, + modifier = Modifier.focusRequester(focusRequester), + placeholder = { Text("Suchen") }, + trailingIcon = { + if (state.query.isNotEmpty()) IconButton(onClick = { state.query = "" }) { + Icon(Icons.Default.Clear, "Clear") + } + } + ) + } +} + + +@Composable +fun TopBarSearchAction( + state: TopBarSearchFieldState, + modifier: Modifier = Modifier, + alwaysShow: Boolean = false +) { + if (alwaysShow || !state.show) IconButton( + onClick = { state.show = !state.show }, + modifier + ) { + Icon(Icons.Default.Search, "Suchen") + } +} + -- GitLab