Android overhaul: shared failure texts, saved-split label and limits in :core:design
* format/Failures.kt: FAILURE_NETWORK and commonFailureText, the §14.3.2 code-to-text map that :feature:account
(Errors.kt) and :feature:settings (Messages.kt) each spelled out; both keep only their own codes. The "network"
literals in :feature:onboarding and the two NETWORK constants use FAILURE_NETWORK.
* component/Split.kt: savedSplitLabel, the S-8 and S-16 line ("Kontots standard 50/50", "Egen fördelning 70/30",
"Exakta belopp", "Allt på dig/Anna") that ExpenseDetailScreen.modeLabel and SyncStatusScreen.lostModeLabel
duplicated line for line.
* format/Limits.kt: BP_TOTAL (five copies in four features and a literal in :core:design and S-19) and
DISPLAY_NAME_MAX (three copies in :feature:onboarding and :feature:settings).
Behaviour and goldens unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
+9
-14
@@ -9,9 +9,11 @@ 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.design.format.FAILURE_NETWORK
|
||||
import se.hagstedt.kvitt.design.format.commonFailureText
|
||||
import se.hagstedt.kvitt.feature.settings.R
|
||||
import se.hagstedt.kvitt.model.OnlineResult
|
||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
||||
import se.hagstedt.kvitt.feature.settings.R
|
||||
|
||||
/**
|
||||
* A snackbar the screen shows once (design-spec §1 conventions): [text] from resources with [args]; [retry] adds
|
||||
@@ -36,35 +38,28 @@ fun OnlineResult<*>.failureCode(): String? = when (this) {
|
||||
}
|
||||
|
||||
const val OFFLINE = "offline"
|
||||
const val NETWORK = "network"
|
||||
|
||||
/**
|
||||
* The words for a refused online call (design-spec §1, `strings.csv` `error_*`): network → "Kunde inte nå servern",
|
||||
* offline → "Kräver anslutning", known §14.3.2 codes → their text, anything else → the generic validation text.
|
||||
* The words for a refused online call (design-spec §1, `strings.csv` `error_*`): offline → "Kräver anslutning", this
|
||||
* module's codes → their text, the shared ones through [commonFailureText] (network → "Kunde inte nå servern"),
|
||||
* anything else → the generic validation text.
|
||||
*/
|
||||
@StringRes
|
||||
fun failureText(code: String): Int = when (code) {
|
||||
NETWORK -> DesignR.string.common_could_not_reach_server
|
||||
OFFLINE -> DesignR.string.common_needs_connection
|
||||
"household_full" -> DesignR.string.error_household_full
|
||||
"household_incomplete" -> DesignR.string.error_household_incomplete
|
||||
"forbidden" -> DesignR.string.error_forbidden
|
||||
"not_found" -> DesignR.string.error_not_found
|
||||
"rate_limited" -> DesignR.string.error_rate_limited
|
||||
"internal", "unavailable", "not_implemented" -> DesignR.string.error_server
|
||||
"protocol_unsupported" -> DesignR.string.error_app_too_old
|
||||
"validation_failed", "invalid_request" -> DesignR.string.error_validation
|
||||
"invalid_request" -> DesignR.string.error_validation
|
||||
"image_invalid" -> DesignR.string.profile_picture_invalid
|
||||
"payload_too_large" -> DesignR.string.profile_picture_too_large
|
||||
"unsupported_media_type" -> DesignR.string.profile_picture_unsupported
|
||||
"category_in_use" -> R.string.settings_category_delete_blocked_in_use
|
||||
"category_is_default" -> R.string.settings_category_delete_blocked_default
|
||||
"cannot_revoke_current_device" -> R.string.settings_devices_cannot_revoke_current
|
||||
else -> DesignR.string.error_generic_invalid
|
||||
else -> commonFailureText(code) ?: DesignR.string.error_generic_invalid
|
||||
}
|
||||
|
||||
/** The snackbar for a failed call: network failures offer "Försök igen" when [canRetry]. */
|
||||
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 == FAILURE_NETWORK)
|
||||
|
||||
/**
|
||||
* Shows [message] once in [host] (design-spec §3.11: 4 s, 10 s with "Försök igen") through [showKvittSnackbar], so its
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import se.hagstedt.kvitt.feature.settings.common.NETWORK
|
||||
import se.hagstedt.kvitt.design.format.FAILURE_NETWORK
|
||||
import se.hagstedt.kvitt.feature.settings.common.UiMessage
|
||||
import se.hagstedt.kvitt.feature.settings.common.failureCode
|
||||
import se.hagstedt.kvitt.feature.settings.common.failureMessage
|
||||
@@ -76,7 +76,7 @@ class DevicesViewModel @Inject constructor(
|
||||
val list = when (result) {
|
||||
is OnlineResult.Ok -> DeviceList.Loaded(result.value.filter { d -> !d.revoked })
|
||||
OnlineResult.Offline -> DeviceList.Unavailable(unreachable = false)
|
||||
is OnlineResult.Failed -> DeviceList.Unavailable(unreachable = result.code == NETWORK)
|
||||
is OnlineResult.Failed -> DeviceList.Unavailable(unreachable = result.code == FAILURE_NETWORK)
|
||||
}
|
||||
local.update { it.copy(list = list) }
|
||||
}
|
||||
|
||||
+2
-4
@@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import se.hagstedt.kvitt.design.format.DISPLAY_NAME_MAX
|
||||
import se.hagstedt.kvitt.feature.settings.common.UiMessage
|
||||
import se.hagstedt.kvitt.feature.settings.common.failureCode
|
||||
import se.hagstedt.kvitt.feature.settings.common.failureMessage
|
||||
@@ -29,16 +30,13 @@ sealed interface NameError {
|
||||
data object Refused : NameError // validation_failed on display_name from the server
|
||||
}
|
||||
|
||||
/** §9.6 limit of a display name. */
|
||||
const val NAME_MAX = 40
|
||||
|
||||
/** The local check of a display name, or null when it may be sent. */
|
||||
fun nameError(name: String): NameError? {
|
||||
val trimmed = name.trim()
|
||||
val length = trimmed.codePointCount(0, trimmed.length)
|
||||
return when {
|
||||
length == 0 -> NameError.Required
|
||||
length > NAME_MAX -> NameError.TooLong(NAME_MAX)
|
||||
length > DISPLAY_NAME_MAX -> NameError.TooLong(DISPLAY_NAME_MAX)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -76,6 +76,7 @@ import se.hagstedt.kvitt.design.component.splitModeLabel
|
||||
import se.hagstedt.kvitt.design.component.splitOption
|
||||
import se.hagstedt.kvitt.design.format.AmountInput
|
||||
import se.hagstedt.kvitt.design.format.AmountKey
|
||||
import se.hagstedt.kvitt.design.format.BP_TOTAL
|
||||
import se.hagstedt.kvitt.design.format.DateFormatter
|
||||
import se.hagstedt.kvitt.design.format.KvittLanguage
|
||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||
@@ -236,7 +237,7 @@ private fun EditorBody(state: RecurringEditorState, actions: RecurringEditorActi
|
||||
accountOption(state.account?.name.orEmpty(), state.accountColorIndex) { actions.onOpenSheet(EditorSheet.Account) },
|
||||
payerOption(payer, if (payer == me) partner else me, actions.onTogglePayer),
|
||||
splitOption(
|
||||
if (form.mode == SplitMode.RATIO) stringResource(DesignR.string.editor_split_mode_ratio) + " " + stringResource(DesignR.string.common_ratio, (form.viewerBp / 100).toString(), ((10_000 - form.viewerBp) / 100).toString()) else splitModeLabel(form.mode, defaultLabel),
|
||||
if (form.mode == SplitMode.RATIO) stringResource(DesignR.string.editor_split_mode_ratio) + " " + stringResource(DesignR.string.common_ratio, (form.viewerBp / 100).toString(), ((BP_TOTAL - form.viewerBp) / 100).toString()) else splitModeLabel(form.mode, defaultLabel),
|
||||
form.splitOpen,
|
||||
actions.onToggleSplit,
|
||||
),
|
||||
@@ -252,7 +253,7 @@ private fun EditorBody(state: RecurringEditorState, actions: RecurringEditorActi
|
||||
SplitMode.RATIO -> {
|
||||
val viewerTyped = form.ratioTyped == RatioField.Viewer
|
||||
val viewerText = if (viewerTyped) form.ratioInput.display(language) else AmountInput.percent(form.viewerBp.toLong()).display(language)
|
||||
val partnerText = if (viewerTyped) AmountInput.percent((10_000 - form.viewerBp).toLong()).display(language) else form.ratioInput.display(language)
|
||||
val partnerText = if (viewerTyped) AmountInput.percent((BP_TOTAL - form.viewerBp).toLong()).display(language) else form.ratioInput.display(language)
|
||||
val focused = when (form.focus) {
|
||||
EditorFocus.RatioViewer -> RatioField.Viewer
|
||||
EditorFocus.RatioPartner -> RatioField.Partner
|
||||
|
||||
+1
-1
@@ -3,6 +3,7 @@ package se.hagstedt.kvitt.feature.settings.recurring
|
||||
import se.hagstedt.kvitt.design.component.RatioField
|
||||
import se.hagstedt.kvitt.design.format.AmountInput
|
||||
import se.hagstedt.kvitt.design.format.AmountKey
|
||||
import se.hagstedt.kvitt.design.format.BP_TOTAL
|
||||
import se.hagstedt.kvitt.model.AccountId
|
||||
import se.hagstedt.kvitt.model.CategoryId
|
||||
import se.hagstedt.kvitt.model.ExpenseDraft
|
||||
@@ -17,7 +18,6 @@ enum class EditorFocus { None, Amount, RatioViewer, RatioPartner, Exact }
|
||||
|
||||
/** §9.6: description 1 to 100, notes 0 to 1000 code points. */
|
||||
const val DESCRIPTION_MAX = 100
|
||||
const val BP_TOTAL = 10_000
|
||||
|
||||
/**
|
||||
* The S-19 editor's input, independent of the repositories: the expense fields of S-7 without the date (D-019) plus
|
||||
|
||||
Reference in New Issue
Block a user