fix(#41,#42,#43): numeric amount input, clickable DateField, search-based category picker #47
@@ -1,61 +0,0 @@
|
|||||||
package dev.achmad.ledgerr.ui.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
|
||||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
|
||||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
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 dev.achmad.ledgerr.domain.category.model.Category
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun CategoryDropdownField(
|
|
||||||
categoryId: Long?,
|
|
||||||
categories: List<Category>,
|
|
||||||
onCategoryChange: (Long) -> Unit,
|
|
||||||
label: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
|
||||||
val selectedName = categories.firstOrNull { it.id == categoryId }?.name.orEmpty()
|
|
||||||
|
|
||||||
ExposedDropdownMenuBox(
|
|
||||||
expanded = expanded,
|
|
||||||
onExpandedChange = { expanded = !expanded },
|
|
||||||
modifier = modifier,
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = selectedName,
|
|
||||||
onValueChange = {},
|
|
||||||
readOnly = true,
|
|
||||||
label = { Text(label) },
|
|
||||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, enabled = true),
|
|
||||||
)
|
|
||||||
ExposedDropdownMenu(
|
|
||||||
expanded = expanded,
|
|
||||||
onDismissRequest = { expanded = false },
|
|
||||||
) {
|
|
||||||
categories.forEach { category ->
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(category.name) },
|
|
||||||
onClick = {
|
|
||||||
onCategoryChange(category.id)
|
|
||||||
expanded = false
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package dev.achmad.ledgerr.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.ArrowDropDown
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import dev.achmad.ledgerr.R
|
||||||
|
import dev.achmad.ledgerr.domain.category.model.Category
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CategoryPickerField(
|
||||||
|
categoryId: Long?,
|
||||||
|
categories: List<Category>,
|
||||||
|
onCategoryChange: (Long) -> Unit,
|
||||||
|
label: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
|
val selected = categories.firstOrNull { it.id == categoryId }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { showDialog = true },
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = selected?.name.orEmpty(),
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text(label) },
|
||||||
|
leadingIcon = {
|
||||||
|
if (selected != null) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(20.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(selected.color))
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
|
||||||
|
shape = CircleShape,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.ArrowDropDown,
|
||||||
|
contentDescription = stringResource(R.string.action_pick),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDialog) {
|
||||||
|
ListSearchDialog(
|
||||||
|
title = label,
|
||||||
|
items = categories,
|
||||||
|
selected = selected,
|
||||||
|
onSelected = { category ->
|
||||||
|
onCategoryChange(category.id)
|
||||||
|
showDialog = false
|
||||||
|
},
|
||||||
|
onDismiss = { showDialog = false },
|
||||||
|
labelFor = { it.name },
|
||||||
|
swatchColorFor = { it.color },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package dev.achmad.ledgerr.ui.components
|
package dev.achmad.ledgerr.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.CalendarMonth
|
import androidx.compose.material.icons.outlined.CalendarMonth
|
||||||
@@ -8,7 +9,6 @@ import androidx.compose.material3.DatePicker
|
|||||||
import androidx.compose.material3.DatePickerDialog
|
import androidx.compose.material3.DatePickerDialog
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@@ -36,21 +36,25 @@ fun DateField(
|
|||||||
) {
|
) {
|
||||||
var showPicker by remember { mutableStateOf(false) }
|
var showPicker by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
OutlinedTextField(
|
Box(
|
||||||
value = date.format(dateFormatter()),
|
modifier = modifier
|
||||||
onValueChange = {},
|
.fillMaxWidth()
|
||||||
label = { Text(label) },
|
.clickable { showPicker = true },
|
||||||
readOnly = true,
|
) {
|
||||||
modifier = modifier.fillMaxWidth().clickable { showPicker = true },
|
OutlinedTextField(
|
||||||
trailingIcon = {
|
value = date.format(dateFormatter()),
|
||||||
IconButton(onClick = { showPicker = true }) {
|
onValueChange = {},
|
||||||
|
label = { Text(label) },
|
||||||
|
readOnly = true,
|
||||||
|
trailingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.CalendarMonth,
|
imageVector = Icons.Outlined.CalendarMonth,
|
||||||
contentDescription = stringResource(R.string.action_pick),
|
contentDescription = stringResource(R.string.action_pick),
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
},
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (showPicker) {
|
if (showPicker) {
|
||||||
val initialMillis = date.atStartOfDay(ZoneId.of("UTC"))
|
val initialMillis = date.atStartOfDay(ZoneId.of("UTC"))
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
package dev.achmad.ledgerr.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.selection.selectable
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Check
|
||||||
|
import androidx.compose.material.icons.outlined.Search
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SearchBar
|
||||||
|
import androidx.compose.material3.SearchBarDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
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.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import dev.achmad.ledgerr.R
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun <T : Any> ListSearchDialog(
|
||||||
|
title: String,
|
||||||
|
items: List<T>,
|
||||||
|
selected: T?,
|
||||||
|
onSelected: (T) -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
labelFor: (T) -> String,
|
||||||
|
swatchColorFor: ((T) -> Int)? = null,
|
||||||
|
) {
|
||||||
|
val density = LocalDensity.current
|
||||||
|
var searchBarHeight by remember { mutableStateOf(0.dp) }
|
||||||
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
|
val filteredItems by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
if (searchQuery.isEmpty()) {
|
||||||
|
items
|
||||||
|
} else {
|
||||||
|
items.filter { labelFor(it).contains(searchQuery, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text(text = title) },
|
||||||
|
text = {
|
||||||
|
Box {
|
||||||
|
val state = rememberLazyListState()
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.onSizeChanged { searchBarHeight = with(density) { it.height.toDp() } },
|
||||||
|
) {
|
||||||
|
SearchBar(
|
||||||
|
modifier = Modifier.offset(y = (-4).dp),
|
||||||
|
inputField = {
|
||||||
|
SearchBarDefaults.InputField(
|
||||||
|
query = searchQuery,
|
||||||
|
onQueryChange = { searchQuery = it },
|
||||||
|
onSearch = {},
|
||||||
|
expanded = false,
|
||||||
|
onExpandedChange = {},
|
||||||
|
placeholder = { Text(stringResource(R.string.picker_search_hint)) },
|
||||||
|
trailingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Search,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
shape = RectangleShape,
|
||||||
|
expanded = false,
|
||||||
|
onExpandedChange = {},
|
||||||
|
content = {},
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
if (filteredItems.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.picker_no_results),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = searchBarHeight)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.padding(vertical = 48.dp),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ScrollbarLazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = searchBarHeight)
|
||||||
|
.padding(top = 8.dp),
|
||||||
|
state = state,
|
||||||
|
) {
|
||||||
|
filteredItems.forEach { current ->
|
||||||
|
val isSelected = selected == current
|
||||||
|
item {
|
||||||
|
ListSearchDialogRow(
|
||||||
|
label = labelFor(current),
|
||||||
|
isSelected = isSelected,
|
||||||
|
swatchColor = swatchColorFor?.invoke(current),
|
||||||
|
onSelected = {
|
||||||
|
onSelected(current)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter))
|
||||||
|
if (state.canScrollForward) {
|
||||||
|
HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(text = stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ListSearchDialogRow(
|
||||||
|
label: String,
|
||||||
|
isSelected: Boolean,
|
||||||
|
swatchColor: Int?,
|
||||||
|
onSelected: () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(MaterialTheme.shapes.small)
|
||||||
|
.selectable(
|
||||||
|
selected = isSelected,
|
||||||
|
onClick = { if (!isSelected) onSelected() },
|
||||||
|
)
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.minimumInteractiveComponentSize(),
|
||||||
|
) {
|
||||||
|
if (swatchColor != null) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(20.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(swatchColor))
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
|
||||||
|
shape = CircleShape,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
} else {
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
if (isSelected) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -29,7 +29,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator
|
|||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import dev.achmad.ledgerr.R
|
import dev.achmad.ledgerr.R
|
||||||
import dev.achmad.ledgerr.ui.components.AppBar
|
import dev.achmad.ledgerr.ui.components.AppBar
|
||||||
import dev.achmad.ledgerr.ui.components.CategoryDropdownField
|
import dev.achmad.ledgerr.ui.components.CategoryPickerField
|
||||||
import dev.achmad.ledgerr.ui.components.DateField
|
import dev.achmad.ledgerr.ui.components.DateField
|
||||||
import dev.achmad.ledgerr.ui.screens.category.CategoryScreen
|
import dev.achmad.ledgerr.ui.screens.category.CategoryScreen
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ data class AddEditExpenseScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
|
||||||
CategoryDropdownField(
|
CategoryPickerField(
|
||||||
categoryId = categoryId,
|
categoryId = categoryId,
|
||||||
categories = categories,
|
categories = categories,
|
||||||
onCategoryChange = screenModel::setCategory,
|
onCategoryChange = screenModel::setCategory,
|
||||||
|
|||||||
+2
-1
@@ -8,6 +8,7 @@ import dev.achmad.ledgerr.domain.category.model.Category
|
|||||||
import dev.achmad.ledgerr.domain.expense.interactor.GetExpenses
|
import dev.achmad.ledgerr.domain.expense.interactor.GetExpenses
|
||||||
import dev.achmad.ledgerr.domain.expense.interactor.UpsertExpense
|
import dev.achmad.ledgerr.domain.expense.interactor.UpsertExpense
|
||||||
import dev.achmad.ledgerr.domain.expense.model.Expense
|
import dev.achmad.ledgerr.domain.expense.model.Expense
|
||||||
|
import dev.achmad.ledgerr.ui.util.sanitizeAmountInput
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
@@ -74,7 +75,7 @@ class AddEditExpenseScreenModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setAmount(value: String) {
|
fun setAmount(value: String) {
|
||||||
_amount.value = value
|
_amount.value = value.sanitizeAmountInput()
|
||||||
refreshIsValid()
|
refreshIsValid()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -32,7 +32,7 @@ import cafe.adriel.voyager.navigator.currentOrThrow
|
|||||||
import dev.achmad.ledgerr.R
|
import dev.achmad.ledgerr.R
|
||||||
import dev.achmad.ledgerr.domain.recurring.model.RecurringInterval
|
import dev.achmad.ledgerr.domain.recurring.model.RecurringInterval
|
||||||
import dev.achmad.ledgerr.ui.components.AppBar
|
import dev.achmad.ledgerr.ui.components.AppBar
|
||||||
import dev.achmad.ledgerr.ui.components.CategoryDropdownField
|
import dev.achmad.ledgerr.ui.components.CategoryPickerField
|
||||||
import dev.achmad.ledgerr.ui.components.DateField
|
import dev.achmad.ledgerr.ui.components.DateField
|
||||||
import dev.achmad.ledgerr.ui.components.LabeledRadioButton
|
import dev.achmad.ledgerr.ui.components.LabeledRadioButton
|
||||||
import dev.achmad.ledgerr.ui.components.ToggleItem
|
import dev.achmad.ledgerr.ui.components.ToggleItem
|
||||||
@@ -102,7 +102,7 @@ data class AddEditRecurringScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
|
|
||||||
CategoryDropdownField(
|
CategoryPickerField(
|
||||||
categoryId = categoryId,
|
categoryId = categoryId,
|
||||||
categories = categories,
|
categories = categories,
|
||||||
onCategoryChange = screenModel::setCategory,
|
onCategoryChange = screenModel::setCategory,
|
||||||
|
|||||||
+2
-1
@@ -9,6 +9,7 @@ import dev.achmad.ledgerr.domain.recurring.interactor.GetRecurringExpenses
|
|||||||
import dev.achmad.ledgerr.domain.recurring.interactor.UpsertRecurringExpense
|
import dev.achmad.ledgerr.domain.recurring.interactor.UpsertRecurringExpense
|
||||||
import dev.achmad.ledgerr.domain.recurring.model.RecurringExpense
|
import dev.achmad.ledgerr.domain.recurring.model.RecurringExpense
|
||||||
import dev.achmad.ledgerr.domain.recurring.model.RecurringInterval
|
import dev.achmad.ledgerr.domain.recurring.model.RecurringInterval
|
||||||
|
import dev.achmad.ledgerr.ui.util.sanitizeAmountInput
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
@@ -86,7 +87,7 @@ class AddEditRecurringScreenModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setAmount(value: String) {
|
fun setAmount(value: String) {
|
||||||
_amount.value = value
|
_amount.value = value.sanitizeAmountInput()
|
||||||
refreshIsValid()
|
refreshIsValid()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,3 +19,12 @@ fun String.toTitleCase(): String {
|
|||||||
word.replaceFirstChar { it.uppercase() }
|
word.replaceFirstChar { it.uppercase() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun String.sanitizeAmountInput(): String {
|
||||||
|
val parts = split('.', limit = 2)
|
||||||
|
return when (parts.size) {
|
||||||
|
1 -> parts[0].filter(Char::isDigit)
|
||||||
|
2 -> parts[0].filter(Char::isDigit) + "." + parts[1].filter(Char::isDigit)
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -126,4 +126,8 @@
|
|||||||
<string name="home_dashboard_empty">No expenses yet for this period</string>
|
<string name="home_dashboard_empty">No expenses yet for this period</string>
|
||||||
<string name="home_settings">Settings</string>
|
<string name="home_settings">Settings</string>
|
||||||
<string name="home_export_failure">Export failed</string>
|
<string name="home_export_failure">Export failed</string>
|
||||||
|
|
||||||
|
<!-- Picker dialog (issue #43) -->
|
||||||
|
<string name="picker_search_hint">Search…</string>
|
||||||
|
<string name="picker_no_results">No matches</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user