fix: add search to category picker

This commit is contained in:
Rafal Wisniewski
2026-05-06 10:49:09 +02:00
parent 79551ab69d
commit 6c067f64ce
5 changed files with 116 additions and 37 deletions

View File

@@ -2,7 +2,6 @@ package cc.n0th1ng.tripmoney.data.dao
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Delete import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction import androidx.room.Transaction
import androidx.room.Upsert import androidx.room.Upsert
@@ -21,7 +20,7 @@ interface CategoryDao {
@Transaction @Transaction
@Query( @Query(
""" """
SELECT * FROM category WHERE archived is 0 SELECT * FROM category WHERE archived is 0 ORDER BY name
""" """
) )
fun categories(): Flow<List<Category>> fun categories(): Flow<List<Category>>

View File

@@ -98,7 +98,8 @@ fun AddExpenseBottomSheet(
expenseDtoToEdit = expenseDtoToEdit, expenseDtoToEdit = expenseDtoToEdit,
state = state, state = state,
currentTrip = currentTrip ?: Trip.DUMMY, currentTrip = currentTrip ?: Trip.DUMMY,
categories = categories categories = categories,
onSaveCategory = {expenseAndCategoryViewModel.save(it)}
) )
} }
@@ -111,7 +112,8 @@ fun AddExpenseBottomSheet(
expenseDtoToEdit: ExpenseDto?, expenseDtoToEdit: ExpenseDto?,
state: SheetState, state: SheetState,
currentTrip: Trip, currentTrip: Trip,
categories: List<Category> categories: List<Category>,
onSaveCategory: (Category) -> Unit
) { ) {
val currentTripId = currentTrip.id val currentTripId = currentTrip.id
@@ -319,7 +321,8 @@ fun AddExpenseBottomSheet(
category = selectedCategory category = selectedCategory
}, },
selected = category, selected = category,
categories = categories categories = categories,
onSaveCategory = onSaveCategory
) )
} }
} }
@@ -578,7 +581,8 @@ fun PreviewAddExpenseDisabled() {
LocalDate.parse("2020-01-15"), LocalDate.parse("2020-01-15"),
Currencies.entries.random().name Currencies.entries.random().name
), ),
categories = categoriesToPreview categories = categoriesToPreview,
{}
) )
} }
@@ -622,7 +626,8 @@ fun PreviewAddExpenseEnabled() {
LocalDate.parse("2020-01-11"), LocalDate.parse("2020-01-11"),
Currencies.entries.random().name Currencies.entries.random().name
), ),
categories = categoriesToPreview categories = categoriesToPreview,
{}
) )
} }

View File

@@ -13,22 +13,26 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
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.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.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import cc.n0th1ng.tripmoney.R.string import cc.n0th1ng.tripmoney.R.string
import cc.n0th1ng.tripmoney.data.entity.Category import cc.n0th1ng.tripmoney.data.entity.Category
import cc.n0th1ng.tripmoney.screens.AddCategoryDialog import cc.n0th1ng.tripmoney.screens.AddCategoryDialog
import cc.n0th1ng.tripmoney.viewmodel.ExpenseAndCategoryViewModel import cc.n0th1ng.tripmoney.screens.addexpense.categoriesToPreview
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
import cc.n0th1ng.tripmoney.utils.AllPreviews
import cc.n0th1ng.tripmoney.utils.SearchTextOutlined
import com.composables.icons.materialsymbols.outlined.R import com.composables.icons.materialsymbols.outlined.R
@Composable @Composable
@@ -37,22 +41,41 @@ fun CategorySelectionDialog(
onCategorySelected: (Category) -> Unit, onCategorySelected: (Category) -> Unit,
selected: Category?, selected: Category?,
categories: List<Category>, categories: List<Category>,
onSaveCategory: (Category) -> Unit
) { ) {
val expenseAndCategoryViewModel: ExpenseAndCategoryViewModel = hiltViewModel()
val listState = rememberLazyListState() val listState = rememberLazyListState()
var showAddCategoryDialog by remember { mutableStateOf(false) } var showAddCategoryDialog by remember { mutableStateOf(false) }
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, title = { Text(stringResource(string.pick_category)) }, text = { onDismissRequest = onDismiss,
title = { Text(stringResource(string.pick_category)) },
text = {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
if(selected != null) {
listState.animateScrollToItem(categories.indexOfFirst { it == selected })
}
// focusRequester.requestFocus()
}
Column { Column {
var search by remember { mutableStateOf("") }
val filteredCategories = if (search.isBlank()) {
categories
} else {
categories.filter { category ->
category.name.lowercase().contains(search.lowercase())
}
}
LazyColumn( LazyColumn(
modifier = Modifier.heightIn(max = 300.dp), modifier = Modifier.heightIn(max = 300.dp),
state = listState, state = listState,
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
items( items(
count = categories.size, count = filteredCategories.size,
key = { index -> categories[index].id }) { index -> key = { index -> filteredCategories[index].id }) { index ->
val category = categories[index] val category = filteredCategories[index]
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -83,24 +106,41 @@ fun CategorySelectionDialog(
.clickable { .clickable {
showAddCategoryDialog = true showAddCategoryDialog = true
} }
.padding(top = 15.dp), .padding(bottom = 10.dp),
verticalAlignment = Alignment.CenterVertically) { verticalAlignment = Alignment.CenterVertically) {
Icon( Icon(
painter = painterResource(R.drawable.materialsymbols_ic_add_outlined), painter = painterResource(R.drawable.materialsymbols_ic_add_outlined),
contentDescription = stringResource(string.category) contentDescription = stringResource(string.category)
) )
Text( Text(
text = stringResource(string.add_new), modifier = Modifier.padding(start = 8.dp), text = stringResource(string.add_new),
modifier = Modifier.padding(start = 8.dp),
) )
} }
SearchTextOutlined(
text = search,
onTextChange = { newText -> search = newText },
focusRequester = focusRequester
)
} }
}, confirmButton = {}) },
confirmButton = {})
if (showAddCategoryDialog) { if (showAddCategoryDialog) {
AddCategoryDialog(onDismiss = { AddCategoryDialog(onDismiss = {
showAddCategoryDialog = false showAddCategoryDialog = false
}, onSave = { category -> }, onSave = { category ->
expenseAndCategoryViewModel.save(category) onSaveCategory(category)
showAddCategoryDialog = false showAddCategoryDialog = false
}) })
} }
} }
@AllPreviews
@Composable
fun PreviewCategorySelectionDialog() {
TripMoneyTheme {
CategorySelectionDialog(
{}, {},
categoriesToPreview.random(), categoriesToPreview, {})
}
}

