mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 08:47:33 +00:00
Merge pull request #3632 from vitorpamplona/claude/settings-search-placement-n1wkqw
Move settings search field to top bar
This commit is contained in:
+163
@@ -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
|
||||
+9
-74
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user