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 {
|
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.kotlinx.coroutines.test)
|
||||||
testImplementation(libs.androidx.compose.ui.test.junit4)
|
testImplementation(libs.androidx.compose.ui.test.junit4)
|
||||||
testImplementation(libs.robolectric)
|
testImplementation(libs.robolectric)
|
||||||
|
|||||||
+12
-10
@@ -10,7 +10,6 @@ import androidx.compose.foundation.lazy.LazyListState
|
|||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SnackbarDuration
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -22,6 +21,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.window.Popup
|
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.TopBarNavigation
|
||||||
import se.hagstedt.kvitt.design.component.kvittLanguage
|
import se.hagstedt.kvitt.design.component.kvittLanguage
|
||||||
import se.hagstedt.kvitt.design.component.ratioSpoken
|
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.format.MoneyFormatter
|
||||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||||
import se.hagstedt.kvitt.model.AccountId
|
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. */
|
/** Shows [message] once (4 s, 10 s with an action, design-spec §3.11); "Försök igen" repeats the failed action. */
|
||||||
@Composable
|
@Composable
|
||||||
private fun MessageEffect(message: AccountMessage, host: SnackbarHostState, actions: AccountDetailActions) {
|
private fun MessageEffect(message: AccountMessage, host: SnackbarHostState, actions: AccountDetailActions) {
|
||||||
val text = when (message) {
|
val resources = LocalResources.current
|
||||||
is AccountMessage.CouldNotReach -> stringResource(DesignR.string.common_could_not_reach_server)
|
LaunchedEffect(message) {
|
||||||
is AccountMessage.Restored -> stringResource(R.string.account_unarchived_snackbar, message.name)
|
val (text, args) = when (message) {
|
||||||
AccountMessage.DeleteBlocked -> stringResource(R.string.account_delete_blocked)
|
is AccountMessage.CouldNotReach -> DesignR.string.common_could_not_reach_server to emptyList()
|
||||||
AccountMessage.NeedsConnection -> stringResource(DesignR.string.common_needs_connection)
|
is AccountMessage.Restored -> R.string.account_unarchived_snackbar to listOf<Any>(message.name)
|
||||||
is AccountMessage.Failed -> errorText(message.code)
|
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) stringResource(DesignR.string.common_retry) else null
|
val action = if (message is AccountMessage.CouldNotReach) DesignR.string.common_retry else null
|
||||||
LaunchedEffect(message, text, action) {
|
val result = host.showKvittSnackbar(resources, text, args, action)
|
||||||
val result = host.showSnackbar(text, action, duration = if (action != null) SnackbarDuration.Long else SnackbarDuration.Short)
|
|
||||||
if (result == SnackbarResult.ActionPerformed && message is AccountMessage.CouldNotReach) actions.onRetry(message.retry)
|
if (result == SnackbarResult.ActionPerformed && message is AccountMessage.CouldNotReach) actions.onRetry(message.retry)
|
||||||
actions.onMessageShown()
|
actions.onMessageShown()
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-13
@@ -1,5 +1,6 @@
|
|||||||
package se.hagstedt.kvitt.feature.account
|
package se.hagstedt.kvitt.feature.account
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SnackbarDuration
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.compose.material3.Text
|
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.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
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.kvittLanguage
|
||||||
import se.hagstedt.kvitt.design.component.money
|
import se.hagstedt.kvitt.design.component.money
|
||||||
import se.hagstedt.kvitt.design.component.ratioSpoken
|
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.format.MoneyFormatter
|
||||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||||
@@ -90,6 +90,8 @@ fun AccountEditorScreen(
|
|||||||
viewModel.savedHandled()
|
viewModel.savedHandled()
|
||||||
onSaved(saved)
|
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(
|
val actions = AccountEditorActions(
|
||||||
onClose = { if (viewModel.requestClose()) onClose() },
|
onClose = { if (viewModel.requestClose()) onClose() },
|
||||||
onName = viewModel::onName,
|
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
|
@Composable
|
||||||
private fun SaveAction(ui: AccountEditorUi, actions: AccountEditorActions) {
|
private fun SaveAction(ui: AccountEditorUi, actions: AccountEditorActions) {
|
||||||
Row(Modifier.padding(end = KvittTheme.spacing.s2), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(KvittTheme.spacing.s2)) {
|
KvittButton(
|
||||||
if (ui.saving) CircularProgressIndicator(Modifier.size(KvittDimens.iconButton), strokeWidth = KvittDimens.placeholderBorder)
|
stringResource(DesignR.string.common_save),
|
||||||
KvittButton(stringResource(DesignR.string.common_save), actions.onSave, enabled = ui.canSave)
|
actions.onSave,
|
||||||
}
|
Modifier.padding(end = KvittTheme.spacing.s2),
|
||||||
|
enabled = ui.canSave || ui.saving,
|
||||||
|
loading = ui.saving,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -293,14 +298,15 @@ private const val PREVIEW_ORE = BP_TOTAL.toLong()
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun EditorMessageEffect(message: EditorMessage, host: SnackbarHostState, actions: AccountEditorActions) {
|
private fun EditorMessageEffect(message: EditorMessage, host: SnackbarHostState, actions: AccountEditorActions) {
|
||||||
|
val resources = LocalResources.current
|
||||||
|
LaunchedEffect(message) {
|
||||||
val text = when (message) {
|
val text = when (message) {
|
||||||
EditorMessage.CouldNotReach -> stringResource(DesignR.string.common_could_not_reach_server)
|
EditorMessage.CouldNotReach -> DesignR.string.common_could_not_reach_server
|
||||||
EditorMessage.NeedsConnection -> stringResource(DesignR.string.common_needs_connection)
|
EditorMessage.NeedsConnection -> DesignR.string.common_needs_connection
|
||||||
is EditorMessage.Failed -> errorText(message.code)
|
is EditorMessage.Failed -> errorText(message.code)
|
||||||
}
|
}
|
||||||
val action = if (message == EditorMessage.CouldNotReach) stringResource(DesignR.string.common_retry) else null
|
val action = if (message == EditorMessage.CouldNotReach) DesignR.string.common_retry else null
|
||||||
LaunchedEffect(message, text, action) {
|
val result = host.showKvittSnackbar(resources, text, action = action)
|
||||||
val result = host.showSnackbar(text, action, duration = if (action != null) SnackbarDuration.Long else SnackbarDuration.Short)
|
|
||||||
if (result == SnackbarResult.ActionPerformed) actions.onSave()
|
if (result == SnackbarResult.ActionPerformed) actions.onSave()
|
||||||
actions.onMessageShown()
|
actions.onMessageShown()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
package se.hagstedt.kvitt.feature.account
|
package se.hagstedt.kvitt.feature.account
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
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_*`). */
|
/** The snackbar text of an `OnlineResult.Failed` code without a field at fault (§14.3.2, strings.csv `error_*`). */
|
||||||
@Composable
|
@StringRes
|
||||||
@ReadOnlyComposable
|
internal fun errorText(code: String): Int = when (code) {
|
||||||
internal fun errorText(code: String): String = stringResource(
|
|
||||||
when (code) {
|
|
||||||
NETWORK -> DesignR.string.common_could_not_reach_server
|
NETWORK -> DesignR.string.common_could_not_reach_server
|
||||||
"household_incomplete" -> DesignR.string.error_household_incomplete
|
"household_incomplete" -> DesignR.string.error_household_incomplete
|
||||||
"not_found" -> DesignR.string.error_not_found
|
"not_found" -> DesignR.string.error_not_found
|
||||||
@@ -20,5 +16,4 @@ internal fun errorText(code: String): String = stringResource(
|
|||||||
"validation_failed" -> DesignR.string.error_validation
|
"validation_failed" -> DesignR.string.error_validation
|
||||||
"account_not_empty" -> R.string.account_delete_blocked
|
"account_not_empty" -> R.string.account_delete_blocked
|
||||||
else -> DesignR.string.error_generic_invalid
|
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) }
|
if (summary.clockSkew) item(key = "clock") { ClockBanner(actions.onOpenDateSettings) }
|
||||||
item(key = "status") { StatusCard(ui) }
|
item(key = "status") { StatusCard(ui) }
|
||||||
item(key = "sync-now") {
|
item(key = "sync-now") {
|
||||||
|
val syncing = ui.online && summary.running
|
||||||
Column {
|
Column {
|
||||||
KvittButton(
|
KvittButton(
|
||||||
stringResource(R.string.home_sync_status_sync_now),
|
stringResource(R.string.home_sync_status_sync_now),
|
||||||
@@ -141,7 +142,8 @@ private fun SyncStatusList(ui: SyncStatusUi.Ready, actions: SyncStatusActions, p
|
|||||||
Modifier.fillMaxWidth(),
|
Modifier.fillMaxWidth(),
|
||||||
size = ButtonSize.Medium,
|
size = ButtonSize.Medium,
|
||||||
icon = KvittIcon.Sync,
|
icon = KvittIcon.Sync,
|
||||||
enabled = ui.canSyncNow,
|
enabled = ui.canSyncNow || syncing,
|
||||||
|
loading = syncing,
|
||||||
)
|
)
|
||||||
if (!ui.online) NeedsConnectionHint()
|
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.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SnackbarDuration
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -27,6 +26,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalClipboard
|
import androidx.compose.ui.platform.LocalClipboard
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.LiveRegionMode
|
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.KvittIconImage
|
||||||
import se.hagstedt.kvitt.design.component.KvittScaffold
|
import se.hagstedt.kvitt.design.component.KvittScaffold
|
||||||
import se.hagstedt.kvitt.design.component.KvittTextField
|
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.icon.KvittIcon
|
||||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||||
|
|
||||||
@@ -113,14 +114,15 @@ fun PairingContent(
|
|||||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||||
) {
|
) {
|
||||||
val snackbarText = when (state.error) {
|
val snackbarText = when (state.error) {
|
||||||
PairingError.AppTooOld -> stringResource(DesignR.string.error_app_too_old)
|
PairingError.AppTooOld -> DesignR.string.error_app_too_old
|
||||||
PairingError.Server -> stringResource(DesignR.string.error_server)
|
PairingError.Server -> DesignR.string.error_server
|
||||||
PairingError.Validation -> stringResource(DesignR.string.error_validation)
|
PairingError.Validation -> DesignR.string.error_validation
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
val resources = LocalResources.current
|
||||||
LaunchedEffect(snackbarText) {
|
LaunchedEffect(snackbarText) {
|
||||||
if (snackbarText != null) {
|
if (snackbarText != null) {
|
||||||
snackbarHostState.showSnackbar(snackbarText, duration = SnackbarDuration.Short)
|
snackbarHostState.showKvittSnackbar(resources, snackbarText)
|
||||||
actions.onErrorShown()
|
actions.onErrorShown()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-3
@@ -22,7 +22,6 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SnackbarDuration
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -33,6 +32,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
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.PersonAvatar
|
||||||
import se.hagstedt.kvitt.design.component.ProvideHousehold
|
import se.hagstedt.kvitt.design.component.ProvideHousehold
|
||||||
import se.hagstedt.kvitt.design.component.kvittLanguage
|
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.format.KvittLanguage
|
||||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||||
@@ -130,10 +131,11 @@ fun ProfileStepContent(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||||
) {
|
) {
|
||||||
val message = state.message?.let { stringResource(it.text) }
|
val message = state.message?.text
|
||||||
|
val resources = LocalResources.current
|
||||||
LaunchedEffect(message) {
|
LaunchedEffect(message) {
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
snackbarHostState.showSnackbar(message, duration = SnackbarDuration.Short)
|
snackbarHostState.showKvittSnackbar(resources, message)
|
||||||
actions.onMessageShown()
|
actions.onMessageShown()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-4
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.statusBarsPadding
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SnackbarDuration
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -22,6 +21,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
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.ProvideHousehold
|
||||||
import se.hagstedt.kvitt.design.component.SegmentedGroup
|
import se.hagstedt.kvitt.design.component.SegmentedGroup
|
||||||
import se.hagstedt.kvitt.design.component.kvittLanguage
|
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.format.DateFormatter
|
||||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
@@ -83,12 +84,12 @@ fun WaitingContent(
|
|||||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||||
) {
|
) {
|
||||||
val error = state.loadError?.let {
|
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) {
|
LaunchedEffect(error) {
|
||||||
if (error != null) {
|
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()
|
actions.onErrorShown()
|
||||||
if (result == SnackbarResult.ActionPerformed) actions.onRetry()
|
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.foundation.verticalScroll
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
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.FactRow
|
||||||
import se.hagstedt.kvitt.design.component.KvittButton
|
import se.hagstedt.kvitt.design.component.KvittButton
|
||||||
import se.hagstedt.kvitt.design.component.KvittIconImage
|
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.KvittTopAppBar
|
||||||
import se.hagstedt.kvitt.design.component.NeedsConnectionHint
|
import se.hagstedt.kvitt.design.component.NeedsConnectionHint
|
||||||
import se.hagstedt.kvitt.design.component.SegmentedGroup
|
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.icon.KvittIcon
|
||||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||||
import se.hagstedt.kvitt.feature.settings.R
|
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.model.UpdateState
|
||||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
import se.hagstedt.kvitt.core.design.R as DesignR
|
||||||
|
|
||||||
@@ -76,13 +79,14 @@ class AboutActions(
|
|||||||
/** Stateless S-22. */
|
/** Stateless S-22. */
|
||||||
@Composable
|
@Composable
|
||||||
fun AboutContent(state: AboutScreenState?, actions: AboutActions, updateCheckAvailable: Boolean, modifier: Modifier = Modifier) {
|
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(),
|
modifier.fillMaxSize(),
|
||||||
topBar = { KvittTopAppBar(stringResource(R.string.settings_about_title), navigation = TopBarNavigation.Back, onNavigation = actions.onBack) },
|
topBar = { KvittTopAppBar(stringResource(R.string.settings_about_title), navigation = TopBarNavigation.Back, onNavigation = actions.onBack) },
|
||||||
message = state?.message,
|
snackbarHostState = snackbar,
|
||||||
onMessageShown = actions.onMessageShown,
|
|
||||||
) { padding ->
|
) { padding ->
|
||||||
if (state == null) return@SettingsScaffold
|
if (state == null) return@KvittScaffold
|
||||||
val about = state.about
|
val about = state.about
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
|
|||||||
+9
-5
@@ -11,10 +11,12 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
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.KvittBadge
|
||||||
import se.hagstedt.kvitt.design.component.KvittButton
|
import se.hagstedt.kvitt.design.component.KvittButton
|
||||||
import se.hagstedt.kvitt.design.component.KvittIconImage
|
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.KvittTextField
|
||||||
import se.hagstedt.kvitt.design.component.KvittTopAppBar
|
import se.hagstedt.kvitt.design.component.KvittTopAppBar
|
||||||
import se.hagstedt.kvitt.design.component.NeedsConnectionHint
|
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.design.theme.KvittDimens
|
||||||
import se.hagstedt.kvitt.feature.settings.R
|
import se.hagstedt.kvitt.feature.settings.R
|
||||||
import se.hagstedt.kvitt.feature.settings.common.HintText
|
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.SaveAction
|
||||||
|
import se.hagstedt.kvitt.feature.settings.common.SettingsMessageEffect
|
||||||
import se.hagstedt.kvitt.model.ColorToken
|
import se.hagstedt.kvitt.model.ColorToken
|
||||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
import se.hagstedt.kvitt.core.design.R as DesignR
|
||||||
|
|
||||||
@@ -100,7 +103,9 @@ class CategoryEditorActions(
|
|||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryEditorContent(state: CategoryEditorState?, actions: CategoryEditorActions, modifier: Modifier = Modifier) {
|
fun CategoryEditorContent(state: CategoryEditorState?, actions: CategoryEditorActions, modifier: Modifier = Modifier) {
|
||||||
SettingsScaffold(
|
val snackbar = remember { SnackbarHostState() }
|
||||||
|
SettingsMessageEffect(state?.message, snackbar, actions.onMessageShown)
|
||||||
|
KvittScaffold(
|
||||||
modifier.fillMaxSize(),
|
modifier.fillMaxSize(),
|
||||||
topBar = {
|
topBar = {
|
||||||
val title = if (state?.isNew != false) R.string.settings_category_editor_new else R.string.settings_category_editor_edit
|
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)
|
SaveAction(enabled = state?.canSave == true, onClick = actions.onSave)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
message = state?.message,
|
snackbarHostState = snackbar,
|
||||||
onMessageShown = actions.onMessageShown,
|
|
||||||
) { padding ->
|
) { padding ->
|
||||||
if (state == null) return@SettingsScaffold
|
if (state == null) return@KvittScaffold
|
||||||
val defaultName = state.defaultKey?.let { stringResource(defaultCategoryName(it)) }
|
val defaultName = state.defaultKey?.let { stringResource(defaultCategoryName(it)) }
|
||||||
val shownName = state.name.trim().ifEmpty { defaultName ?: "" }
|
val shownName = state.name.trim().ifEmpty { defaultName ?: "" }
|
||||||
Column(
|
Column(
|
||||||
|
|||||||
+8
-5
@@ -19,6 +19,7 @@ import androidx.compose.foundation.rememberScrollState
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
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.CategoryTile
|
||||||
import se.hagstedt.kvitt.design.component.CollapsibleRow
|
import se.hagstedt.kvitt.design.component.CollapsibleRow
|
||||||
import se.hagstedt.kvitt.design.component.KvittIconImage
|
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.KvittTopAppBar
|
||||||
import se.hagstedt.kvitt.design.component.NeedsConnectionHint
|
import se.hagstedt.kvitt.design.component.NeedsConnectionHint
|
||||||
import se.hagstedt.kvitt.design.component.SegmentedGroup
|
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.icon.KvittIcon
|
||||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||||
import se.hagstedt.kvitt.feature.settings.R
|
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.Category
|
||||||
import se.hagstedt.kvitt.model.CategoryId
|
import se.hagstedt.kvitt.model.CategoryId
|
||||||
|
|
||||||
@@ -118,7 +120,9 @@ class CategoryListActions(
|
|||||||
/** Stateless S-15 list. */
|
/** Stateless S-15 list. */
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoryListContent(state: CategoryListState?, actions: CategoryListActions, modifier: Modifier = Modifier) {
|
fun CategoryListContent(state: CategoryListState?, actions: CategoryListActions, modifier: Modifier = Modifier) {
|
||||||
SettingsScaffold(
|
val snackbar = remember { SnackbarHostState() }
|
||||||
|
SettingsMessageEffect(state?.message, snackbar, actions.onMessageShown)
|
||||||
|
KvittScaffold(
|
||||||
modifier.fillMaxSize(),
|
modifier.fillMaxSize(),
|
||||||
topBar = {
|
topBar = {
|
||||||
KvittTopAppBar(stringResource(R.string.settings_categories_title), navigation = TopBarNavigation.Back, onNavigation = actions.onBack) {
|
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,
|
snackbarHostState = snackbar,
|
||||||
onMessageShown = actions.onMessageShown,
|
|
||||||
) { padding ->
|
) { padding ->
|
||||||
if (state == null) return@SettingsScaffold
|
if (state == null) return@KvittScaffold
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|||||||
+18
-51
@@ -1,24 +1,14 @@
|
|||||||
package se.hagstedt.kvitt.feature.settings.common
|
package se.hagstedt.kvitt.feature.settings.common
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.semantics.LiveRegionMode
|
import se.hagstedt.kvitt.design.component.showKvittSnackbar
|
||||||
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 se.hagstedt.kvitt.model.OnlineResult
|
import se.hagstedt.kvitt.model.OnlineResult
|
||||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
import se.hagstedt.kvitt.core.design.R as DesignR
|
||||||
import se.hagstedt.kvitt.feature.settings.R
|
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)
|
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:
|
* Shows [message] once in [host] (design-spec §3.11: 4 s, 10 s with "Försök igen") through [showKvittSnackbar], so its
|
||||||
* `inverse-surface`, 4 s, 10 s with "Försök igen"), drawn at the bottom above the bars. The text is resolved while
|
* text follows the app language while it shows. [onMessageShown] when it times out or its action is used; the action
|
||||||
* shown, so it follows the app language. [onMessageShown] when it times out or its action is used.
|
* then runs [onRetry].
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScaffold(
|
fun SettingsMessageEffect(message: UiMessage?, host: SnackbarHostState, onMessageShown: () -> Unit, onRetry: () -> Unit = {}) {
|
||||||
modifier: Modifier = Modifier,
|
val resources = LocalResources.current
|
||||||
topBar: @Composable () -> Unit = {},
|
val shown by rememberUpdatedState(onMessageShown)
|
||||||
bottomBar: @Composable () -> Unit = {},
|
val retry by rememberUpdatedState(onRetry)
|
||||||
floatingActionButton: @Composable () -> Unit = {},
|
LaunchedEffect(message?.id) {
|
||||||
message: UiMessage? = null,
|
if (message == null) return@LaunchedEffect
|
||||||
onMessageShown: () -> Unit = {},
|
val result = host.showKvittSnackbar(resources, message.text, message.args, if (message.retry) DesignR.string.common_retry else null)
|
||||||
onRetry: () -> Unit = {},
|
shown()
|
||||||
content: @Composable (PaddingValues) -> Unit,
|
if (result == SnackbarResult.ActionPerformed) retry()
|
||||||
) {
|
|
||||||
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()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-5
@@ -15,9 +15,11 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
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.KvittBadge
|
||||||
import se.hagstedt.kvitt.design.component.KvittButton
|
import se.hagstedt.kvitt.design.component.KvittButton
|
||||||
import se.hagstedt.kvitt.design.component.KvittIconImage
|
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.KvittTopAppBar
|
||||||
import se.hagstedt.kvitt.design.component.PersonAvatar
|
import se.hagstedt.kvitt.design.component.PersonAvatar
|
||||||
import se.hagstedt.kvitt.design.component.ProvideHousehold
|
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.R
|
||||||
import se.hagstedt.kvitt.feature.settings.common.CentredStatus
|
import se.hagstedt.kvitt.feature.settings.common.CentredStatus
|
||||||
import se.hagstedt.kvitt.feature.settings.common.HintText
|
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.ColorToken
|
||||||
import se.hagstedt.kvitt.model.Device
|
import se.hagstedt.kvitt.model.Device
|
||||||
import se.hagstedt.kvitt.model.User
|
import se.hagstedt.kvitt.model.User
|
||||||
@@ -73,13 +76,14 @@ class DevicesActions(
|
|||||||
/** Stateless S-20. */
|
/** Stateless S-20. */
|
||||||
@Composable
|
@Composable
|
||||||
fun DevicesContent(state: DevicesState?, actions: DevicesActions, modifier: Modifier = Modifier) {
|
fun DevicesContent(state: DevicesState?, actions: DevicesActions, modifier: Modifier = Modifier) {
|
||||||
SettingsScaffold(
|
val snackbar = remember { SnackbarHostState() }
|
||||||
|
SettingsMessageEffect(state?.message, snackbar, actions.onMessageShown)
|
||||||
|
KvittScaffold(
|
||||||
modifier.fillMaxSize(),
|
modifier.fillMaxSize(),
|
||||||
topBar = { KvittTopAppBar(stringResource(R.string.settings_devices_title), navigation = TopBarNavigation.Back, onNavigation = actions.onBack) },
|
topBar = { KvittTopAppBar(stringResource(R.string.settings_devices_title), navigation = TopBarNavigation.Back, onNavigation = actions.onBack) },
|
||||||
message = state?.message,
|
snackbarHostState = snackbar,
|
||||||
onMessageShown = actions.onMessageShown,
|
|
||||||
) { padding ->
|
) { padding ->
|
||||||
if (state == null) return@SettingsScaffold
|
if (state == null) return@KvittScaffold
|
||||||
ProvideHousehold(state.me, state.partner) {
|
ProvideHousehold(state.me, state.partner) {
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
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.KvittDestination
|
||||||
import se.hagstedt.kvitt.design.component.KvittDivider
|
import se.hagstedt.kvitt.design.component.KvittDivider
|
||||||
import se.hagstedt.kvitt.design.component.KvittNavigationBar
|
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.KvittTopAppBar
|
||||||
import se.hagstedt.kvitt.design.component.PersonAvatar
|
import se.hagstedt.kvitt.design.component.PersonAvatar
|
||||||
import se.hagstedt.kvitt.design.component.ProvideHousehold
|
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.icon.KvittIcon
|
||||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||||
import se.hagstedt.kvitt.feature.settings.R
|
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.ColorToken
|
||||||
import se.hagstedt.kvitt.model.Role
|
import se.hagstedt.kvitt.model.Role
|
||||||
import se.hagstedt.kvitt.model.User
|
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). */
|
/** Stateless S-13; [state] null while the session loads (nothing but the bars). */
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsHubContent(state: SettingsHubState?, actions: SettingsHubActions, modifier: Modifier = Modifier) {
|
fun SettingsHubContent(state: SettingsHubState?, actions: SettingsHubActions, modifier: Modifier = Modifier) {
|
||||||
SettingsScaffold(
|
KvittScaffold(
|
||||||
modifier.fillMaxSize(),
|
modifier.fillMaxSize(),
|
||||||
topBar = { KvittTopAppBar(stringResource(R.string.settings_title)) },
|
topBar = { KvittTopAppBar(stringResource(R.string.settings_title)) },
|
||||||
bottomBar = { KvittNavigationBar(KvittDestination.Settings, actions.onNavigate) },
|
bottomBar = { KvittNavigationBar(KvittDestination.Settings, actions.onNavigate) },
|
||||||
) { padding ->
|
) { padding ->
|
||||||
if (state == null) return@SettingsScaffold
|
if (state == null) return@KvittScaffold
|
||||||
ProvideHousehold(state.me, state.partner) {
|
ProvideHousehold(state.me, state.partner) {
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
|
|||||||
+9
-6
@@ -18,9 +18,11 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.KvittButton
|
||||||
import se.hagstedt.kvitt.design.component.KvittCard
|
import se.hagstedt.kvitt.design.component.KvittCard
|
||||||
import se.hagstedt.kvitt.design.component.KvittIconImage
|
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.KvittTopAppBar
|
||||||
import se.hagstedt.kvitt.design.component.LocalKvittClock
|
import se.hagstedt.kvitt.design.component.LocalKvittClock
|
||||||
import se.hagstedt.kvitt.design.component.ProvideHousehold
|
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.design.theme.KvittDimens
|
||||||
import se.hagstedt.kvitt.feature.settings.R
|
import se.hagstedt.kvitt.feature.settings.R
|
||||||
import se.hagstedt.kvitt.feature.settings.common.HintText
|
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.OfflineBanner
|
||||||
|
import se.hagstedt.kvitt.feature.settings.common.SettingsMessageEffect
|
||||||
import se.hagstedt.kvitt.model.ColorToken
|
import se.hagstedt.kvitt.model.ColorToken
|
||||||
import se.hagstedt.kvitt.model.Invite
|
import se.hagstedt.kvitt.model.Invite
|
||||||
import se.hagstedt.kvitt.model.InviteKind
|
import se.hagstedt.kvitt.model.InviteKind
|
||||||
@@ -121,17 +124,17 @@ class InvitesActions(
|
|||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun InvitesContent(state: InvitesState?, actions: InvitesActions, modifier: Modifier = Modifier) {
|
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(),
|
modifier.fillMaxSize(),
|
||||||
topBar = {
|
topBar = {
|
||||||
val title = if (state != null && state.partner == null) stringResource(DesignR.string.common_invite_partner) else stringResource(R.string.settings_invites_title_device)
|
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)
|
KvittTopAppBar(title, navigation = TopBarNavigation.Back, onNavigation = actions.onBack)
|
||||||
},
|
},
|
||||||
message = state?.message,
|
snackbarHostState = snackbar,
|
||||||
onMessageShown = actions.onMessageShown,
|
|
||||||
onRetry = actions.onRetry,
|
|
||||||
) { padding ->
|
) { padding ->
|
||||||
if (state == null) return@SettingsScaffold
|
if (state == null) return@KvittScaffold
|
||||||
ProvideHousehold(state.me, state.partner) {
|
ProvideHousehold(state.me, state.partner) {
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
|
|||||||
+9
-6
@@ -13,9 +13,11 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.AvatarState
|
||||||
import se.hagstedt.kvitt.design.component.ButtonKind
|
import se.hagstedt.kvitt.design.component.ButtonKind
|
||||||
import se.hagstedt.kvitt.design.component.KvittButton
|
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.KvittTextField
|
||||||
import se.hagstedt.kvitt.design.component.KvittTopAppBar
|
import se.hagstedt.kvitt.design.component.KvittTopAppBar
|
||||||
import se.hagstedt.kvitt.design.component.PersonAvatar
|
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.icon.KvittIcon
|
||||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||||
import se.hagstedt.kvitt.feature.settings.R
|
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.OfflineBanner
|
||||||
import se.hagstedt.kvitt.feature.settings.common.SaveAction
|
import se.hagstedt.kvitt.feature.settings.common.SaveAction
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
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. */
|
/** S-14 Profile. [onBack] after a saved name and on the back arrow. */
|
||||||
@Composable
|
@Composable
|
||||||
@@ -105,18 +108,18 @@ class ProfileActions(
|
|||||||
/** Stateless S-14. */
|
/** Stateless S-14. */
|
||||||
@Composable
|
@Composable
|
||||||
fun ProfileContent(state: ProfileState?, actions: ProfileActions, modifier: Modifier = Modifier) {
|
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(),
|
modifier.fillMaxSize(),
|
||||||
topBar = {
|
topBar = {
|
||||||
KvittTopAppBar(stringResource(R.string.settings_profile), navigation = TopBarNavigation.Back, onNavigation = actions.onBack) {
|
KvittTopAppBar(stringResource(R.string.settings_profile), navigation = TopBarNavigation.Back, onNavigation = actions.onBack) {
|
||||||
SaveAction(enabled = state?.canSave == true, onClick = actions.onSave)
|
SaveAction(enabled = state?.canSave == true, onClick = actions.onSave)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
message = state?.message,
|
snackbarHostState = snackbar,
|
||||||
onMessageShown = actions.onMessageShown,
|
|
||||||
onRetry = actions.onRetry,
|
|
||||||
) { padding ->
|
) { padding ->
|
||||||
if (state == null) return@SettingsScaffold
|
if (state == null) return@KvittScaffold
|
||||||
ProvideHousehold(state.me, state.partner) {
|
ProvideHousehold(state.me, state.partner) {
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
|
|||||||
+8
-6
@@ -19,6 +19,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
|||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
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.FullSplitPicker
|
||||||
import se.hagstedt.kvitt.design.component.KvittButton
|
import se.hagstedt.kvitt.design.component.KvittButton
|
||||||
import se.hagstedt.kvitt.design.component.KvittIconImage
|
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.KvittTextField
|
||||||
import se.hagstedt.kvitt.design.component.KvittTopAppBar
|
import se.hagstedt.kvitt.design.component.KvittTopAppBar
|
||||||
import se.hagstedt.kvitt.design.component.NotesField
|
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.design.icon.KvittIcon
|
||||||
import se.hagstedt.kvitt.feature.settings.R
|
import se.hagstedt.kvitt.feature.settings.R
|
||||||
import se.hagstedt.kvitt.feature.settings.common.HintText
|
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.OfflineBanner
|
||||||
import se.hagstedt.kvitt.feature.settings.common.SaveAction
|
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.AccountId
|
||||||
import se.hagstedt.kvitt.model.CategoryId
|
import se.hagstedt.kvitt.model.CategoryId
|
||||||
import se.hagstedt.kvitt.model.SplitMode
|
import se.hagstedt.kvitt.model.SplitMode
|
||||||
@@ -165,7 +167,9 @@ class RecurringEditorActions(
|
|||||||
@Composable
|
@Composable
|
||||||
fun RecurringEditorContent(state: RecurringEditorState?, actions: RecurringEditorActions, modifier: Modifier = Modifier) {
|
fun RecurringEditorContent(state: RecurringEditorState?, actions: RecurringEditorActions, modifier: Modifier = Modifier) {
|
||||||
val keypad = state != null && state.form.focus != EditorFocus.None
|
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(),
|
modifier.fillMaxSize(),
|
||||||
topBar = {
|
topBar = {
|
||||||
val title = if (state?.isNew != false) R.string.settings_recurring_editor_new else R.string.settings_recurring_editor_edit
|
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 = {
|
bottomBar = {
|
||||||
if (keypad) AmountKeypad(actions.onKey, Modifier.navigationBarsPadding(), compact = state.form.splitOpen)
|
if (keypad) AmountKeypad(actions.onKey, Modifier.navigationBarsPadding(), compact = state.form.splitOpen)
|
||||||
},
|
},
|
||||||
message = state?.message,
|
snackbarHostState = snackbar,
|
||||||
onMessageShown = actions.onMessageShown,
|
|
||||||
onRetry = actions.onRetry,
|
|
||||||
) { padding ->
|
) { padding ->
|
||||||
if (state == null) return@SettingsScaffold
|
if (state == null) return@KvittScaffold
|
||||||
ProvideHousehold(state.options.me, state.options.partner) {
|
ProvideHousehold(state.options.me, state.options.partner) {
|
||||||
EditorBody(state, actions, Modifier.padding(padding))
|
EditorBody(state, actions, Modifier.padding(padding))
|
||||||
when (state.sheet) {
|
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.CategoryTile
|
||||||
import se.hagstedt.kvitt.design.component.EmptyState
|
import se.hagstedt.kvitt.design.component.EmptyState
|
||||||
import se.hagstedt.kvitt.design.component.KvittBadge
|
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.KvittTopAppBar
|
||||||
import se.hagstedt.kvitt.design.component.LocalKvittClock
|
import se.hagstedt.kvitt.design.component.LocalKvittClock
|
||||||
import se.hagstedt.kvitt.design.component.LocalKvittHousehold
|
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.icon.KvittIcon
|
||||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||||
import se.hagstedt.kvitt.feature.settings.R
|
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.OfflineBanner
|
||||||
import se.hagstedt.kvitt.model.TemplateId
|
import se.hagstedt.kvitt.model.TemplateId
|
||||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
import se.hagstedt.kvitt.core.design.R as DesignR
|
||||||
@@ -77,14 +77,14 @@ class RecurringListActions(val onBack: () -> Unit = {}, val onOpen: (TemplateId)
|
|||||||
@Composable
|
@Composable
|
||||||
fun RecurringListContent(state: RecurringListState?, actions: RecurringListActions, modifier: Modifier = Modifier) {
|
fun RecurringListContent(state: RecurringListState?, actions: RecurringListActions, modifier: Modifier = Modifier) {
|
||||||
val empty = state != null && state.rows.isEmpty()
|
val empty = state != null && state.rows.isEmpty()
|
||||||
SettingsScaffold(
|
KvittScaffold(
|
||||||
modifier.fillMaxSize(),
|
modifier.fillMaxSize(),
|
||||||
topBar = { KvittTopAppBar(stringResource(R.string.settings_recurring_title), navigation = TopBarNavigation.Back, onNavigation = actions.onBack) },
|
topBar = { KvittTopAppBar(stringResource(R.string.settings_recurring_title), navigation = TopBarNavigation.Back, onNavigation = actions.onBack) },
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
if (state != null && !empty) AddExpenseFab(actions.onNew, text = stringResource(R.string.settings_recurring_new), enabled = state.online)
|
if (state != null && !empty) AddExpenseFab(actions.onNew, text = stringResource(R.string.settings_recurring_new), enabled = state.online)
|
||||||
},
|
},
|
||||||
) { padding ->
|
) { padding ->
|
||||||
if (state == null) return@SettingsScaffold
|
if (state == null) return@KvittScaffold
|
||||||
ProvideHousehold(state.me, state.partner) {
|
ProvideHousehold(state.me, state.partner) {
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
|
|||||||
+2
-1
@@ -123,7 +123,8 @@ fun SettlementDetailContent(
|
|||||||
action = stringResource(R.string.settle_detail_sync_now),
|
action = stringResource(R.string.settle_detail_sync_now),
|
||||||
actionIcon = KvittIcon.Sync,
|
actionIcon = KvittIcon.Sync,
|
||||||
actionKind = ButtonKind.Tonal,
|
actionKind = ButtonKind.Tonal,
|
||||||
onAction = actions::onSyncNow,
|
actionEnabled = state.online,
|
||||||
|
onAction = { if (state.online) actions.onSyncNow() },
|
||||||
)
|
)
|
||||||
else -> DetailBody(state, detail)
|
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.AccountId
|
||||||
import se.hagstedt.kvitt.model.AccountRepository
|
import se.hagstedt.kvitt.model.AccountRepository
|
||||||
import se.hagstedt.kvitt.model.AuditAction
|
import se.hagstedt.kvitt.model.AuditAction
|
||||||
|
import se.hagstedt.kvitt.model.ConnectivityRepository
|
||||||
import se.hagstedt.kvitt.model.HomeRepository
|
import se.hagstedt.kvitt.model.HomeRepository
|
||||||
import se.hagstedt.kvitt.model.SettlementDetailState
|
import se.hagstedt.kvitt.model.SettlementDetailState
|
||||||
import se.hagstedt.kvitt.model.SettlementId
|
import se.hagstedt.kvitt.model.SettlementId
|
||||||
@@ -25,7 +26,8 @@ import se.hagstedt.kvitt.model.TimelineLine
|
|||||||
import se.hagstedt.kvitt.model.User
|
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(
|
data class SettlementDetailUiState(
|
||||||
val loading: Boolean = true,
|
val loading: Boolean = true,
|
||||||
@@ -35,6 +37,7 @@ data class SettlementDetailUiState(
|
|||||||
val accounts: List<Account> = emptyList(),
|
val accounts: List<Account> = emptyList(),
|
||||||
val confirmDelete: Boolean = false,
|
val confirmDelete: Boolean = false,
|
||||||
val deletedByMe: Boolean = false,
|
val deletedByMe: Boolean = false,
|
||||||
|
val online: Boolean = true,
|
||||||
) {
|
) {
|
||||||
/** Design-spec §2.4: an account's colour is its position in `AccountRepository.all()`. */
|
/** 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)
|
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,
|
private val settlements: SettlementRepository,
|
||||||
accounts: AccountRepository,
|
accounts: AccountRepository,
|
||||||
home: HomeRepository,
|
home: HomeRepository,
|
||||||
|
connectivity: ConnectivityRepository,
|
||||||
private val sync: SyncRepository,
|
private val sync: SyncRepository,
|
||||||
) : ViewModel(), SettlementDetailActions {
|
) : ViewModel(), SettlementDetailActions {
|
||||||
val id: SettlementId? = savedStateHandle.get<String>(SettleArgs.SETTLEMENT_ID)?.let(::SettlementId)
|
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),
|
id?.let { settlements.detail(it) } ?: flowOf(null),
|
||||||
accounts.all(),
|
accounts.all(),
|
||||||
home.home(),
|
home.home(),
|
||||||
|
connectivity.online,
|
||||||
local,
|
local,
|
||||||
) { detail, all, h, l ->
|
) { detail, all, h, online, l ->
|
||||||
l.copy(loading = false, detail = detail, accounts = all, me = h.me, partner = h.partner)
|
l.copy(loading = false, detail = detail, accounts = all, me = h.me, partner = h.partner, online = online)
|
||||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(STOP_TIMEOUT_MS), SettlementDetailUiState())
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(STOP_TIMEOUT_MS), SettlementDetailUiState())
|
||||||
|
|
||||||
override fun onDelete() = local.update { it.copy(confirmDelete = true) }
|
override fun onDelete() = local.update { it.copy(confirmDelete = true) }
|
||||||
|
|||||||
+9
-1
@@ -30,7 +30,7 @@ class SettlementDetailViewModelTest {
|
|||||||
fun reset() = Dispatchers.resetMain()
|
fun reset() = Dispatchers.resetMain()
|
||||||
|
|
||||||
private fun vm(id: String, repos: FakeRepositories = FakeRepositories(SettleScenarios.detail)) =
|
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
|
@Test
|
||||||
fun loadedShowsTheSettlementAndItsActions() = runTest {
|
fun loadedShowsTheSettlementAndItsActions() = runTest {
|
||||||
@@ -48,6 +48,14 @@ class SettlementDetailViewModelTest {
|
|||||||
assertFalse(s.actions)
|
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
|
@Test
|
||||||
fun deletedHasTheDeletionLineAndNoActions() = runTest {
|
fun deletedHasTheDeletionLineAndNoActions() = runTest {
|
||||||
val s = vm(SettleScenarios.deleted.value).state.first { !it.loading }
|
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_offline" to Detail(SettleScenarios.detailOffline, SettleScenarios.split.value),
|
||||||
"detail_delete_dialog" to Detail(SettleScenarios.detail, SettleScenarios.swish.value) { onDelete() },
|
"detail_delete_dialog" to Detail(SettleScenarios.detail, SettleScenarios.swish.value) { onDelete() },
|
||||||
"detail_missing" to Detail(SettleScenarios.detail, SettleScenarios.missing.value),
|
"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),
|
"detail_long" to Detail(SettleScenarios.longDetail, SettleScenarios.long.value, height = longHeight),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -105,7 +106,7 @@ class SettleScreenshotTest(private val name: String) {
|
|||||||
is Detail -> {
|
is Detail -> {
|
||||||
val repos = FakeRepositories(case.scenario)
|
val repos = FakeRepositories(case.scenario)
|
||||||
val vm = SettlementDetailViewModel(
|
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)
|
vm.apply(case.steps)
|
||||||
ScreenSnapshots.snap(compose, name, case.height, case.largeFont) {
|
ScreenSnapshots.snap(compose, name, case.height, case.largeFont) {
|
||||||
|
|||||||
Reference in New Issue
Block a user