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