From ed4b851518a284414a797847568fba3d04d7f8e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 23:50:25 +0000 Subject: [PATCH] feat(settings): host the settings filter in the app bar Move the All Settings search field from a separate row below the top bar into the app bar itself, replacing the static "Settings" title. The back arrow is still shown only when the back stack can pop (deep-link entry), so bottom-nav entries stay chromeless. Adds SettingsSearchTopBar with a compact pill search field sized to fit the app bar and leaves the shared TopBarWithBackButton untouched. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01RBSLyPYssVu8QdBrvCM7wk --- .../topbars/SettingsSearchTopBar.kt | 163 ++++++++++++++++++ .../loggedIn/settings/AllSettingsScreen.kt | 83 +-------- 2 files changed, 172 insertions(+), 74 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/SettingsSearchTopBar.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/SettingsSearchTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/SettingsSearchTopBar.kt new file mode 100644 index 0000000000..39925e6543 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/SettingsSearchTopBar.kt @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.navigation.topbars + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +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.shape.CircleShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon +import com.vitorpamplona.amethyst.ui.stringRes + +/** + * Top bar for the All Settings screen that hosts the settings filter directly in the app bar, + * replacing the static "Settings" title. The back arrow is still shown only when there is + * something to pop (i.e. the user arrived from a deep/profile link rather than the bottom nav, + * which clears the stack). This keeps the shared [TopBarWithBackButton] untouched for every other + * screen while giving Settings a search-as-app-bar layout. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsSearchTopBar( + query: String, + onQueryChange: (String) -> Unit, + onClear: () -> Unit, + nav: INav, +) { + ShorterTopAppBar( + expandedHeight = SettingsSearchTopBarHeight, + title = { + CompactSettingsSearchField( + query = query, + onQueryChange = onQueryChange, + onClear = onClear, + modifier = + Modifier + .fillMaxWidth() + .padding(end = 12.dp), + ) + }, + navigationIcon = { + // Suppress the back arrow when this is the bottom of the back stack (bottom-nav entry + // clears it with popUpTo(route) { inclusive = true }); show it for deep-link entries. + if (nav.canPop()) { + IconButton(nav::popBack) { + ArrowBackIcon() + } + } + }, + ) +} + +/** + * A compact, pill-shaped search field sized to sit inside the app bar. It filters the settings + * list in place rather than opening a results overlay, so it is a plain [BasicTextField] with a + * custom decoration box (a full [androidx.compose.material3.TextField] enforces a ~56dp min height + * that does not fit an app bar). + */ +@Composable +private fun CompactSettingsSearchField( + query: String, + onQueryChange: (String) -> Unit, + onClear: () -> Unit, + modifier: Modifier = Modifier, +) { + val colorScheme = MaterialTheme.colorScheme + BasicTextField( + value = query, + onValueChange = onQueryChange, + modifier = modifier, + singleLine = true, + textStyle = MaterialTheme.typography.bodyLarge.copy(color = colorScheme.onSurface), + cursorBrush = SolidColor(colorScheme.primary), + decorationBox = { innerTextField -> + Row( + modifier = + Modifier + .fillMaxWidth() + .height(SettingsSearchFieldHeight) + .clip(CircleShape) + .background(colorScheme.surfaceContainerHigh) + .padding(start = 12.dp, end = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + symbol = MaterialSymbols.Search, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = colorScheme.onSurfaceVariant, + ) + Box( + modifier = + Modifier + .weight(1f) + .padding(horizontal = 8.dp), + contentAlignment = Alignment.CenterStart, + ) { + if (query.isEmpty()) { + Text( + text = stringRes(R.string.settings_search_placeholder), + style = MaterialTheme.typography.bodyLarge, + color = colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + innerTextField() + } + if (query.isNotEmpty()) { + IconButton(onClick = onClear) { + Icon( + symbol = MaterialSymbols.Close, + contentDescription = stringRes(R.string.clear), + modifier = Modifier.size(20.dp), + tint = LocalContentColor.current, + ) + } + } + } + }, + ) +} + +private val SettingsSearchTopBarHeight = 56.dp +private val SettingsSearchFieldHeight = 42.dp diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt index bc55763eee..ecf6e5af9a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt @@ -25,22 +25,17 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -51,7 +46,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.style.TextAlign @@ -64,7 +58,7 @@ import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route -import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.ui.navigation.topbars.SettingsSearchTopBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes @@ -121,7 +115,12 @@ fun AllSettingsScreen( Scaffold( topBar = { - TopBarWithBackButton(stringRes(id = R.string.settings), nav) + SettingsSearchTopBar( + query = query, + onQueryChange = { query = it }, + onClear = { query = "" }, + nav = nav, + ) }, bottomBar = { AppBottomBar(Route.AllSettings, nav, accountViewModel) { route -> @@ -134,18 +133,8 @@ fun AllSettingsScreen( }, ) { padding -> Column(modifier = Modifier.padding(padding).fillMaxSize()) { - SettingsSearchField( - query = query, - onQueryChange = { query = it }, - onClear = { query = "" }, - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), - ) - // Filtering happens in place: the list below is the search result. A blank query - // yields the whole catalog, narrowing as the user types. + // yields the whole catalog, narrowing as the user types via the top-bar search field. if (filtered.isEmpty()) { SettingsSearchEmptyState( query = query, @@ -156,6 +145,7 @@ fun AllSettingsScreen( modifier = Modifier .verticalScroll(scrollState) + .padding(top = 12.dp) .padding(horizontal = 16.dp) .padding(bottom = 12.dp), verticalArrangement = Arrangement.spacedBy(20.dp), @@ -194,61 +184,6 @@ fun AllSettingsScreen( } } -/** - * Persistent, pill-shaped search field styled after the Material 3 search bar. It filters the - * settings list in place rather than opening a results dropdown — appropriate for an in-page - * filter (see the Android system Settings app). - */ -@Composable -private fun SettingsSearchField( - query: String, - onQueryChange: (String) -> Unit, - onClear: () -> Unit, - modifier: Modifier = Modifier, -) { - TextField( - value = query, - onValueChange = onQueryChange, - modifier = modifier, - singleLine = true, - shape = CircleShape, - placeholder = { Text(stringRes(R.string.settings_search_placeholder)) }, - leadingIcon = { - Icon( - symbol = MaterialSymbols.Search, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - }, - trailingIcon = - if (query.isNotEmpty()) { - { - IconButton(onClick = onClear) { - Icon( - symbol = MaterialSymbols.Close, - contentDescription = stringRes(R.string.clear), - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - } else { - null - }, - colors = - TextFieldDefaults.colors( - focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - errorIndicatorColor = Color.Transparent, - ), - ) -} - @Composable private fun SettingsSearchEmptyState( query: String,