M8: app snackbars through showKvittSnackbar; account-archived undo with common_undo
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
@@ -12,6 +12,8 @@ import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import se.hagstedt.kvitt.link.AppLink
|
||||
import se.hagstedt.kvitt.link.OpenTarget
|
||||
import se.hagstedt.kvitt.model.AccountId
|
||||
import se.hagstedt.kvitt.model.AccountRepository
|
||||
import se.hagstedt.kvitt.model.SessionRepository
|
||||
import se.hagstedt.kvitt.model.SessionState
|
||||
import se.hagstedt.kvitt.model.SyncRepository
|
||||
@@ -22,6 +24,7 @@ import javax.inject.Inject
|
||||
class AppViewModel @Inject constructor(
|
||||
session: SessionRepository,
|
||||
private val sync: SyncRepository,
|
||||
private val accounts: AccountRepository,
|
||||
) : ViewModel() {
|
||||
/** Null until the first emission (the shell shows the empty themed screen meanwhile). */
|
||||
val session: StateFlow<SessionState?> = session.session.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
@@ -65,4 +68,9 @@ class AppViewModel @Inject constructor(
|
||||
fun notificationsAsked() {
|
||||
askNotificationsState.value = false
|
||||
}
|
||||
|
||||
/** "Ångra" on the archived snackbar of S-5 (design-spec S-5). */
|
||||
fun undoArchive(id: AccountId) {
|
||||
viewModelScope.launch { accounts.setArchived(id, false) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package se.hagstedt.kvitt.nav
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Resources
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
@@ -25,6 +27,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
@@ -43,6 +46,8 @@ import se.hagstedt.kvitt.design.component.KvittClock
|
||||
import se.hagstedt.kvitt.design.component.KvittDestination
|
||||
import se.hagstedt.kvitt.design.component.KvittHousehold
|
||||
import se.hagstedt.kvitt.design.component.KvittSnackbar
|
||||
import se.hagstedt.kvitt.design.component.KvittSnackbarVisuals
|
||||
import se.hagstedt.kvitt.design.component.showKvittSnackbar
|
||||
import se.hagstedt.kvitt.design.component.LocalAvatarPictures
|
||||
import se.hagstedt.kvitt.design.component.LocalKvittClock
|
||||
import se.hagstedt.kvitt.design.component.LocalKvittHousehold
|
||||
@@ -130,7 +135,8 @@ private fun AppGraph(viewModel: AppViewModel, session: SessionState, updateCheck
|
||||
val start = remember { startRoute(session) }
|
||||
val entry by nav.currentBackStackEntryAsState()
|
||||
val route = entry?.destination?.route
|
||||
val shell = Shell(nav, snackbar, scope)
|
||||
val resources = LocalResources.current
|
||||
val shell = Shell(nav, snackbar, scope, resources)
|
||||
|
||||
// The session gate: revocation (S-18) and a lost pairing (S-1 "Pair this phone again") replace the whole stack.
|
||||
LaunchedEffect(session) {
|
||||
@@ -157,13 +163,16 @@ private fun AppGraph(viewModel: AppViewModel, session: SessionState, updateCheck
|
||||
NavHost(nav, startDestination = start) {
|
||||
onboarding(viewModel, shell)
|
||||
topLevel(session, shell)
|
||||
accountsAndExpenses(shell)
|
||||
accountsAndExpenses(viewModel, shell)
|
||||
settings(shell, updateCheckAvailable)
|
||||
}
|
||||
// Home shows result snackbars in its own scaffold; every other screen gets them here.
|
||||
if (route != Routes.HOME) {
|
||||
SnackbarHost(snackbar, Modifier.align(Alignment.BottomCenter).navigationBarsPadding().imePadding()) { data ->
|
||||
KvittSnackbar(data.visuals.message, Modifier.padding(KvittTheme.spacing.s4), data.visuals.actionLabel, { data.performAction() })
|
||||
val visuals = data.visuals
|
||||
val message = if (visuals is KvittSnackbarVisuals) stringResource(visuals.text, *visuals.args.toTypedArray()) else visuals.message
|
||||
val action = if (visuals is KvittSnackbarVisuals) visuals.action?.let { stringResource(it) } else visuals.actionLabel
|
||||
KvittSnackbar(message, Modifier.padding(KvittTheme.spacing.s4), action, { data.performAction() })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,7 +185,7 @@ private fun startRoute(session: SessionState): String = when (session) {
|
||||
}
|
||||
|
||||
/** Navigation helpers shared by the graph sections. */
|
||||
private class Shell(val nav: NavHostController, val snackbar: SnackbarHostState, val scope: CoroutineScope) {
|
||||
private class Shell(val nav: NavHostController, val snackbar: SnackbarHostState, val scope: CoroutineScope, val resources: Resources) {
|
||||
fun replaceAll(route: String) = nav.navigate(route) {
|
||||
popUpTo(nav.graph.id) { inclusive = true }
|
||||
launchSingleTop = true
|
||||
@@ -201,11 +210,16 @@ private class Shell(val nav: NavHostController, val snackbar: SnackbarHostState,
|
||||
nav.popBackStack()
|
||||
}
|
||||
|
||||
fun show(message: String?, action: String? = null, onAction: () -> Unit = {}) {
|
||||
/** A snackbar text a feature resolved itself (S-7, S-8 hand over a String). */
|
||||
fun show(message: String?) {
|
||||
if (message == null) return
|
||||
scope.launch { snackbar.showSnackbar(message, withDismissAction = false) }
|
||||
}
|
||||
|
||||
/** `:app`'s own snackbars, resolved from resources while they show (language-safe, `showKvittSnackbar`). */
|
||||
fun show(@StringRes text: Int, args: List<Any> = emptyList(), @StringRes action: Int? = null, onAction: () -> Unit = {}) {
|
||||
scope.launch {
|
||||
val result = snackbar.showSnackbar(message, actionLabel = action, withDismissAction = false)
|
||||
if (result == SnackbarResult.ActionPerformed) onAction()
|
||||
if (snackbar.showKvittSnackbar(resources, text, args, action) == SnackbarResult.ActionPerformed) onAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,9 +293,8 @@ private fun NavGraphBuilder.topLevel(session: SessionState, shell: Shell) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun NavGraphBuilder.accountsAndExpenses(shell: Shell) {
|
||||
private fun NavGraphBuilder.accountsAndExpenses(viewModel: AppViewModel, shell: Shell) {
|
||||
composable(Routes.ACCOUNT, arguments = Routes.accountArgs) {
|
||||
val resources = LocalResources.current
|
||||
AccountDetailScreen(
|
||||
onBack = shell::back,
|
||||
onOpenExpense = { shell.nav.navigate(Routes.expense(it)) },
|
||||
@@ -289,11 +302,10 @@ private fun NavGraphBuilder.accountsAndExpenses(shell: Shell) {
|
||||
onAddExpense = { shell.nav.navigate(Routes.expenseEditor(account = it)) },
|
||||
onSettle = { shell.nav.navigate(Routes.settle(account = it)) },
|
||||
onEdit = { shell.nav.navigate(Routes.accountEditor(it)) },
|
||||
onArchived = { _, name ->
|
||||
onArchived = { id, name ->
|
||||
shell.nav.popBackStack(Routes.HOME, inclusive = false)
|
||||
// design-spec S-5: "Vardag arkiverades" with "Ångra" on Home. The undo label (common_undo) is not in
|
||||
// :core:design yet, so the snackbar has no action until it is (M8 report, contract request).
|
||||
shell.show(resources.getString(AccountR.string.account_archived_snackbar, name))
|
||||
// design-spec S-5: "Vardag arkiverades" with "Ångra" on Home.
|
||||
shell.show(AccountR.string.account_archived_snackbar, listOf(name), DesignR.string.common_undo) { viewModel.undoArchive(id) }
|
||||
},
|
||||
onDeleted = { shell.nav.popBackStack(Routes.HOME, inclusive = false) },
|
||||
)
|
||||
@@ -333,23 +345,21 @@ private fun NavGraphBuilder.accountsAndExpenses(shell: Shell) {
|
||||
)
|
||||
}
|
||||
composable(Routes.SETTLE, arguments = Routes.settleArgs) {
|
||||
val resources = LocalResources.current
|
||||
SettleScreen(
|
||||
onClose = shell::back,
|
||||
onSaved = {
|
||||
shell.back()
|
||||
shell.show(resources.getString(DesignR.string.snackbar_payment_saved))
|
||||
shell.show(DesignR.string.snackbar_payment_saved)
|
||||
},
|
||||
)
|
||||
}
|
||||
composable(Routes.SETTLEMENT, arguments = Routes.settlementArgs) {
|
||||
val resources = LocalResources.current
|
||||
SettlementDetailScreen(
|
||||
onBack = shell::back,
|
||||
onEdit = { shell.nav.navigate(Routes.settle(editing = it)) },
|
||||
onDeleted = {
|
||||
shell.back()
|
||||
shell.show(resources.getString(DesignR.string.snackbar_payment_deleted))
|
||||
shell.show(DesignR.string.snackbar_payment_deleted)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user