Add ExpandedFabScrim composable that renders a Material 3 scrim
overlay fading in/out in sync with the mini-FABs. Tapping the scrim
dismisses the FAB.
Move the ExpandedFab out of Scaffold's floatingActionButton slot and
into the body inside a Box, so the scrim can match the body size via
matchParentSize() and stack above the list but below the FAB. Add a
BackHandler that dismisses the FAB on system back while it is open.
Wrap getExpenses.awaitOne, upsertExpense.await, getRecurringExpenses.awaitOne
and upsertRecurringExpense.await in withContext(Dispatchers.IO) { ... } so
the suspending Room calls run off the main thread. State-flow updates stay
inside screenModelScope.launch, which is Main-bound, and execute after the
withContext block returns.
- Add ui/components/ExpandedFab.kt with ExpandedFab + MiniFab helpers; HomeScreen and ExpenseListScreen both consume it, the tab-collapsing LaunchedEffect in ExpenseListScreen is hoisted to its Content()
- Inject GetExpenseSummary in HomeScreenModel; drive summary via dateRange.flatMapLatest { getExpenseSummary.await(it) } (fixes the period-filter total-card flicker) and drop the inline combine(expenses, dateRange) recomputation
- Hoist isFabExpanded out of HomeScreenModel into HomeScreen.Content() so the FAB state is local to the composable
- Convert HomeScreenModel.exportToCsv from a callback to a suspend fun returning Result<Unit>; the screen does the snackbar dispatch on the coroutineScope
- Consolidate DateRangeOption label mapping to a single DateRangeOption.labelRes() / .labelText() pair (one source of truth)
- Rename FAB string keys to shared fab_manual / fab_import and drop the home_fab_* duplicates
- Update docs/04-implementation-plan.md and .opencode/agent/implementor.md Charts sections to reflect the Vico 2.0.0 column-chart-with-legend substitution (Vico 2.0.0 has no pie layer)
- Add HomeScreenModel with expenses/summary/recurring-banner/fab state flows and a getExpenses + processDueRecurring + exportExpensesToCsv + expensePreference constructor
- Replace the HomeScreen stub with a Material 3 dashboard: AppBar (Export + Settings), total card, period filter, Vico ColumnCartesianLayer chart with per-category legend, manage-categories/see-all actions, recent expenses, and an expanded FAB exposing Manual + Import sub-actions
- Add home strings and a home_recurring_banner plurals resource
- matchesQuery: use %.2f format to match the display (was Double.toString, which can use scientific notation and disagree with the row's %.2f)
- Remove unused Switch import in AddEditRecurringScreen (the active toggle uses ToggleItem)
- Extract CategoryDropdownField to ui/components/CategoryDropdownField.kt so both add/edit screens share one implementation; takes label as a parameter
- Remove unused recurring_list_active string
Drop the Context injection from the ScreenModel. Emit a sealed
ImportSnackbarMessage carrying either a @StringRes id or a dynamic
text (for the error.message case where the bank importer surfaces a
useful 'BRI import not yet implemented' string). The screen resolves
resource ids via stringResource() in Composable scope and shows the
snackbar; the original error.message info is preserved via the
Dynamic variant instead of being dropped.
The three snackbar strings in ImportBankStatementScreenModel were
hardcoded English while the rest of the app uses stringResource. Move
them to res/values/strings.xml. Also drop the
error::class.simpleName fallback in the failure branch — it surfaced
Kotlin class names like NotImplementedError to end users. Now falls
back to the localized 'Import failed' string.
Gitea blocks self-approval and self-requested-changes, so the reviewer
agent (which always reviews its own work on this same-account setup)
must always submit with state: COMMENT. Verdicts go in the summary body;
blocking concerns are marked inline with **Blocking:** / **Nit:** /
**Suggestion:** prefixes so the implementor can triage them.
DateField seeded the picker with ZoneId.systemDefault() but converted
the selectedDateMillis back with ZoneId.of("UTC"). For any non-UTC
user, the field and the picker displayed different days, and
confirming without re-picking silently shifted the export range by a
day. Use UTC on both sides (the DatePickerState contract is that
selectedDateMillis is UTC midnight of the picked day).
screenModelScope is backed by PlatformMainDispatcher (Main.immediate),
so direct interactor calls run DB queries and file I/O on the UI
thread. Switch reactive flows with .flowOn(Dispatchers.IO) and wrap
suspend calls in withContext(Dispatchers.IO).
Move architecture, package structure, dependencies, and implementation
rules into .opencode/agent/implementor.md (default primary agent). Add
.opencode/agent/reviewer.md (read-only primary agent) with PR review
workflow that leaves inline review comments only -- no issue creation,
no edits, no commits. AGENTS.md is now the shared context both agents
load: workflow, git/PR conventions, and docs index. Set
default_agent: implementor in opencode.json.
Per review on PR #8 (#8):
- Split @Upsert into @Insert(OnConflictStrategy.REPLACE) + @Update in all 3 DAOs.
@Upsert returns -1 on the update path, so callers wanting the row ID would
get a junk value. Interactors now call insert vs update based on id == 0.
UpsertCategory returns category.id explicitly for the id != 0 branch.
- Add @Transaction to the 3 @Relation queries (ExpenseDao.subscribeAll,
ExpenseDao.subscribeByDateRange, RecurringExpenseDao.subscribeAll). This
silences the KSP warnings the PR body mentioned and makes the intent
explicit.
- Switch MainApplication seeding from a fire-and-forget CoroutineScope to
runBlocking(Dispatchers.IO). A fast first-tap on HomeScreen could otherwise
call GetCategories.awaitDefault() before seeding completed and crash.
- Add documenting comment on CategoryDao.getDefault() noting that the
'only one isDefault = 1' invariant is maintained at the interactor layer
(partial unique index would be a v2 migration).
- Add trailing newline to app/build.gradle.kts.
- File follow-up issue #9 for flipping exportSchema to true before any v2
migration lands.