M7g: feature cleanups after the design-system fix pass
Resource-backed snackbars (showKvittSnackbar) in account, onboarding and settings; SettingsScaffold replaced by KvittScaffold + SettingsMessageEffect. KvittButton(loading) for S-6 saving and S-16 Synka nu. S-10 missing state disables Synka nu offline (connectivity in the ViewModel, new detail_missing_offline goldens). S-6 system back asks to discard unsaved changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
+13
-11
@@ -10,7 +10,6 @@ import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -22,6 +21,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.window.Popup
|
||||
@@ -44,6 +44,7 @@ import se.hagstedt.kvitt.design.component.ProvideHousehold
|
||||
import se.hagstedt.kvitt.design.component.TopBarNavigation
|
||||
import se.hagstedt.kvitt.design.component.kvittLanguage
|
||||
import se.hagstedt.kvitt.design.component.ratioSpoken
|
||||
import se.hagstedt.kvitt.design.component.showKvittSnackbar
|
||||
import se.hagstedt.kvitt.design.format.MoneyFormatter
|
||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||
import se.hagstedt.kvitt.model.AccountId
|
||||
@@ -273,16 +274,17 @@ private fun DeleteDialog(name: String, actions: AccountDetailActions) {
|
||||
/** Shows [message] once (4 s, 10 s with an action, design-spec §3.11); "Försök igen" repeats the failed action. */
|
||||
@Composable
|
||||
private fun MessageEffect(message: AccountMessage, host: SnackbarHostState, actions: AccountDetailActions) {
|
||||
val text = when (message) {
|
||||
is AccountMessage.CouldNotReach -> stringResource(DesignR.string.common_could_not_reach_server)
|
||||
is AccountMessage.Restored -> stringResource(R.string.account_unarchived_snackbar, message.name)
|
||||
AccountMessage.DeleteBlocked -> stringResource(R.string.account_delete_blocked)
|
||||
AccountMessage.NeedsConnection -> stringResource(DesignR.string.common_needs_connection)
|
||||
is AccountMessage.Failed -> errorText(message.code)
|
||||
}
|
||||
val action = if (message is AccountMessage.CouldNotReach) stringResource(DesignR.string.common_retry) else null
|
||||
LaunchedEffect(message, text, action) {
|
||||
val result = host.showSnackbar(text, action, duration = if (action != null) SnackbarDuration.Long else SnackbarDuration.Short)
|
||||
val resources = LocalResources.current
|
||||
LaunchedEffect(message) {
|
||||
val (text, args) = when (message) {
|
||||
is AccountMessage.CouldNotReach -> DesignR.string.common_could_not_reach_server to emptyList()
|
||||
is AccountMessage.Restored -> R.string.account_unarchived_snackbar to listOf<Any>(message.name)
|
||||
AccountMessage.DeleteBlocked -> R.string.account_delete_blocked to emptyList()
|
||||
AccountMessage.NeedsConnection -> DesignR.string.common_needs_connection to emptyList()
|
||||
is AccountMessage.Failed -> errorText(message.code) to emptyList()
|
||||
}
|
||||
val action = if (message is AccountMessage.CouldNotReach) DesignR.string.common_retry else null
|
||||
val result = host.showKvittSnackbar(resources, text, args, action)
|
||||
if (result == SnackbarResult.ActionPerformed && message is AccountMessage.CouldNotReach) actions.onRetry(message.retry)
|
||||
actions.onMessageShown()
|
||||
}
|
||||
|
||||
+22
-16
@@ -1,5 +1,6 @@
|
||||
package se.hagstedt.kvitt.feature.account
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -10,12 +11,9 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Text
|
||||
@@ -29,6 +27,7 @@ import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
@@ -54,6 +53,7 @@ import se.hagstedt.kvitt.design.component.TopBarNavigation
|
||||
import se.hagstedt.kvitt.design.component.kvittLanguage
|
||||
import se.hagstedt.kvitt.design.component.money
|
||||
import se.hagstedt.kvitt.design.component.ratioSpoken
|
||||
import se.hagstedt.kvitt.design.component.showKvittSnackbar
|
||||
import se.hagstedt.kvitt.design.format.MoneyFormatter
|
||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||
@@ -90,6 +90,8 @@ fun AccountEditorScreen(
|
||||
viewModel.savedHandled()
|
||||
onSaved(saved)
|
||||
}
|
||||
// System back with unsaved changes asks first, as the close button does (design-spec S-6).
|
||||
BackHandler(enabled = ui.dirty && !ui.discardDialog) { viewModel.requestClose() }
|
||||
val actions = AccountEditorActions(
|
||||
onClose = { if (viewModel.requestClose()) onClose() },
|
||||
onName = viewModel::onName,
|
||||
@@ -142,13 +144,16 @@ fun AccountEditorContent(ui: AccountEditorUi, actions: AccountEditorActions, mod
|
||||
}
|
||||
}
|
||||
|
||||
/** "Spara" (small filled); a progress indicator in its place while saving (design-spec S-6). */
|
||||
/** "Spara" (small filled); its in-button progress form while saving (design-spec S-6). */
|
||||
@Composable
|
||||
private fun SaveAction(ui: AccountEditorUi, actions: AccountEditorActions) {
|
||||
Row(Modifier.padding(end = KvittTheme.spacing.s2), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(KvittTheme.spacing.s2)) {
|
||||
if (ui.saving) CircularProgressIndicator(Modifier.size(KvittDimens.iconButton), strokeWidth = KvittDimens.placeholderBorder)
|
||||
KvittButton(stringResource(DesignR.string.common_save), actions.onSave, enabled = ui.canSave)
|
||||
}
|
||||
KvittButton(
|
||||
stringResource(DesignR.string.common_save),
|
||||
actions.onSave,
|
||||
Modifier.padding(end = KvittTheme.spacing.s2),
|
||||
enabled = ui.canSave || ui.saving,
|
||||
loading = ui.saving,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -293,14 +298,15 @@ private const val PREVIEW_ORE = BP_TOTAL.toLong()
|
||||
|
||||
@Composable
|
||||
private fun EditorMessageEffect(message: EditorMessage, host: SnackbarHostState, actions: AccountEditorActions) {
|
||||
val text = when (message) {
|
||||
EditorMessage.CouldNotReach -> stringResource(DesignR.string.common_could_not_reach_server)
|
||||
EditorMessage.NeedsConnection -> stringResource(DesignR.string.common_needs_connection)
|
||||
is EditorMessage.Failed -> errorText(message.code)
|
||||
}
|
||||
val action = if (message == EditorMessage.CouldNotReach) stringResource(DesignR.string.common_retry) else null
|
||||
LaunchedEffect(message, text, action) {
|
||||
val result = host.showSnackbar(text, action, duration = if (action != null) SnackbarDuration.Long else SnackbarDuration.Short)
|
||||
val resources = LocalResources.current
|
||||
LaunchedEffect(message) {
|
||||
val text = when (message) {
|
||||
EditorMessage.CouldNotReach -> DesignR.string.common_could_not_reach_server
|
||||
EditorMessage.NeedsConnection -> DesignR.string.common_needs_connection
|
||||
is EditorMessage.Failed -> errorText(message.code)
|
||||
}
|
||||
val action = if (message == EditorMessage.CouldNotReach) DesignR.string.common_retry else null
|
||||
val result = host.showKvittSnackbar(resources, text, action = action)
|
||||
if (result == SnackbarResult.ActionPerformed) actions.onSave()
|
||||
actions.onMessageShown()
|
||||
}
|
||||
|
||||
+14
-19
@@ -1,24 +1,19 @@
|
||||
package se.hagstedt.kvitt.feature.account
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.annotation.StringRes
|
||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
||||
|
||||
/** The snackbar text of an `OnlineResult.Failed` code without a field at fault (§14.3.2, strings.csv `error_*`). */
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
internal fun errorText(code: String): String = stringResource(
|
||||
when (code) {
|
||||
NETWORK -> DesignR.string.common_could_not_reach_server
|
||||
"household_incomplete" -> DesignR.string.error_household_incomplete
|
||||
"not_found" -> DesignR.string.error_not_found
|
||||
"forbidden" -> DesignR.string.error_forbidden
|
||||
"rate_limited" -> DesignR.string.error_rate_limited
|
||||
"protocol_unsupported" -> DesignR.string.error_app_too_old
|
||||
"internal", "unavailable", "not_implemented" -> DesignR.string.error_server
|
||||
"validation_failed" -> DesignR.string.error_validation
|
||||
"account_not_empty" -> R.string.account_delete_blocked
|
||||
else -> DesignR.string.error_generic_invalid
|
||||
},
|
||||
)
|
||||
@StringRes
|
||||
internal fun errorText(code: String): Int = when (code) {
|
||||
NETWORK -> DesignR.string.common_could_not_reach_server
|
||||
"household_incomplete" -> DesignR.string.error_household_incomplete
|
||||
"not_found" -> DesignR.string.error_not_found
|
||||
"forbidden" -> DesignR.string.error_forbidden
|
||||
"rate_limited" -> DesignR.string.error_rate_limited
|
||||
"protocol_unsupported" -> DesignR.string.error_app_too_old
|
||||
"internal", "unavailable", "not_implemented" -> DesignR.string.error_server
|
||||
"validation_failed" -> DesignR.string.error_validation
|
||||
"account_not_empty" -> R.string.account_delete_blocked
|
||||
else -> DesignR.string.error_generic_invalid
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user