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:
@@ -10,6 +10,8 @@ roborazzi {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// BackHandler: system back on S-6 with unsaved changes asks to discard (design-spec S-6).
|
||||
implementation(libs.androidx.activity.compose)
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
testImplementation(libs.robolectric)
|
||||
|
||||
+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
|
||||
}
|
||||
|
||||
+3
-1
@@ -134,6 +134,7 @@ private fun SyncStatusList(ui: SyncStatusUi.Ready, actions: SyncStatusActions, p
|
||||
if (summary.clockSkew) item(key = "clock") { ClockBanner(actions.onOpenDateSettings) }
|
||||
item(key = "status") { StatusCard(ui) }
|
||||
item(key = "sync-now") {
|
||||
val syncing = ui.online && summary.running
|
||||
Column {
|
||||
KvittButton(
|
||||
stringResource(R.string.home_sync_status_sync_now),
|
||||
@@ -141,7 +142,8 @@ private fun SyncStatusList(ui: SyncStatusUi.Ready, actions: SyncStatusActions, p
|
||||
Modifier.fillMaxWidth(),
|
||||
size = ButtonSize.Medium,
|
||||
icon = KvittIcon.Sync,
|
||||
enabled = ui.canSyncNow,
|
||||
enabled = ui.canSyncNow || syncing,
|
||||
loading = syncing,
|
||||
)
|
||||
if (!ui.online) NeedsConnectionHint()
|
||||
}
|
||||
|
||||
+7
-5
@@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -27,6 +26,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboard
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.LiveRegionMode
|
||||
@@ -46,6 +46,7 @@ import se.hagstedt.kvitt.design.component.InviteCodeField
|
||||
import se.hagstedt.kvitt.design.component.KvittIconImage
|
||||
import se.hagstedt.kvitt.design.component.KvittScaffold
|
||||
import se.hagstedt.kvitt.design.component.KvittTextField
|
||||
import se.hagstedt.kvitt.design.component.showKvittSnackbar
|
||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||
|
||||
@@ -113,14 +114,15 @@ fun PairingContent(
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
) {
|
||||
val snackbarText = when (state.error) {
|
||||
PairingError.AppTooOld -> stringResource(DesignR.string.error_app_too_old)
|
||||
PairingError.Server -> stringResource(DesignR.string.error_server)
|
||||
PairingError.Validation -> stringResource(DesignR.string.error_validation)
|
||||
PairingError.AppTooOld -> DesignR.string.error_app_too_old
|
||||
PairingError.Server -> DesignR.string.error_server
|
||||
PairingError.Validation -> DesignR.string.error_validation
|
||||
else -> null
|
||||
}
|
||||
val resources = LocalResources.current
|
||||
LaunchedEffect(snackbarText) {
|
||||
if (snackbarText != null) {
|
||||
snackbarHostState.showSnackbar(snackbarText, duration = SnackbarDuration.Short)
|
||||
snackbarHostState.showKvittSnackbar(resources, snackbarText)
|
||||
actions.onErrorShown()
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -22,7 +22,6 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -33,6 +32,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
@@ -60,6 +60,7 @@ import se.hagstedt.kvitt.design.component.NeedsConnectionHint
|
||||
import se.hagstedt.kvitt.design.component.PersonAvatar
|
||||
import se.hagstedt.kvitt.design.component.ProvideHousehold
|
||||
import se.hagstedt.kvitt.design.component.kvittLanguage
|
||||
import se.hagstedt.kvitt.design.component.showKvittSnackbar
|
||||
import se.hagstedt.kvitt.design.format.KvittLanguage
|
||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||
@@ -130,10 +131,11 @@ fun ProfileStepContent(
|
||||
modifier: Modifier = Modifier,
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
) {
|
||||
val message = state.message?.let { stringResource(it.text) }
|
||||
val message = state.message?.text
|
||||
val resources = LocalResources.current
|
||||
LaunchedEffect(message) {
|
||||
if (message != null) {
|
||||
snackbarHostState.showSnackbar(message, duration = SnackbarDuration.Short)
|
||||
snackbarHostState.showKvittSnackbar(resources, message)
|
||||
actions.onMessageShown()
|
||||
}
|
||||
}
|
||||
|
||||
+5
-4
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
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
|
||||
@@ -22,6 +21,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
@@ -43,6 +43,7 @@ import se.hagstedt.kvitt.design.component.NeedsConnectionHint
|
||||
import se.hagstedt.kvitt.design.component.ProvideHousehold
|
||||
import se.hagstedt.kvitt.design.component.SegmentedGroup
|
||||
import se.hagstedt.kvitt.design.component.kvittLanguage
|
||||
import se.hagstedt.kvitt.design.component.showKvittSnackbar
|
||||
import se.hagstedt.kvitt.design.format.DateFormatter
|
||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||
import kotlin.time.Instant
|
||||
@@ -83,12 +84,12 @@ fun WaitingContent(
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
) {
|
||||
val error = state.loadError?.let {
|
||||
stringResource(if (it == "network") DesignR.string.common_could_not_reach_server else DesignR.string.error_server)
|
||||
if (it == "network") DesignR.string.common_could_not_reach_server else DesignR.string.error_server
|
||||
}
|
||||
val retry = stringResource(DesignR.string.common_retry)
|
||||
val resources = LocalResources.current
|
||||
LaunchedEffect(error) {
|
||||
if (error != null) {
|
||||
val result = snackbarHostState.showSnackbar(error, actionLabel = retry, duration = SnackbarDuration.Long)
|
||||
val result = snackbarHostState.showKvittSnackbar(resources, error, action = DesignR.string.common_retry)
|
||||
actions.onErrorShown()
|
||||
if (result == SnackbarResult.ActionPerformed) actions.onRetry()
|
||||
}
|
||||
|
||||
+9
-5
@@ -15,9 +15,11 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -35,6 +37,7 @@ import se.hagstedt.kvitt.design.component.ButtonSize
|
||||
import se.hagstedt.kvitt.design.component.FactRow
|
||||
import se.hagstedt.kvitt.design.component.KvittButton
|
||||
import se.hagstedt.kvitt.design.component.KvittIconImage
|
||||
import se.hagstedt.kvitt.design.component.KvittScaffold
|
||||
import se.hagstedt.kvitt.design.component.KvittTopAppBar
|
||||
import se.hagstedt.kvitt.design.component.NeedsConnectionHint
|
||||
import se.hagstedt.kvitt.design.component.SegmentedGroup
|
||||
@@ -44,7 +47,7 @@ import se.hagstedt.kvitt.design.component.UpdateBanner
|
||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||
import se.hagstedt.kvitt.feature.settings.R
|
||||
import se.hagstedt.kvitt.feature.settings.common.SettingsScaffold
|
||||
import se.hagstedt.kvitt.feature.settings.common.SettingsMessageEffect
|
||||
import se.hagstedt.kvitt.model.UpdateState
|
||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
||||
|
||||
@@ -76,13 +79,14 @@ class AboutActions(
|
||||
/** Stateless S-22. */
|
||||
@Composable
|
||||
fun AboutContent(state: AboutScreenState?, actions: AboutActions, updateCheckAvailable: Boolean, modifier: Modifier = Modifier) {
|
||||
SettingsScaffold(
|
||||
val snackbar = remember { SnackbarHostState() }
|
||||
SettingsMessageEffect(state?.message, snackbar, actions.onMessageShown)
|
||||
KvittScaffold(
|
||||
modifier.fillMaxSize(),
|
||||
topBar = { KvittTopAppBar(stringResource(R.string.settings_about_title), navigation = TopBarNavigation.Back, onNavigation = actions.onBack) },
|
||||
message = state?.message,
|
||||
onMessageShown = actions.onMessageShown,
|
||||
snackbarHostState = snackbar,
|
||||
) { padding ->
|
||||
if (state == null) return@SettingsScaffold
|
||||
if (state == null) return@KvittScaffold
|
||||
val about = state.about
|
||||
Column(
|
||||
Modifier
|
||||
|
||||
+9
-5
@@ -11,10 +11,12 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
@@ -30,6 +32,7 @@ import se.hagstedt.kvitt.design.component.ConfirmDialog
|
||||
import se.hagstedt.kvitt.design.component.KvittBadge
|
||||
import se.hagstedt.kvitt.design.component.KvittButton
|
||||
import se.hagstedt.kvitt.design.component.KvittIconImage
|
||||
import se.hagstedt.kvitt.design.component.KvittScaffold
|
||||
import se.hagstedt.kvitt.design.component.KvittTextField
|
||||
import se.hagstedt.kvitt.design.component.KvittTopAppBar
|
||||
import se.hagstedt.kvitt.design.component.NeedsConnectionHint
|
||||
@@ -39,8 +42,8 @@ import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||
import se.hagstedt.kvitt.feature.settings.R
|
||||
import se.hagstedt.kvitt.feature.settings.common.HintText
|
||||
import se.hagstedt.kvitt.feature.settings.common.SettingsScaffold
|
||||
import se.hagstedt.kvitt.feature.settings.common.SaveAction
|
||||
import se.hagstedt.kvitt.feature.settings.common.SettingsMessageEffect
|
||||
import se.hagstedt.kvitt.model.ColorToken
|
||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
||||
|
||||
@@ -100,7 +103,9 @@ class CategoryEditorActions(
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun CategoryEditorContent(state: CategoryEditorState?, actions: CategoryEditorActions, modifier: Modifier = Modifier) {
|
||||
SettingsScaffold(
|
||||
val snackbar = remember { SnackbarHostState() }
|
||||
SettingsMessageEffect(state?.message, snackbar, actions.onMessageShown)
|
||||
KvittScaffold(
|
||||
modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
val title = if (state?.isNew != false) R.string.settings_category_editor_new else R.string.settings_category_editor_edit
|
||||
@@ -108,10 +113,9 @@ fun CategoryEditorContent(state: CategoryEditorState?, actions: CategoryEditorAc
|
||||
SaveAction(enabled = state?.canSave == true, onClick = actions.onSave)
|
||||
}
|
||||
},
|
||||
message = state?.message,
|
||||
onMessageShown = actions.onMessageShown,
|
||||
snackbarHostState = snackbar,
|
||||
) { padding ->
|
||||
if (state == null) return@SettingsScaffold
|
||||
if (state == null) return@KvittScaffold
|
||||
val defaultName = state.defaultKey?.let { stringResource(defaultCategoryName(it)) }
|
||||
val shownName = state.name.trim().ifEmpty { defaultName ?: "" }
|
||||
Column(
|
||||
|
||||
+8
-5
@@ -19,6 +19,7 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -52,6 +53,7 @@ import se.hagstedt.kvitt.design.KvittTheme
|
||||
import se.hagstedt.kvitt.design.component.CategoryTile
|
||||
import se.hagstedt.kvitt.design.component.CollapsibleRow
|
||||
import se.hagstedt.kvitt.design.component.KvittIconImage
|
||||
import se.hagstedt.kvitt.design.component.KvittScaffold
|
||||
import se.hagstedt.kvitt.design.component.KvittTopAppBar
|
||||
import se.hagstedt.kvitt.design.component.NeedsConnectionHint
|
||||
import se.hagstedt.kvitt.design.component.SegmentedGroup
|
||||
@@ -61,7 +63,7 @@ import se.hagstedt.kvitt.design.component.categoryName
|
||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||
import se.hagstedt.kvitt.feature.settings.R
|
||||
import se.hagstedt.kvitt.feature.settings.common.SettingsScaffold
|
||||
import se.hagstedt.kvitt.feature.settings.common.SettingsMessageEffect
|
||||
import se.hagstedt.kvitt.model.Category
|
||||
import se.hagstedt.kvitt.model.CategoryId
|
||||
|
||||
@@ -118,7 +120,9 @@ class CategoryListActions(
|
||||
/** Stateless S-15 list. */
|
||||
@Composable
|
||||
fun CategoryListContent(state: CategoryListState?, actions: CategoryListActions, modifier: Modifier = Modifier) {
|
||||
SettingsScaffold(
|
||||
val snackbar = remember { SnackbarHostState() }
|
||||
SettingsMessageEffect(state?.message, snackbar, actions.onMessageShown)
|
||||
KvittScaffold(
|
||||
modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
KvittTopAppBar(stringResource(R.string.settings_categories_title), navigation = TopBarNavigation.Back, onNavigation = actions.onBack) {
|
||||
@@ -127,10 +131,9 @@ fun CategoryListContent(state: CategoryListState?, actions: CategoryListActions,
|
||||
}
|
||||
}
|
||||
},
|
||||
message = state?.message,
|
||||
onMessageShown = actions.onMessageShown,
|
||||
snackbarHostState = snackbar,
|
||||
) { padding ->
|
||||
if (state == null) return@SettingsScaffold
|
||||
if (state == null) return@KvittScaffold
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
|
||||
+18
-51
@@ -1,24 +1,14 @@
|
||||
package se.hagstedt.kvitt.feature.settings.common
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.LiveRegionMode
|
||||
import androidx.compose.ui.semantics.liveRegion
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import kotlinx.coroutines.delay
|
||||
import se.hagstedt.kvitt.design.KvittTheme
|
||||
import se.hagstedt.kvitt.design.component.KvittScaffold
|
||||
import se.hagstedt.kvitt.design.component.KvittSnackbar
|
||||
import se.hagstedt.kvitt.design.component.snackbarMillis
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import se.hagstedt.kvitt.design.component.showKvittSnackbar
|
||||
import se.hagstedt.kvitt.model.OnlineResult
|
||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
||||
import se.hagstedt.kvitt.feature.settings.R
|
||||
@@ -77,42 +67,19 @@ fun failureText(code: String): Int = when (code) {
|
||||
fun failureMessage(code: String, canRetry: Boolean = true): UiMessage = UiMessage(failureText(code), retry = canRetry && code == NETWORK)
|
||||
|
||||
/**
|
||||
* The settings screens' scaffold: [KvittScaffold] plus the one-shot snackbar of [message] (design-spec §3.11:
|
||||
* `inverse-surface`, 4 s, 10 s with "Försök igen"), drawn at the bottom above the bars. The text is resolved while
|
||||
* shown, so it follows the app language. [onMessageShown] when it times out or its action is used.
|
||||
* Shows [message] once in [host] (design-spec §3.11: 4 s, 10 s with "Försök igen") through [showKvittSnackbar], so its
|
||||
* text follows the app language while it shows. [onMessageShown] when it times out or its action is used; the action
|
||||
* then runs [onRetry].
|
||||
*/
|
||||
@Composable
|
||||
fun SettingsScaffold(
|
||||
modifier: Modifier = Modifier,
|
||||
topBar: @Composable () -> Unit = {},
|
||||
bottomBar: @Composable () -> Unit = {},
|
||||
floatingActionButton: @Composable () -> Unit = {},
|
||||
message: UiMessage? = null,
|
||||
onMessageShown: () -> Unit = {},
|
||||
onRetry: () -> Unit = {},
|
||||
content: @Composable (PaddingValues) -> Unit,
|
||||
) {
|
||||
Box(modifier.fillMaxSize()) {
|
||||
KvittScaffold(Modifier.fillMaxSize(), topBar = topBar, bottomBar = bottomBar, floatingActionButton = floatingActionButton, content = content)
|
||||
if (message != null) {
|
||||
LaunchedEffect(message.id) {
|
||||
delay(snackbarMillis(message.retry))
|
||||
onMessageShown()
|
||||
}
|
||||
val text = stringResource(message.text, *message.args.toTypedArray())
|
||||
KvittSnackbar(
|
||||
text,
|
||||
Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.navigationBarsPadding()
|
||||
.padding(KvittTheme.spacing.s4)
|
||||
.semantics { liveRegion = LiveRegionMode.Polite },
|
||||
action = if (message.retry) stringResource(DesignR.string.common_retry) else null,
|
||||
onAction = {
|
||||
onMessageShown()
|
||||
onRetry()
|
||||
},
|
||||
)
|
||||
}
|
||||
fun SettingsMessageEffect(message: UiMessage?, host: SnackbarHostState, onMessageShown: () -> Unit, onRetry: () -> Unit = {}) {
|
||||
val resources = LocalResources.current
|
||||
val shown by rememberUpdatedState(onMessageShown)
|
||||
val retry by rememberUpdatedState(onRetry)
|
||||
LaunchedEffect(message?.id) {
|
||||
if (message == null) return@LaunchedEffect
|
||||
val result = host.showKvittSnackbar(resources, message.text, message.args, if (message.retry) DesignR.string.common_retry else null)
|
||||
shown()
|
||||
if (result == SnackbarResult.ActionPerformed) retry()
|
||||
}
|
||||
}
|
||||
|
||||
+9
-5
@@ -15,9 +15,11 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -33,6 +35,7 @@ import se.hagstedt.kvitt.design.component.ConfirmDialog
|
||||
import se.hagstedt.kvitt.design.component.KvittBadge
|
||||
import se.hagstedt.kvitt.design.component.KvittButton
|
||||
import se.hagstedt.kvitt.design.component.KvittIconImage
|
||||
import se.hagstedt.kvitt.design.component.KvittScaffold
|
||||
import se.hagstedt.kvitt.design.component.KvittTopAppBar
|
||||
import se.hagstedt.kvitt.design.component.PersonAvatar
|
||||
import se.hagstedt.kvitt.design.component.ProvideHousehold
|
||||
@@ -48,7 +51,7 @@ import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||
import se.hagstedt.kvitt.feature.settings.R
|
||||
import se.hagstedt.kvitt.feature.settings.common.CentredStatus
|
||||
import se.hagstedt.kvitt.feature.settings.common.HintText
|
||||
import se.hagstedt.kvitt.feature.settings.common.SettingsScaffold
|
||||
import se.hagstedt.kvitt.feature.settings.common.SettingsMessageEffect
|
||||
import se.hagstedt.kvitt.model.ColorToken
|
||||
import se.hagstedt.kvitt.model.Device
|
||||
import se.hagstedt.kvitt.model.User
|
||||
@@ -73,13 +76,14 @@ class DevicesActions(
|
||||
/** Stateless S-20. */
|
||||
@Composable
|
||||
fun DevicesContent(state: DevicesState?, actions: DevicesActions, modifier: Modifier = Modifier) {
|
||||
SettingsScaffold(
|
||||
val snackbar = remember { SnackbarHostState() }
|
||||
SettingsMessageEffect(state?.message, snackbar, actions.onMessageShown)
|
||||
KvittScaffold(
|
||||
modifier.fillMaxSize(),
|
||||
topBar = { KvittTopAppBar(stringResource(R.string.settings_devices_title), navigation = TopBarNavigation.Back, onNavigation = actions.onBack) },
|
||||
message = state?.message,
|
||||
onMessageShown = actions.onMessageShown,
|
||||
snackbarHostState = snackbar,
|
||||
) { padding ->
|
||||
if (state == null) return@SettingsScaffold
|
||||
if (state == null) return@KvittScaffold
|
||||
ProvideHousehold(state.me, state.partner) {
|
||||
Column(
|
||||
Modifier
|
||||
|
||||
+3
-3
@@ -29,6 +29,7 @@ import se.hagstedt.kvitt.design.component.KvittCard
|
||||
import se.hagstedt.kvitt.design.component.KvittDestination
|
||||
import se.hagstedt.kvitt.design.component.KvittDivider
|
||||
import se.hagstedt.kvitt.design.component.KvittNavigationBar
|
||||
import se.hagstedt.kvitt.design.component.KvittScaffold
|
||||
import se.hagstedt.kvitt.design.component.KvittTopAppBar
|
||||
import se.hagstedt.kvitt.design.component.PersonAvatar
|
||||
import se.hagstedt.kvitt.design.component.ProvideHousehold
|
||||
@@ -38,7 +39,6 @@ import se.hagstedt.kvitt.design.component.syncLineText
|
||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||
import se.hagstedt.kvitt.feature.settings.R
|
||||
import se.hagstedt.kvitt.feature.settings.common.SettingsScaffold
|
||||
import se.hagstedt.kvitt.model.ColorToken
|
||||
import se.hagstedt.kvitt.model.Role
|
||||
import se.hagstedt.kvitt.model.User
|
||||
@@ -77,12 +77,12 @@ class SettingsHubActions(
|
||||
/** Stateless S-13; [state] null while the session loads (nothing but the bars). */
|
||||
@Composable
|
||||
fun SettingsHubContent(state: SettingsHubState?, actions: SettingsHubActions, modifier: Modifier = Modifier) {
|
||||
SettingsScaffold(
|
||||
KvittScaffold(
|
||||
modifier.fillMaxSize(),
|
||||
topBar = { KvittTopAppBar(stringResource(R.string.settings_title)) },
|
||||
bottomBar = { KvittNavigationBar(KvittDestination.Settings, actions.onNavigate) },
|
||||
) { padding ->
|
||||
if (state == null) return@SettingsScaffold
|
||||
if (state == null) return@KvittScaffold
|
||||
ProvideHousehold(state.me, state.partner) {
|
||||
Column(
|
||||
Modifier
|
||||
|
||||
+9
-6
@@ -18,9 +18,11 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -44,6 +46,7 @@ import se.hagstedt.kvitt.design.component.InviteCodeDisplay
|
||||
import se.hagstedt.kvitt.design.component.KvittButton
|
||||
import se.hagstedt.kvitt.design.component.KvittCard
|
||||
import se.hagstedt.kvitt.design.component.KvittIconImage
|
||||
import se.hagstedt.kvitt.design.component.KvittScaffold
|
||||
import se.hagstedt.kvitt.design.component.KvittTopAppBar
|
||||
import se.hagstedt.kvitt.design.component.LocalKvittClock
|
||||
import se.hagstedt.kvitt.design.component.ProvideHousehold
|
||||
@@ -58,8 +61,8 @@ import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||
import se.hagstedt.kvitt.feature.settings.R
|
||||
import se.hagstedt.kvitt.feature.settings.common.HintText
|
||||
import se.hagstedt.kvitt.feature.settings.common.SettingsScaffold
|
||||
import se.hagstedt.kvitt.feature.settings.common.OfflineBanner
|
||||
import se.hagstedt.kvitt.feature.settings.common.SettingsMessageEffect
|
||||
import se.hagstedt.kvitt.model.ColorToken
|
||||
import se.hagstedt.kvitt.model.Invite
|
||||
import se.hagstedt.kvitt.model.InviteKind
|
||||
@@ -121,17 +124,17 @@ class InvitesActions(
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun InvitesContent(state: InvitesState?, actions: InvitesActions, modifier: Modifier = Modifier) {
|
||||
SettingsScaffold(
|
||||
val snackbar = remember { SnackbarHostState() }
|
||||
SettingsMessageEffect(state?.message, snackbar, actions.onMessageShown, actions.onRetry)
|
||||
KvittScaffold(
|
||||
modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
val title = if (state != null && state.partner == null) stringResource(DesignR.string.common_invite_partner) else stringResource(R.string.settings_invites_title_device)
|
||||
KvittTopAppBar(title, navigation = TopBarNavigation.Back, onNavigation = actions.onBack)
|
||||
},
|
||||
message = state?.message,
|
||||
onMessageShown = actions.onMessageShown,
|
||||
onRetry = actions.onRetry,
|
||||
snackbarHostState = snackbar,
|
||||
) { padding ->
|
||||
if (state == null) return@SettingsScaffold
|
||||
if (state == null) return@KvittScaffold
|
||||
ProvideHousehold(state.me, state.partner) {
|
||||
Column(
|
||||
Modifier
|
||||
|
||||
+9
-6
@@ -13,9 +13,11 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -33,6 +35,7 @@ import se.hagstedt.kvitt.design.component.AvatarShape
|
||||
import se.hagstedt.kvitt.design.component.AvatarState
|
||||
import se.hagstedt.kvitt.design.component.ButtonKind
|
||||
import se.hagstedt.kvitt.design.component.KvittButton
|
||||
import se.hagstedt.kvitt.design.component.KvittScaffold
|
||||
import se.hagstedt.kvitt.design.component.KvittTextField
|
||||
import se.hagstedt.kvitt.design.component.KvittTopAppBar
|
||||
import se.hagstedt.kvitt.design.component.PersonAvatar
|
||||
@@ -41,11 +44,11 @@ import se.hagstedt.kvitt.design.component.TopBarNavigation
|
||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||
import se.hagstedt.kvitt.feature.settings.R
|
||||
import se.hagstedt.kvitt.feature.settings.common.SettingsScaffold
|
||||
import se.hagstedt.kvitt.feature.settings.common.OfflineBanner
|
||||
import se.hagstedt.kvitt.feature.settings.common.SaveAction
|
||||
import java.io.IOException
|
||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
||||
import se.hagstedt.kvitt.feature.settings.common.SettingsMessageEffect
|
||||
|
||||
/** S-14 Profile. [onBack] after a saved name and on the back arrow. */
|
||||
@Composable
|
||||
@@ -105,18 +108,18 @@ class ProfileActions(
|
||||
/** Stateless S-14. */
|
||||
@Composable
|
||||
fun ProfileContent(state: ProfileState?, actions: ProfileActions, modifier: Modifier = Modifier) {
|
||||
SettingsScaffold(
|
||||
val snackbar = remember { SnackbarHostState() }
|
||||
SettingsMessageEffect(state?.message, snackbar, actions.onMessageShown, actions.onRetry)
|
||||
KvittScaffold(
|
||||
modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
KvittTopAppBar(stringResource(R.string.settings_profile), navigation = TopBarNavigation.Back, onNavigation = actions.onBack) {
|
||||
SaveAction(enabled = state?.canSave == true, onClick = actions.onSave)
|
||||
}
|
||||
},
|
||||
message = state?.message,
|
||||
onMessageShown = actions.onMessageShown,
|
||||
onRetry = actions.onRetry,
|
||||
snackbarHostState = snackbar,
|
||||
) { padding ->
|
||||
if (state == null) return@SettingsScaffold
|
||||
if (state == null) return@KvittScaffold
|
||||
ProvideHousehold(state.me, state.partner) {
|
||||
Column(
|
||||
Modifier
|
||||
|
||||
+8
-6
@@ -19,6 +19,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
@@ -57,6 +58,7 @@ import se.hagstedt.kvitt.design.component.ExactSplitEditor
|
||||
import se.hagstedt.kvitt.design.component.FullSplitPicker
|
||||
import se.hagstedt.kvitt.design.component.KvittButton
|
||||
import se.hagstedt.kvitt.design.component.KvittIconImage
|
||||
import se.hagstedt.kvitt.design.component.KvittScaffold
|
||||
import se.hagstedt.kvitt.design.component.KvittTextField
|
||||
import se.hagstedt.kvitt.design.component.KvittTopAppBar
|
||||
import se.hagstedt.kvitt.design.component.NotesField
|
||||
@@ -79,9 +81,9 @@ import se.hagstedt.kvitt.design.format.KvittLanguage
|
||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||
import se.hagstedt.kvitt.feature.settings.R
|
||||
import se.hagstedt.kvitt.feature.settings.common.HintText
|
||||
import se.hagstedt.kvitt.feature.settings.common.SettingsScaffold
|
||||
import se.hagstedt.kvitt.feature.settings.common.OfflineBanner
|
||||
import se.hagstedt.kvitt.feature.settings.common.SaveAction
|
||||
import se.hagstedt.kvitt.feature.settings.common.SettingsMessageEffect
|
||||
import se.hagstedt.kvitt.model.AccountId
|
||||
import se.hagstedt.kvitt.model.CategoryId
|
||||
import se.hagstedt.kvitt.model.SplitMode
|
||||
@@ -165,7 +167,9 @@ class RecurringEditorActions(
|
||||
@Composable
|
||||
fun RecurringEditorContent(state: RecurringEditorState?, actions: RecurringEditorActions, modifier: Modifier = Modifier) {
|
||||
val keypad = state != null && state.form.focus != EditorFocus.None
|
||||
SettingsScaffold(
|
||||
val snackbar = remember { SnackbarHostState() }
|
||||
SettingsMessageEffect(state?.message, snackbar, actions.onMessageShown, actions.onRetry)
|
||||
KvittScaffold(
|
||||
modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
val title = if (state?.isNew != false) R.string.settings_recurring_editor_new else R.string.settings_recurring_editor_edit
|
||||
@@ -176,11 +180,9 @@ fun RecurringEditorContent(state: RecurringEditorState?, actions: RecurringEdito
|
||||
bottomBar = {
|
||||
if (keypad) AmountKeypad(actions.onKey, Modifier.navigationBarsPadding(), compact = state.form.splitOpen)
|
||||
},
|
||||
message = state?.message,
|
||||
onMessageShown = actions.onMessageShown,
|
||||
onRetry = actions.onRetry,
|
||||
snackbarHostState = snackbar,
|
||||
) { padding ->
|
||||
if (state == null) return@SettingsScaffold
|
||||
if (state == null) return@KvittScaffold
|
||||
ProvideHousehold(state.options.me, state.options.partner) {
|
||||
EditorBody(state, actions, Modifier.padding(padding))
|
||||
when (state.sheet) {
|
||||
|
||||
+3
-3
@@ -38,6 +38,7 @@ import se.hagstedt.kvitt.design.component.BadgeKind
|
||||
import se.hagstedt.kvitt.design.component.CategoryTile
|
||||
import se.hagstedt.kvitt.design.component.EmptyState
|
||||
import se.hagstedt.kvitt.design.component.KvittBadge
|
||||
import se.hagstedt.kvitt.design.component.KvittScaffold
|
||||
import se.hagstedt.kvitt.design.component.KvittTopAppBar
|
||||
import se.hagstedt.kvitt.design.component.LocalKvittClock
|
||||
import se.hagstedt.kvitt.design.component.LocalKvittHousehold
|
||||
@@ -53,7 +54,6 @@ import se.hagstedt.kvitt.design.format.DateFormatter
|
||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||
import se.hagstedt.kvitt.feature.settings.R
|
||||
import se.hagstedt.kvitt.feature.settings.common.SettingsScaffold
|
||||
import se.hagstedt.kvitt.feature.settings.common.OfflineBanner
|
||||
import se.hagstedt.kvitt.model.TemplateId
|
||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
||||
@@ -77,14 +77,14 @@ class RecurringListActions(val onBack: () -> Unit = {}, val onOpen: (TemplateId)
|
||||
@Composable
|
||||
fun RecurringListContent(state: RecurringListState?, actions: RecurringListActions, modifier: Modifier = Modifier) {
|
||||
val empty = state != null && state.rows.isEmpty()
|
||||
SettingsScaffold(
|
||||
KvittScaffold(
|
||||
modifier.fillMaxSize(),
|
||||
topBar = { KvittTopAppBar(stringResource(R.string.settings_recurring_title), navigation = TopBarNavigation.Back, onNavigation = actions.onBack) },
|
||||
floatingActionButton = {
|
||||
if (state != null && !empty) AddExpenseFab(actions.onNew, text = stringResource(R.string.settings_recurring_new), enabled = state.online)
|
||||
},
|
||||
) { padding ->
|
||||
if (state == null) return@SettingsScaffold
|
||||
if (state == null) return@KvittScaffold
|
||||
ProvideHousehold(state.me, state.partner) {
|
||||
Column(
|
||||
Modifier
|
||||
|
||||
+2
-1
@@ -123,7 +123,8 @@ fun SettlementDetailContent(
|
||||
action = stringResource(R.string.settle_detail_sync_now),
|
||||
actionIcon = KvittIcon.Sync,
|
||||
actionKind = ButtonKind.Tonal,
|
||||
onAction = actions::onSyncNow,
|
||||
actionEnabled = state.online,
|
||||
onAction = { if (state.online) actions.onSyncNow() },
|
||||
)
|
||||
else -> DetailBody(state, detail)
|
||||
}
|
||||
|
||||
+8
-3
@@ -16,6 +16,7 @@ import se.hagstedt.kvitt.model.Account
|
||||
import se.hagstedt.kvitt.model.AccountId
|
||||
import se.hagstedt.kvitt.model.AccountRepository
|
||||
import se.hagstedt.kvitt.model.AuditAction
|
||||
import se.hagstedt.kvitt.model.ConnectivityRepository
|
||||
import se.hagstedt.kvitt.model.HomeRepository
|
||||
import se.hagstedt.kvitt.model.SettlementDetailState
|
||||
import se.hagstedt.kvitt.model.SettlementId
|
||||
@@ -25,7 +26,8 @@ import se.hagstedt.kvitt.model.TimelineLine
|
||||
import se.hagstedt.kvitt.model.User
|
||||
|
||||
/**
|
||||
* Everything S-10 shows. [detail] null after loading is the missing state (the S-8 missing layout, design-spec S-10).
|
||||
* Everything S-10 shows. [detail] null after loading is the missing state (the S-8 missing layout, design-spec S-10);
|
||||
* its "Synka nu" needs [online].
|
||||
*/
|
||||
data class SettlementDetailUiState(
|
||||
val loading: Boolean = true,
|
||||
@@ -35,6 +37,7 @@ data class SettlementDetailUiState(
|
||||
val accounts: List<Account> = emptyList(),
|
||||
val confirmDelete: Boolean = false,
|
||||
val deletedByMe: Boolean = false,
|
||||
val online: Boolean = true,
|
||||
) {
|
||||
/** Design-spec §2.4: an account's colour is its position in `AccountRepository.all()`. */
|
||||
fun colorIndex(id: AccountId): Int = accounts.indexOfFirst { it.id == id }.coerceAtLeast(0)
|
||||
@@ -61,6 +64,7 @@ class SettlementDetailViewModel @javax.inject.Inject constructor(
|
||||
private val settlements: SettlementRepository,
|
||||
accounts: AccountRepository,
|
||||
home: HomeRepository,
|
||||
connectivity: ConnectivityRepository,
|
||||
private val sync: SyncRepository,
|
||||
) : ViewModel(), SettlementDetailActions {
|
||||
val id: SettlementId? = savedStateHandle.get<String>(SettleArgs.SETTLEMENT_ID)?.let(::SettlementId)
|
||||
@@ -70,9 +74,10 @@ class SettlementDetailViewModel @javax.inject.Inject constructor(
|
||||
id?.let { settlements.detail(it) } ?: flowOf(null),
|
||||
accounts.all(),
|
||||
home.home(),
|
||||
connectivity.online,
|
||||
local,
|
||||
) { detail, all, h, l ->
|
||||
l.copy(loading = false, detail = detail, accounts = all, me = h.me, partner = h.partner)
|
||||
) { detail, all, h, online, l ->
|
||||
l.copy(loading = false, detail = detail, accounts = all, me = h.me, partner = h.partner, online = online)
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(STOP_TIMEOUT_MS), SettlementDetailUiState())
|
||||
|
||||
override fun onDelete() = local.update { it.copy(confirmDelete = true) }
|
||||
|
||||
+9
-1
@@ -30,7 +30,7 @@ class SettlementDetailViewModelTest {
|
||||
fun reset() = Dispatchers.resetMain()
|
||||
|
||||
private fun vm(id: String, repos: FakeRepositories = FakeRepositories(SettleScenarios.detail)) =
|
||||
SettlementDetailViewModel(SavedStateHandle(mapOf(SettleArgs.SETTLEMENT_ID to id)), repos.settlements, repos.accounts, repos.home, repos.sync)
|
||||
SettlementDetailViewModel(SavedStateHandle(mapOf(SettleArgs.SETTLEMENT_ID to id)), repos.settlements, repos.accounts, repos.home, repos.connectivity, repos.sync)
|
||||
|
||||
@Test
|
||||
fun loadedShowsTheSettlementAndItsActions() = runTest {
|
||||
@@ -48,6 +48,14 @@ class SettlementDetailViewModelTest {
|
||||
assertFalse(s.actions)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun missingOfflineCarriesTheConnectivityForSyncNow() = runTest {
|
||||
assertTrue(vm(SettleScenarios.missing.value).state.first { !it.loading }.online)
|
||||
val offline = vm(SettleScenarios.missing.value, FakeRepositories(SettleScenarios.detailOffline)).state.first { !it.loading }
|
||||
assertNull(offline.detail)
|
||||
assertFalse(offline.online)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deletedHasTheDeletionLineAndNoActions() = runTest {
|
||||
val s = vm(SettleScenarios.deleted.value).state.first { !it.loading }
|
||||
|
||||
+2
-1
@@ -78,6 +78,7 @@ private val cases: Map<String, Case> = linkedMapOf(
|
||||
"detail_offline" to Detail(SettleScenarios.detailOffline, SettleScenarios.split.value),
|
||||
"detail_delete_dialog" to Detail(SettleScenarios.detail, SettleScenarios.swish.value) { onDelete() },
|
||||
"detail_missing" to Detail(SettleScenarios.detail, SettleScenarios.missing.value),
|
||||
"detail_missing_offline" to Detail(SettleScenarios.detailOffline, SettleScenarios.missing.value),
|
||||
"detail_long" to Detail(SettleScenarios.longDetail, SettleScenarios.long.value, height = longHeight),
|
||||
)
|
||||
|
||||
@@ -105,7 +106,7 @@ class SettleScreenshotTest(private val name: String) {
|
||||
is Detail -> {
|
||||
val repos = FakeRepositories(case.scenario)
|
||||
val vm = SettlementDetailViewModel(
|
||||
SavedStateHandle(mapOf(SettleArgs.SETTLEMENT_ID to case.id)), repos.settlements, repos.accounts, repos.home, repos.sync,
|
||||
SavedStateHandle(mapOf(SettleArgs.SETTLEMENT_ID to case.id)), repos.settlements, repos.accounts, repos.home, repos.connectivity, repos.sync,
|
||||
)
|
||||
vm.apply(case.steps)
|
||||
ScreenSnapshots.snap(compose, name, case.height, case.largeFont) {
|
||||
|
||||
Reference in New Issue
Block a user