View File

@@ -12,9 +12,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -25,14 +23,13 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cc.n0th1ng.tripmoney.R import cc.n0th1ng.tripmoney.R
import cc.n0th1ng.tripmoney.theme.TripMoneyTheme import cc.n0th1ng.tripmoney.theme.TripMoneyTheme
import cc.n0th1ng.tripmoney.utils.AllPreviews import cc.n0th1ng.tripmoney.utils.AllPreviews
import cc.n0th1ng.tripmoney.utils.Currencies import cc.n0th1ng.tripmoney.utils.Currencies
import com.composables.icons.materialsymbols.outlined.R.drawable import cc.n0th1ng.tripmoney.utils.SearchTextOutlined
@Composable @Composable
fun CurrencySelectionDialog( fun CurrencySelectionDialog(
@@ -57,20 +54,6 @@ fun CurrencySelectionDialog(
} }
Column(verticalArrangement = Arrangement.spacedBy(5.dp)) { Column(verticalArrangement = Arrangement.spacedBy(5.dp)) {
OutlinedTextField(
value = search,
onValueChange = { newText ->
search = newText
},
modifier = Modifier.fillMaxWidth(),
trailingIcon = {
Icon(
painter = painterResource(drawable.materialsymbols_ic_search_outlined),
contentDescription = "search"
)
}
)
val filteredCurrencies = if (search.isBlank()) { val filteredCurrencies = if (search.isBlank()) {
currencies currencies
} else { } else {
@@ -79,7 +62,12 @@ fun CurrencySelectionDialog(
} }
} }
LazyColumn(state = scrollState) { LazyColumn(
state = scrollState,
modifier = Modifier
.weight(1f)
.padding(bottom = 10.dp)
) {
items( items(
count = filteredCurrencies.size, count = filteredCurrencies.size,
key = { index -> filteredCurrencies[index] } key = { index -> filteredCurrencies[index] }
@@ -104,6 +92,8 @@ fun CurrencySelectionDialog(
} }
} }
} }
SearchTextOutlined(
text = search, onTextChange = { newText -> search = newText })
} }
}, },
confirmButton = {}, confirmButton = {},

View File

@@ -0,0 +1,45 @@
package cc.n0th1ng.tripmoney.utils
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.painterResource
import com.composables.icons.materialsymbols.outlined.R.drawable
@Composable
fun SearchTextOutlined(
modifier: Modifier = Modifier,
text: String,
onTextChange: (String) -> Unit,
focusRequester: FocusRequester = FocusRequester()
) {
OutlinedTextField(
value = text,
onValueChange = onTextChange,
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
trailingIcon = {
if (text.isNotBlank()) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "close",
modifier = Modifier.clickable(true, onClick = { onTextChange("") })
)
} else {
Icon(
painter = painterResource(drawable.materialsymbols_ic_search_outlined),
contentDescription = "search"
)
}
}
)
}