diff --git a/.opencode/agent/implementor.md b/.opencode/agent/implementor.md index 14e0a2a..a9031d8 100644 --- a/.opencode/agent/implementor.md +++ b/.opencode/agent/implementor.md @@ -217,7 +217,7 @@ class AppPreference(private val store: PreferenceStore) { ### Charts - **Vico** (`com.patrykandpatrick.vico:compose`, `compose-m3`, `core`) is the only charting library used. -- `HomeScreen` dashboard renders a pie chart from `ExpenseSummary.byCategory` via Vico's `Chart` composable. No Canvas drawing for charts. +- `HomeScreen` dashboard renders a Vico `ColumnCartesianLayer` (one bar per category) from `ExpenseSummary.byCategory`, with a small `Row { colored swatch; category name; amount }` legend below the chart that carries the per-category color. Vico 2.0.0's `cartesian` package only exposes `Line` / `Column` / `Candlestick` layers (no pie), so the dashboard uses a column chart with a legend; per-category colors live in the legend, not in the chart. No Canvas drawing for charts. - Apache 2.0 license, Compose-native, no AndroidView wrapper. --- @@ -231,7 +231,7 @@ class AppPreference(private val store: PreferenceStore) { | Koin | 4.2.2 | DI | | Room | 2.7.1 | Local DB | | PDFBox-Android | 2.0.27.0 | PDF text extraction | -| Vico (compose / compose-m3 / core) | 2.x | Charts (pie / bar) | +| Vico (compose / compose-m3 / core) | 2.x | Charts (column with legend) | | Okio | (transitive) | CSV write | --- diff --git a/app/src/main/java/dev/achmad/ledgerr/ui/components/ExpandedFab.kt b/app/src/main/java/dev/achmad/ledgerr/ui/components/ExpandedFab.kt new file mode 100644 index 0000000..c43bbbf --- /dev/null +++ b/app/src/main/java/dev/achmad/ledgerr/ui/components/ExpandedFab.kt @@ -0,0 +1,87 @@ +package dev.achmad.ledgerr.ui.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Add +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SmallFloatingActionButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +@Composable +fun ExpandedFab( + expanded: Boolean, + onToggle: () -> Unit, + modifier: Modifier = Modifier, + actions: @Composable ColumnScope.() -> Unit, +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + AnimatedVisibility( + visible = expanded, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + ) { + Column( + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.spacedBy(12.dp), + content = actions, + ) + } + FloatingActionButton(onClick = onToggle) { + Icon( + imageVector = if (expanded) Icons.Outlined.Close else Icons.Outlined.Add, + contentDescription = null, + ) + } + } +} + +@Composable +fun MiniFab( + label: String, + icon: ImageVector, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Surface( + shape = MaterialTheme.shapes.small, + color = MaterialTheme.colorScheme.surface, + shadowElevation = 2.dp, + ) { + Text( + text = label, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), + style = MaterialTheme.typography.bodyMedium, + ) + } + SmallFloatingActionButton(onClick = onClick) { + Icon(imageVector = icon, contentDescription = null) + } + } +} diff --git a/app/src/main/java/dev/achmad/ledgerr/ui/screens/expenses/ExpenseListScreen.kt b/app/src/main/java/dev/achmad/ledgerr/ui/screens/expenses/ExpenseListScreen.kt index ea473c4..ad1e964 100644 --- a/app/src/main/java/dev/achmad/ledgerr/ui/screens/expenses/ExpenseListScreen.kt +++ b/app/src/main/java/dev/achmad/ledgerr/ui/screens/expenses/ExpenseListScreen.kt @@ -1,14 +1,8 @@ package dev.achmad.ledgerr.ui.screens.expenses -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -23,24 +17,19 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Add -import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.Repeat import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.UploadFile import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold -import androidx.compose.material3.SmallFloatingActionButton import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Tab import androidx.compose.material3.PrimaryTabRow @@ -60,7 +49,6 @@ 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.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow @@ -74,7 +62,9 @@ import dev.achmad.ledgerr.domain.expense.model.ExpenseWithCategory import dev.achmad.ledgerr.domain.recurring.model.RecurringExpenseWithCategory import dev.achmad.ledgerr.domain.recurring.model.RecurringInterval import dev.achmad.ledgerr.ui.components.AppBar +import dev.achmad.ledgerr.ui.components.ExpandedFab import dev.achmad.ledgerr.ui.components.ExportAction +import dev.achmad.ledgerr.ui.components.MiniFab import dev.achmad.ledgerr.ui.components.SingleSelectFilterChipGroup import dev.achmad.ledgerr.ui.screens.add_edit_expense.AddEditExpenseScreen import dev.achmad.ledgerr.ui.screens.add_edit_recurring.AddEditRecurringScreen @@ -105,6 +95,11 @@ object ExpenseListScreen : Screen { val pagerState = rememberPagerState(pageCount = { 2 }) val selectedTab = pagerState.currentPage + var isFabExpanded by remember { mutableStateOf(false) } + LaunchedEffect(selectedTab) { + isFabExpanded = false + } + Scaffold( topBar = { AppBar( @@ -129,12 +124,38 @@ object ExpenseListScreen : Screen { }, snackbarHost = { SnackbarHost(snackbarHostState) }, floatingActionButton = { - TabAwareFab( - selectedTab = selectedTab, - onAddExpense = { navigator.push(AddEditExpenseScreen(expenseId = null)) }, - onImportBankStatement = { navigator.push(ImportBankStatementScreen) }, - onAddRecurring = { navigator.push(AddEditRecurringScreen(recurringId = null)) }, - ) + ExpandedFab( + expanded = isFabExpanded, + onToggle = { isFabExpanded = !isFabExpanded }, + ) { + if (selectedTab == 0) { + MiniFab( + label = stringResource(R.string.fab_manual), + icon = Icons.Outlined.Edit, + onClick = { + isFabExpanded = false + navigator.push(AddEditExpenseScreen(expenseId = null)) + }, + ) + MiniFab( + label = stringResource(R.string.fab_import), + icon = Icons.Outlined.UploadFile, + onClick = { + isFabExpanded = false + navigator.push(ImportBankStatementScreen) + }, + ) + } else { + MiniFab( + label = stringResource(R.string.expense_list_fab_add_recurring), + icon = Icons.Outlined.Repeat, + onClick = { + isFabExpanded = false + navigator.push(AddEditRecurringScreen(recurringId = null)) + }, + ) + } + } }, ) { padding -> Column( @@ -414,99 +435,6 @@ private fun RecurringRow( } } -@Composable -private fun TabAwareFab( - selectedTab: Int, - onAddExpense: () -> Unit, - onImportBankStatement: () -> Unit, - onAddRecurring: () -> Unit, -) { - var expanded by remember { mutableStateOf(false) } - - LaunchedEffect(selectedTab) { - expanded = false - } - - Column( - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - AnimatedVisibility( - visible = expanded, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically(), - ) { - Column( - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - if (selectedTab == 0) { - MiniFab( - label = stringResource(R.string.expense_list_fab_manual), - icon = Icons.Outlined.Edit, - onClick = { - expanded = false - onAddExpense() - }, - ) - MiniFab( - label = stringResource(R.string.expense_list_fab_import), - icon = Icons.Outlined.UploadFile, - onClick = { - expanded = false - onImportBankStatement() - }, - ) - } else { - MiniFab( - label = stringResource(R.string.expense_list_fab_add_recurring), - icon = Icons.Outlined.Repeat, - onClick = { - expanded = false - onAddRecurring() - }, - ) - } - } - } - FloatingActionButton( - onClick = { expanded = !expanded }, - ) { - Icon( - imageVector = if (expanded) Icons.Outlined.Close else Icons.Outlined.Add, - contentDescription = null, - ) - } - } -} - -@Composable -private fun MiniFab( - label: String, - icon: ImageVector, - onClick: () -> Unit, -) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Surface( - shape = MaterialTheme.shapes.small, - color = MaterialTheme.colorScheme.surface, - shadowElevation = 2.dp, - ) { - Text( - text = label, - modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), - style = MaterialTheme.typography.bodyMedium, - ) - } - SmallFloatingActionButton(onClick = onClick) { - Icon(imageVector = icon, contentDescription = null) - } - } -} - private fun DateRangeFilter.labelRes(): Int = when (this) { DateRangeFilter.ALL -> R.string.expense_list_filter_all DateRangeFilter.THIS_WEEK -> R.string.expense_list_filter_this_week diff --git a/app/src/main/java/dev/achmad/ledgerr/ui/screens/home/HomeScreen.kt b/app/src/main/java/dev/achmad/ledgerr/ui/screens/home/HomeScreen.kt index f30f983..4533fb6 100644 --- a/app/src/main/java/dev/achmad/ledgerr/ui/screens/home/HomeScreen.kt +++ b/app/src/main/java/dev/achmad/ledgerr/ui/screens/home/HomeScreen.kt @@ -1,10 +1,5 @@ package dev.achmad.ledgerr.ui.screens.home -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -22,20 +17,17 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Add import androidx.compose.material.icons.outlined.Close import androidx.compose.material.icons.outlined.Edit import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.UploadFile import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold -import androidx.compose.material3.SmallFloatingActionButton import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface @@ -44,13 +36,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +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.vector.ImageVector import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow @@ -75,7 +68,9 @@ import dev.achmad.ledgerr.domain.expense.model.ExpenseSummary import dev.achmad.ledgerr.domain.expense.model.ExpenseWithCategory import dev.achmad.ledgerr.domain.preference.DateRangeOption import dev.achmad.ledgerr.ui.components.AppBar +import dev.achmad.ledgerr.ui.components.ExpandedFab import dev.achmad.ledgerr.ui.components.ExportAction +import dev.achmad.ledgerr.ui.components.MiniFab import dev.achmad.ledgerr.ui.components.SingleSelectFilterChipGroup import dev.achmad.ledgerr.ui.screens.add_edit_expense.AddEditExpenseScreen import dev.achmad.ledgerr.ui.screens.category.CategoryScreen @@ -99,10 +94,10 @@ object HomeScreen : Screen { val expenses by screenModel.expenses.collectAsState() val summary by screenModel.summary.collectAsState() val recurringBanner by screenModel.recurringBanner.collectAsState() - val isFabExpanded by screenModel.isFabExpanded.collectAsState() val snackbarHostState = remember { SnackbarHostState() } val coroutineScope = rememberCoroutineScope() + var isFabExpanded by remember { mutableStateOf(false) } val exportFailureText = stringResource(R.string.home_export_failure) @@ -113,11 +108,10 @@ object HomeScreen : Screen { actions = { ExportAction( onExportConfirmed = { range, uri -> - screenModel.exportToCsv(uri, range) { result -> + coroutineScope.launch { + val result = screenModel.exportToCsv(uri, range) if (result.isFailure) { - coroutineScope.launch { - snackbarHostState.showSnackbar(exportFailureText) - } + snackbarHostState.showSnackbar(exportFailureText) } } }, @@ -135,10 +129,25 @@ object HomeScreen : Screen { floatingActionButton = { ExpandedFab( expanded = isFabExpanded, - onToggle = screenModel::toggleFab, - onManual = { navigator.push(AddEditExpenseScreen(expenseId = null)) }, - onImport = { navigator.push(ImportBankStatementScreen) }, - ) + onToggle = { isFabExpanded = !isFabExpanded }, + ) { + MiniFab( + label = stringResource(R.string.fab_manual), + icon = Icons.Outlined.Edit, + onClick = { + isFabExpanded = false + navigator.push(AddEditExpenseScreen(expenseId = null)) + }, + ) + MiniFab( + label = stringResource(R.string.fab_import), + icon = Icons.Outlined.UploadFile, + onClick = { + isFabExpanded = false + navigator.push(ImportBankStatementScreen) + }, + ) + } }, ) { padding -> DashboardContent( @@ -269,12 +278,6 @@ private fun TotalCard( amount: Double, period: DateRangeOption, ) { - val periodLabel = stringResource( - when (period) { - DateRangeOption.THIS_WEEK -> R.string.home_period_this_week - DateRangeOption.THIS_MONTH -> R.string.home_period_this_month - } - ) Surface( modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.medium, @@ -284,7 +287,7 @@ private fun TotalCard( modifier = Modifier.padding(16.dp), ) { Text( - text = stringResource(R.string.home_total, periodLabel), + text = stringResource(R.string.home_total, period.labelText()), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onPrimaryContainer, ) @@ -304,19 +307,19 @@ private fun PeriodFilter( onChange: (DateRangeOption) -> Unit, ) { SingleSelectFilterChipGroup( - options = DateRangeOption.entries.map { it to label(it) }, - selectedOption = selected to label(selected), + options = DateRangeOption.entries.map { it to it.labelText() }, + selectedOption = selected to selected.labelText(), onSelectionChanged = { (option, _) -> onChange(option) }, ) } +private fun DateRangeOption.labelRes(): Int = when (this) { + DateRangeOption.THIS_WEEK -> R.string.home_period_this_week + DateRangeOption.THIS_MONTH -> R.string.home_period_this_month +} + @Composable -private fun label(option: DateRangeOption): String = stringResource( - when (option) { - DateRangeOption.THIS_WEEK -> R.string.home_period_this_week - DateRangeOption.THIS_MONTH -> R.string.home_period_this_month - } -) +private fun DateRangeOption.labelText(): String = stringResource(labelRes()) @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -472,77 +475,3 @@ private fun ExpenseRow(item: ExpenseWithCategory) { } } } - -@Composable -private fun ExpandedFab( - expanded: Boolean, - onToggle: () -> Unit, - onManual: () -> Unit, - onImport: () -> Unit, -) { - Column( - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - AnimatedVisibility( - visible = expanded, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically(), - ) { - Column( - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - MiniFab( - label = stringResource(R.string.home_fab_manual), - icon = Icons.Outlined.Edit, - onClick = { - onToggle() - onManual() - }, - ) - MiniFab( - label = stringResource(R.string.home_fab_import), - icon = Icons.Outlined.UploadFile, - onClick = { - onToggle() - onImport() - }, - ) - } - } - FloatingActionButton(onClick = onToggle) { - Icon( - imageVector = if (expanded) Icons.Outlined.Close else Icons.Outlined.Add, - contentDescription = null, - ) - } - } -} - -@Composable -private fun MiniFab( - label: String, - icon: ImageVector, - onClick: () -> Unit, -) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Surface( - shape = MaterialTheme.shapes.small, - color = MaterialTheme.colorScheme.surface, - shadowElevation = 2.dp, - ) { - Text( - text = label, - modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), - style = MaterialTheme.typography.bodyMedium, - ) - } - SmallFloatingActionButton(onClick = onClick) { - Icon(imageVector = icon, contentDescription = null) - } - } -} diff --git a/app/src/main/java/dev/achmad/ledgerr/ui/screens/home/HomeScreenModel.kt b/app/src/main/java/dev/achmad/ledgerr/ui/screens/home/HomeScreenModel.kt index 28ffe23..0afb029 100644 --- a/app/src/main/java/dev/achmad/ledgerr/ui/screens/home/HomeScreenModel.kt +++ b/app/src/main/java/dev/achmad/ledgerr/ui/screens/home/HomeScreenModel.kt @@ -4,6 +4,7 @@ import android.net.Uri import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope import dev.achmad.ledgerr.di.util.inject +import dev.achmad.ledgerr.domain.expense.interactor.GetExpenseSummary import dev.achmad.ledgerr.domain.expense.interactor.GetExpenses import dev.achmad.ledgerr.domain.expense.model.DateRange import dev.achmad.ledgerr.domain.expense.model.Expense @@ -15,12 +16,13 @@ import dev.achmad.ledgerr.domain.preference.ExpensePreference import dev.achmad.ledgerr.domain.recurring.interactor.ProcessDueRecurringExpenses import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -30,6 +32,7 @@ import kotlinx.coroutines.withContext @OptIn(ExperimentalCoroutinesApi::class) class HomeScreenModel( private val getExpenses: GetExpenses = inject(), + private val getExpenseSummary: GetExpenseSummary = inject(), private val processDueRecurring: ProcessDueRecurringExpenses = inject(), private val exportExpensesToCsv: ExportExpensesToCsv = inject(), private val expensePreference: ExpensePreference = inject(), @@ -57,33 +60,22 @@ class HomeScreenModel( initialValue = emptyList(), ) - val summary: StateFlow = combine(expenses, dateRange) { list, range -> - if (list.isEmpty()) { - null - } else { - ExpenseSummary( - totalAmount = list.sumOf { it.expense.amount }, - byCategory = list - .groupBy { it.expense.categoryId } - .map { (_, group) -> - group.first().category to group.sumOf { it.expense.amount } - } - .sortedByDescending { it.second }, - period = range, - ) - } - }.stateIn( - scope = screenModelScope, - started = SharingStarted.WhileSubscribed(5_000L), - initialValue = null, - ) + val summary: StateFlow = dateRange + .flatMapLatest { range -> summaryFlow(range) } + .stateIn( + scope = screenModelScope, + started = SharingStarted.WhileSubscribed(5_000L), + initialValue = null, + ) + + private fun summaryFlow(range: DateRange): Flow = flow { + val s = withContext(Dispatchers.IO) { getExpenseSummary.await(range) } + emit(s.takeIf { it.byCategory.isNotEmpty() }) + } private val _recurringBanner = MutableStateFlow?>(null) val recurringBanner: StateFlow?> = _recurringBanner.asStateFlow() - private val _isFabExpanded = MutableStateFlow(false) - val isFabExpanded: StateFlow = _isFabExpanded.asStateFlow() - init { screenModelScope.launch { val generated = withContext(Dispatchers.IO) { processDueRecurring.await() } @@ -95,20 +87,12 @@ class HomeScreenModel( _selectedDateRange.value = option } - fun toggleFab() { - _isFabExpanded.value = !_isFabExpanded.value - } - fun dismissRecurringBanner() { _recurringBanner.value = null } - fun exportToCsv(uri: Uri, range: DateRange, onResult: (Result) -> Unit) { - screenModelScope.launch { - val result = withContext(Dispatchers.IO) { exportExpensesToCsv.await(range, uri) } - onResult(result) - } - } + suspend fun exportToCsv(uri: Uri, range: DateRange): Result = + withContext(Dispatchers.IO) { exportExpensesToCsv.await(range, uri) } } private fun DateRangeOption.toDateRange(): DateRange = when (this) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aa374a4..85d4ed9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -72,8 +72,6 @@ All This week This month - Manual - Import Bank Statement Add Recurring Delete expense? This will permanently delete the expense. @@ -103,6 +101,10 @@ Monthly Yearly + + Manual + Import Bank Statement + Ledgerr Total %1$s @@ -115,8 +117,6 @@ %d new recurring expense added %d new recurring expenses added - Manual - Import Bank Statement No expenses yet for this period Settings Export failed diff --git a/docs/04-implementation-plan.md b/docs/04-implementation-plan.md index 7376b4c..f101d83 100644 --- a/docs/04-implementation-plan.md +++ b/docs/04-implementation-plan.md @@ -34,7 +34,7 @@ implementation(libs.vico.compose.m3) implementation(libs.vico.core) ``` -**Charts**: Vico 2.x Compose-native library. `HomeScreen` dashboard renders a pie chart from `ExpenseSummary.byCategory`. No custom Canvas drawing. +**Charts**: Vico 2.x Compose-native library. `HomeScreen` dashboard renders a Vico `ColumnCartesianLayer` from `ExpenseSummary.byCategory`, with a small category legend (`Row { colored swatch; category name; amount }`) below the chart that carries the per-category color. Vico 2.0.0's `cartesian` package only exposes `Line` / `Column` / `Candlestick` layers (no pie), so the dashboard uses a column chart with the legend. No custom Canvas drawing. **No manifest permissions** — SAF handles file access.