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:
+1
-1
@@ -45,6 +45,7 @@ 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.BP_TOTAL
|
||||
import se.hagstedt.kvitt.design.format.MoneyFormatter
|
||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||
import se.hagstedt.kvitt.model.AccountId
|
||||
@@ -215,7 +216,6 @@ private fun defaultSplit(ui: AccountDetailUi.Ready): String {
|
||||
return stringResource(R.string.account_default_split, ratio, names)
|
||||
}
|
||||
|
||||
internal const val BP_TOTAL = 10_000
|
||||
|
||||
/** `more_vert` with the S-5 menu; disabled items carry their reason as a second line (design-spec §3.11). */
|
||||
@Composable
|
||||
|
||||
+2
-4
@@ -11,6 +11,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.FAILURE_NETWORK
|
||||
import se.hagstedt.kvitt.model.AccountDetailState
|
||||
import se.hagstedt.kvitt.model.AccountId
|
||||
import se.hagstedt.kvitt.model.AccountRepository
|
||||
@@ -170,10 +171,7 @@ class AccountDetailViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun failureMessage(code: String, action: AccountAction): AccountMessage = when (code) {
|
||||
NETWORK -> AccountMessage.CouldNotReach(action)
|
||||
FAILURE_NETWORK -> AccountMessage.CouldNotReach(action)
|
||||
"account_not_empty" -> AccountMessage.DeleteBlocked
|
||||
else -> AccountMessage.Failed(code)
|
||||
}
|
||||
|
||||
/** `OnlineResult.Failed` code of a network failure (design-spec §1, §9.9). */
|
||||
internal const val NETWORK = "network"
|
||||
|
||||
+1
@@ -54,6 +54,7 @@ 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.BP_TOTAL
|
||||
import se.hagstedt.kvitt.design.format.MoneyFormatter
|
||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||
|
||||
+3
-1
@@ -13,6 +13,8 @@ import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import se.hagstedt.kvitt.design.format.BP_TOTAL
|
||||
import se.hagstedt.kvitt.design.format.FAILURE_NETWORK
|
||||
import se.hagstedt.kvitt.model.AccountId
|
||||
import se.hagstedt.kvitt.model.AccountRepository
|
||||
import se.hagstedt.kvitt.model.ConnectivityRepository
|
||||
@@ -183,7 +185,7 @@ private fun AccountEditorUi.failed(code: String, field: String?): AccountEditorU
|
||||
code == "name_taken" -> copy(saving = false, serverNameError = NameError.Taken(name.trim()))
|
||||
code == "validation_failed" && field == "name" -> copy(saving = false, serverNameError = NameError.Invalid)
|
||||
code == "validation_failed" && field == "default_shares" -> copy(saving = false, splitError = true)
|
||||
code == NETWORK -> copy(saving = false, message = EditorMessage.CouldNotReach)
|
||||
code == FAILURE_NETWORK -> copy(saving = false, message = EditorMessage.CouldNotReach)
|
||||
else -> copy(saving = false, message = EditorMessage.Failed(code))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
package se.hagstedt.kvitt.feature.account
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import se.hagstedt.kvitt.design.format.commonFailureText
|
||||
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_*`). */
|
||||
@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
|
||||
else -> commonFailureText(code) ?: DesignR.string.error_generic_invalid
|
||||
}
|
||||
|
||||
+3
-2
@@ -8,6 +8,7 @@ import org.junit.runner.RunWith
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.annotation.GraphicsMode
|
||||
import se.hagstedt.kvitt.design.format.FAILURE_NETWORK
|
||||
import se.hagstedt.kvitt.fakes.FakeScenario
|
||||
import se.hagstedt.kvitt.fakes.account.AccountScenarios
|
||||
import se.hagstedt.kvitt.model.AccountId
|
||||
@@ -28,7 +29,7 @@ val accountStates: List<ScreenState> = listOf(
|
||||
detail("S-5-account-delete-confirm", AccountScenarios.empty, AccountScenarios.sommarstugan.id) { it.askDelete() },
|
||||
detail("S-5-account-empty", AccountScenarios.empty, AccountScenarios.sommarstugan.id),
|
||||
detail("S-5-account-offline", AccountScenarios.offline, AccountScenarios.vardag) { it.setMenuOpen(true) },
|
||||
detail("S-5-account-error", AccountScenarios.loaded, AccountScenarios.vardag, failWith = NETWORK) { it.archive() },
|
||||
detail("S-5-account-error", AccountScenarios.loaded, AccountScenarios.vardag, failWith = FAILURE_NETWORK) { it.archive() },
|
||||
detail("S-5-account-long", AccountScenarios.long, AccountScenarios.longAccount, largeFont = true),
|
||||
editor("S-6-account-editor-create", AccountScenarios.editor),
|
||||
editor("S-6-account-editor", AccountScenarios.editor, largeFont = true) { it.onName("Sommarstugan"); it.onViewerPercent(60) },
|
||||
@@ -36,7 +37,7 @@ val accountStates: List<ScreenState> = listOf(
|
||||
editor("S-6-account-editor-decimals", AccountScenarios.editor) { it.onName("Sommarstugan"); it.onPercent(PercentField.Viewer, "62,75") },
|
||||
editor("S-6-account-editor-error", AccountScenarios.editor) { it.onName("Vardag"); it.save() },
|
||||
editor("S-6-account-editor-validation", AccountScenarios.editor) { it.onName(LONG_NAME) },
|
||||
editor("S-6-account-editor-network", AccountScenarios.editor, failWith = NETWORK) { it.onName("Sommarstugan"); it.save() },
|
||||
editor("S-6-account-editor-network", AccountScenarios.editor, failWith = FAILURE_NETWORK) { it.onName("Sommarstugan"); it.save() },
|
||||
editor("S-6-account-editor-offline", AccountScenarios.editorOffline) { it.onName("Sommarstugan") },
|
||||
saving(),
|
||||
)
|
||||
|
||||
+4
-3
@@ -5,6 +5,7 @@ import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import se.hagstedt.kvitt.design.format.FAILURE_NETWORK
|
||||
import se.hagstedt.kvitt.fakes.FakeRepositories
|
||||
import se.hagstedt.kvitt.fakes.FakeScenarios
|
||||
import se.hagstedt.kvitt.fakes.account.AccountScenarios
|
||||
@@ -65,11 +66,11 @@ class AccountDetailViewModelTest {
|
||||
|
||||
@Test
|
||||
fun networkFailureOffersRetryAndChangesNothing() {
|
||||
val ui = ready(detailState(AccountScenarios.loaded, AccountScenarios.vardag, failWith = NETWORK) { it.archive() })
|
||||
val ui = ready(detailState(AccountScenarios.loaded, AccountScenarios.vardag, failWith = FAILURE_NETWORK) { it.archive() })
|
||||
assertEquals(AccountMessage.CouldNotReach(AccountAction.Archive), ui.message)
|
||||
assertFalse(ui.detail.account.archived)
|
||||
assertFalse(ui.busy)
|
||||
val shown = ready(detailState(AccountScenarios.loaded, AccountScenarios.vardag, failWith = NETWORK) { it.archive(); it.messageShown() })
|
||||
val shown = ready(detailState(AccountScenarios.loaded, AccountScenarios.vardag, failWith = FAILURE_NETWORK) { it.archive(); it.messageShown() })
|
||||
assertNull(shown.message)
|
||||
}
|
||||
|
||||
@@ -190,7 +191,7 @@ class AccountEditorViewModelTest {
|
||||
fun serverValidationMapsToTheField() {
|
||||
assertEquals(NameError.Invalid, editorState(AccountScenarios.editor, failWith = "validation_failed", field = "name") { it.onName("X"); it.save() }.nameError)
|
||||
assertTrue(editorState(AccountScenarios.editor, failWith = "validation_failed", field = "default_shares") { it.onName("X"); it.save() }.splitError)
|
||||
assertEquals(EditorMessage.CouldNotReach, editorState(AccountScenarios.editor, failWith = NETWORK) { it.onName("X"); it.save() }.message)
|
||||
assertEquals(EditorMessage.CouldNotReach, editorState(AccountScenarios.editor, failWith = FAILURE_NETWORK) { it.onName("X"); it.save() }.message)
|
||||
assertEquals(EditorMessage.Failed("household_incomplete"), editorState(AccountScenarios.editor, failWith = "household_incomplete") { it.onName("X"); it.save() }.message)
|
||||
}
|
||||
|
||||
|
||||
+4
-25
@@ -30,8 +30,6 @@ import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import se.hagstedt.kvitt.design.KvittTheme
|
||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
||||
import se.hagstedt.kvitt.design.component.KvittScaffold
|
||||
import se.hagstedt.kvitt.design.component.AmountStyle
|
||||
import se.hagstedt.kvitt.design.component.AmountTone
|
||||
import se.hagstedt.kvitt.design.component.BadgeKind
|
||||
@@ -43,6 +41,7 @@ import se.hagstedt.kvitt.design.component.EmptyState
|
||||
import se.hagstedt.kvitt.design.component.FactRow
|
||||
import se.hagstedt.kvitt.design.component.KvittBadge
|
||||
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.MoneyText
|
||||
@@ -55,23 +54,21 @@ import se.hagstedt.kvitt.design.component.SyncPendingMarker
|
||||
import se.hagstedt.kvitt.design.component.Timeline
|
||||
import se.hagstedt.kvitt.design.component.TopBarNavigation
|
||||
import se.hagstedt.kvitt.design.component.categoryName
|
||||
import se.hagstedt.kvitt.design.component.defaultModeLabel
|
||||
import se.hagstedt.kvitt.design.component.detailDate
|
||||
import se.hagstedt.kvitt.design.component.kvittLanguage
|
||||
import se.hagstedt.kvitt.design.component.personName
|
||||
import se.hagstedt.kvitt.design.component.savedSplitLabel
|
||||
import se.hagstedt.kvitt.design.component.shareText
|
||||
import se.hagstedt.kvitt.design.component.spokenMoney
|
||||
import se.hagstedt.kvitt.design.component.splitModeLabel
|
||||
import se.hagstedt.kvitt.design.format.DateFormatter
|
||||
import se.hagstedt.kvitt.design.format.MoneyFormatter
|
||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||
import se.hagstedt.kvitt.model.AuditAction
|
||||
import se.hagstedt.kvitt.model.ExpenseDetailState
|
||||
import se.hagstedt.kvitt.model.ExpenseId
|
||||
import se.hagstedt.kvitt.model.SplitMode
|
||||
import se.hagstedt.kvitt.model.User
|
||||
import kotlin.time.Instant
|
||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
||||
|
||||
/** What S-8 can ask its state holder to do; [ExpenseDetailViewModel] implements it. */
|
||||
interface ExpenseDetailActions {
|
||||
@@ -181,7 +178,7 @@ private fun Loaded(detail: ExpenseDetailState, state: ExpenseDetailUiState) {
|
||||
valueLeading = { PersonAvatar(detail.payer, KvittDimens.avatarChip) },
|
||||
)
|
||||
2 -> {
|
||||
val mode = modeLabel(detail, me, partner)
|
||||
val mode = savedSplitLabel(detail.mode, detail.shares, me.id, partner.displayName)
|
||||
val shares = shareText(detail.shares, me, partner, spoken = true)
|
||||
FactRow(
|
||||
item,
|
||||
@@ -257,23 +254,6 @@ private fun DeletedBanner(detail: ExpenseDetailState) {
|
||||
Banner(BannerKind.Neutral, KvittIcon.Delete, title, body = stringResource(R.string.expense_detail_deleted_body), iconTint = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
|
||||
/** "Kontots standard 50/50" / "Egen fördelning 70/30" / "Exakta belopp" / "Allt på Anna" (design-spec S-8). */
|
||||
@Composable
|
||||
private fun modeLabel(detail: ExpenseDetailState, me: User, partner: User): String {
|
||||
val language = kvittLanguage()
|
||||
val mineBp = detail.shares.firstOrNull { it.userId == me.id }?.shareBp
|
||||
val ratio = mineBp?.let { stringResource(DesignR.string.common_ratio, MoneyFormatter.basisPoints(it, language), MoneyFormatter.basisPoints(BP_TOTAL - it, language)) }
|
||||
return when (detail.mode) {
|
||||
SplitMode.DEFAULT -> if (ratio != null) stringResource(DesignR.string.editor_split_mode_default, ratio) else defaultModeLabel(HALF_PERCENT)
|
||||
SplitMode.RATIO -> splitModeLabel(SplitMode.RATIO, "") + (ratio?.let { " $it" } ?: "")
|
||||
SplitMode.EXACT -> splitModeLabel(SplitMode.EXACT, "")
|
||||
SplitMode.FULL -> {
|
||||
val bearer = detail.shares.firstOrNull { it.shareOre > 0 }?.userId
|
||||
if (bearer == me.id) stringResource(DesignR.string.editor_split_full_on_you) else stringResource(DesignR.string.editor_split_full_on, partner.displayName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun viewerPercent(detail: ExpenseDetailState, me: User): Float {
|
||||
val total = detail.shares.sumOf { it.shareOre }
|
||||
val mine = detail.shares.firstOrNull { it.userId == me.id }?.shareOre ?: 0L
|
||||
@@ -282,6 +262,5 @@ private fun viewerPercent(detail: ExpenseDetailState, me: User): Float {
|
||||
|
||||
private const val FACTS = 3
|
||||
private const val FACTS_WITH_NOTES = 4
|
||||
private const val BP_TOTAL = 10_000
|
||||
private const val HALF_PERCENT = 50
|
||||
private const val FULL_PERCENT = 100f
|
||||
|
||||
+2
-2
@@ -22,6 +22,7 @@ import se.hagstedt.kvitt.design.component.AccountChoice
|
||||
import se.hagstedt.kvitt.design.component.quickRowCategories
|
||||
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.Account
|
||||
import se.hagstedt.kvitt.model.AccountId
|
||||
import se.hagstedt.kvitt.model.AccountRepository
|
||||
@@ -204,7 +205,7 @@ class ExpenseEditorViewModel @Inject constructor(
|
||||
payer = draft.payer,
|
||||
mode = draft.mode,
|
||||
ratioViewer = viewerBp?.let { AmountInput.percent(it.toLong()) } ?: AmountInput(),
|
||||
ratioPartner = viewerBp?.let { AmountInput.percent(BP_TOTAL - it) } ?: AmountInput(),
|
||||
ratioPartner = viewerBp?.let { AmountInput.percent((BP_TOTAL - it).toLong()) } ?: AmountInput(),
|
||||
exactTyped = me,
|
||||
exactInput = exactMine?.let(AmountInput::of) ?: AmountInput(),
|
||||
fullOn = draft.fullOn ?: if (draft.payer == me) partner else me,
|
||||
@@ -523,7 +524,6 @@ class ExpenseEditorViewModel @Inject constructor(
|
||||
/** design-spec §5.3. */
|
||||
const val SUGGESTION_DEBOUNCE_MS = 150L
|
||||
|
||||
private const val BP_TOTAL = 10_000L
|
||||
private const val HALF_BP = 5000
|
||||
private const val PERCENT = 100
|
||||
private val SPLIT_TARGETS = setOf(KeypadTarget.RatioViewer, KeypadTarget.RatioPartner, KeypadTarget.Exact)
|
||||
|
||||
+15
-38
@@ -17,32 +17,41 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.heading
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import se.hagstedt.kvitt.design.KvittElevation
|
||||
import se.hagstedt.kvitt.design.KvittTheme
|
||||
import se.hagstedt.kvitt.design.component.Banner
|
||||
import se.hagstedt.kvitt.design.component.BannerKind
|
||||
import se.hagstedt.kvitt.design.component.ButtonKind
|
||||
import se.hagstedt.kvitt.design.component.ButtonSize
|
||||
import se.hagstedt.kvitt.design.component.ExpenseRow
|
||||
import se.hagstedt.kvitt.design.component.FactRow
|
||||
import se.hagstedt.kvitt.design.component.KvittBadge
|
||||
import se.hagstedt.kvitt.design.component.KvittButton
|
||||
import se.hagstedt.kvitt.design.component.KvittCard
|
||||
import se.hagstedt.kvitt.design.component.KvittIconImage
|
||||
@@ -53,35 +62,23 @@ import se.hagstedt.kvitt.design.component.NeedsConnectionHint
|
||||
import se.hagstedt.kvitt.design.component.ProvideHousehold
|
||||
import se.hagstedt.kvitt.design.component.SectionHeader
|
||||
import se.hagstedt.kvitt.design.component.SegmentedGroup
|
||||
import se.hagstedt.kvitt.design.component.SegmentedItemScope
|
||||
import se.hagstedt.kvitt.design.component.SettlementRow
|
||||
import se.hagstedt.kvitt.design.component.TopBarNavigation
|
||||
import se.hagstedt.kvitt.design.component.kvittLanguage
|
||||
import se.hagstedt.kvitt.design.component.personName
|
||||
import se.hagstedt.kvitt.design.component.relativeTime
|
||||
import se.hagstedt.kvitt.design.component.savedSplitLabel
|
||||
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.model.EntityType
|
||||
import se.hagstedt.kvitt.model.ExpenseId
|
||||
import se.hagstedt.kvitt.model.LedgerRow
|
||||
import se.hagstedt.kvitt.model.RejectedVersion
|
||||
import se.hagstedt.kvitt.model.Rejection
|
||||
import se.hagstedt.kvitt.model.SettlementId
|
||||
import se.hagstedt.kvitt.model.SyncRejectionReason
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.semantics.heading
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import se.hagstedt.kvitt.design.component.FactRow
|
||||
import se.hagstedt.kvitt.design.component.KvittBadge
|
||||
import se.hagstedt.kvitt.design.component.SegmentedItemScope
|
||||
import se.hagstedt.kvitt.design.component.defaultModeLabel
|
||||
import se.hagstedt.kvitt.design.component.personName
|
||||
import se.hagstedt.kvitt.design.component.splitModeLabel
|
||||
import se.hagstedt.kvitt.design.format.MoneyFormatter
|
||||
import se.hagstedt.kvitt.design.KvittElevation
|
||||
import se.hagstedt.kvitt.model.RejectedVersion
|
||||
import se.hagstedt.kvitt.model.SplitMode
|
||||
import kotlin.time.Instant
|
||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
||||
|
||||
@@ -383,7 +380,7 @@ private fun LostFacts(lost: RejectedVersion, ui: SyncStatusUi.Ready, modifier: M
|
||||
add { item -> ExpenseRow(item, row, onClick = null) }
|
||||
lost.accountName?.let { name -> add { item -> FactRow(item, KvittIcon.Inventory2, stringResource(DesignR.string.editor_account), name) } }
|
||||
lost.payer?.let { payer -> add { item -> FactRow(item, KvittIcon.Person, stringResource(DesignR.string.editor_paid_by), personName(payer)) } }
|
||||
lost.mode?.let { mode -> add { item -> FactRow(item, KvittIcon.CallSplit, stringResource(DesignR.string.editor_split), lostModeLabel(mode, lost, ui)) } }
|
||||
lost.mode?.let { mode -> add { item -> FactRow(item, KvittIcon.CallSplit, stringResource(DesignR.string.editor_split), savedSplitLabel(mode, lost.shares, ui.me.id, ui.partner?.displayName.orEmpty())) } }
|
||||
if (lost.notes.isNotEmpty()) add { item -> FactRow(item, KvittIcon.Notes, stringResource(DesignR.string.editor_notes), lost.notes) }
|
||||
}
|
||||
is LedgerRow.SettlementRow -> {
|
||||
@@ -394,23 +391,3 @@ private fun LostFacts(lost: RejectedVersion, ui: SyncStatusUi.Ready, modifier: M
|
||||
}
|
||||
SegmentedGroup(facts.size, modifier) { item -> facts[item.index](item) }
|
||||
}
|
||||
|
||||
/** "Kontots standard 50/50" / "Egen fördelning 70/30" / "Exakta belopp" / "Allt på Anna", as on S-8. */
|
||||
@Composable
|
||||
private fun lostModeLabel(mode: SplitMode, lost: RejectedVersion, ui: SyncStatusUi.Ready): String {
|
||||
val language = kvittLanguage()
|
||||
val mineBp = lost.shares.firstOrNull { it.userId == ui.me.id }?.shareBp
|
||||
val ratio = mineBp?.let { stringResource(DesignR.string.common_ratio, MoneyFormatter.basisPoints(it, language), MoneyFormatter.basisPoints(BP_TOTAL - it, language)) }
|
||||
return when (mode) {
|
||||
SplitMode.DEFAULT -> if (ratio != null) stringResource(DesignR.string.editor_split_mode_default, ratio) else splitModeLabel(mode, defaultModeLabel(HALF_PERCENT))
|
||||
SplitMode.RATIO -> splitModeLabel(mode, "") + (ratio?.let { " $it" } ?: "")
|
||||
SplitMode.EXACT -> splitModeLabel(mode, "")
|
||||
SplitMode.FULL -> {
|
||||
val bearer = lost.shares.firstOrNull { it.shareOre > 0 }?.userId
|
||||
if (bearer == ui.me.id) stringResource(DesignR.string.editor_split_full_on_you) else stringResource(DesignR.string.editor_split_full_on, ui.partner?.displayName.orEmpty())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val BP_TOTAL = 10_000
|
||||
private const val HALF_PERCENT = 50
|
||||
|
||||
+2
-2
@@ -2,6 +2,7 @@ package se.hagstedt.kvitt.feature.onboarding
|
||||
|
||||
import se.hagstedt.kvitt.design.component.INVITE_LENGTH
|
||||
import se.hagstedt.kvitt.design.component.normalizeInviteCode
|
||||
import se.hagstedt.kvitt.design.format.DISPLAY_NAME_MAX
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.net.URLDecoder
|
||||
@@ -62,11 +63,10 @@ object InviteLink {
|
||||
/** A display name of §9.6 (1 to 40 code points after trimming, no control characters), else null. */
|
||||
private fun adminName(value: String?): String? {
|
||||
val name = value?.trim() ?: return null
|
||||
return name.takeIf { it.codePointCount(0, it.length) in 1..NAME_MAX && it.none(Char::isISOControl) }
|
||||
return name.takeIf { it.codePointCount(0, it.length) in 1..DISPLAY_NAME_MAX && it.none(Char::isISOControl) }
|
||||
}
|
||||
|
||||
private const val FROM = "from"
|
||||
private const val NAME_MAX = 40
|
||||
|
||||
/** The normalised code when [text] normalises to exactly 12 alphabet characters (§8.3), else null. */
|
||||
private fun code(text: String): String? {
|
||||
|
||||
+3
-2
@@ -40,15 +40,16 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.launch
|
||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
||||
import se.hagstedt.kvitt.design.KvittTheme
|
||||
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.format.DISPLAY_NAME_MAX
|
||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
||||
|
||||
/** What S-1 can be asked to do; the stateless [PairingContent] calls these. */
|
||||
data class PairingActions(
|
||||
@@ -172,7 +173,7 @@ fun PairingContent(
|
||||
onValueChange = actions.onNameChange,
|
||||
label = stringResource(R.string.onboarding_name_label),
|
||||
error = if (state.nameTooLong) {
|
||||
pluralStringResource(DesignR.plurals.error_too_long, PairingUiState.NAME_MAX, PairingUiState.NAME_MAX)
|
||||
pluralStringResource(DesignR.plurals.error_too_long, DISPLAY_NAME_MAX, DISPLAY_NAME_MAX)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
|
||||
+5
-7
@@ -10,8 +10,10 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import se.hagstedt.kvitt.design.component.INVITE_LENGTH
|
||||
import se.hagstedt.kvitt.design.component.normalizeInviteCode
|
||||
import se.hagstedt.kvitt.model.OnlineResult
|
||||
import se.hagstedt.kvitt.design.format.DISPLAY_NAME_MAX
|
||||
import se.hagstedt.kvitt.design.format.FAILURE_NETWORK
|
||||
import se.hagstedt.kvitt.model.InviteKind
|
||||
import se.hagstedt.kvitt.model.OnlineResult
|
||||
import se.hagstedt.kvitt.model.SessionRepository
|
||||
import se.hagstedt.kvitt.model.SessionState
|
||||
import javax.inject.Inject
|
||||
@@ -63,16 +65,12 @@ data class PairingUiState(
|
||||
val nameRequired: Boolean get() = mode == PairingMode.First
|
||||
|
||||
/** The display name is 1 to 40 code points after trimming (§9.6). */
|
||||
val nameTooLong: Boolean get() = nameRequired && name.trim().let { it.codePointCount(0, it.length) } > NAME_MAX
|
||||
val nameTooLong: Boolean get() = nameRequired && name.trim().let { it.codePointCount(0, it.length) } > DISPLAY_NAME_MAX
|
||||
|
||||
/** "Para ihop" is enabled when the code has 12 characters, the name is set (first pairing), the address is well formed. */
|
||||
val canPair: Boolean
|
||||
get() = !pairing && code.length == INVITE_LENGTH && ServerAddress.isWellFormed(serverUrl) &&
|
||||
(!nameRequired || (name.isNotBlank() && !nameTooLong))
|
||||
|
||||
companion object {
|
||||
const val NAME_MAX = 40
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,7 +186,7 @@ class PairingViewModel internal constructor(
|
||||
internal fun pairingError(code: String): PairingError = when (code) {
|
||||
"invite_invalid" -> PairingError.InviteInvalid
|
||||
"rate_limited" -> PairingError.RateLimited
|
||||
"network" -> PairingError.Network
|
||||
FAILURE_NETWORK -> PairingError.Network
|
||||
"protocol_unsupported" -> PairingError.AppTooOld
|
||||
"internal", "unavailable", "not_implemented" -> PairingError.Server
|
||||
else -> PairingError.Validation
|
||||
|
||||
+3
-2
@@ -46,7 +46,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
||||
import se.hagstedt.kvitt.design.KvittTheme
|
||||
import se.hagstedt.kvitt.design.component.AvatarShape
|
||||
import se.hagstedt.kvitt.design.component.AvatarState
|
||||
@@ -61,11 +60,13 @@ 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.DISPLAY_NAME_MAX
|
||||
import se.hagstedt.kvitt.design.format.KvittLanguage
|
||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||
import se.hagstedt.kvitt.model.User
|
||||
import java.io.IOException
|
||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
||||
|
||||
/** What S-2 can be asked to do; the stateless [ProfileStepContent] calls these. */
|
||||
data class ProfileStepActions(
|
||||
@@ -256,7 +257,7 @@ private fun ProfileCard(state: ProfileStepUiState, actions: ProfileStepActions)
|
||||
label = stringResource(R.string.onboarding_name_label),
|
||||
error = when {
|
||||
state.nameBlank -> stringResource(R.string.onboarding_error_name_required)
|
||||
state.nameTooLong -> pluralStringResource(DesignR.plurals.error_too_long, PairingUiState.NAME_MAX, PairingUiState.NAME_MAX)
|
||||
state.nameTooLong -> pluralStringResource(DesignR.plurals.error_too_long, DISPLAY_NAME_MAX, DISPLAY_NAME_MAX)
|
||||
else -> null
|
||||
},
|
||||
supporting = if (state.online) null else stringResource(DesignR.string.common_needs_connection),
|
||||
|
||||
+5
-3
@@ -9,6 +9,8 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import se.hagstedt.kvitt.design.format.DISPLAY_NAME_MAX
|
||||
import se.hagstedt.kvitt.design.format.FAILURE_NETWORK
|
||||
import se.hagstedt.kvitt.model.ConnectivityRepository
|
||||
import se.hagstedt.kvitt.model.OnlineResult
|
||||
import se.hagstedt.kvitt.model.ProfileRepository
|
||||
@@ -40,7 +42,7 @@ data class ProfileStepUiState(
|
||||
val done: Boolean = false,
|
||||
) {
|
||||
val nameBlank: Boolean get() = name.isBlank()
|
||||
val nameTooLong: Boolean get() = name.trim().let { it.codePointCount(0, it.length) } > PairingUiState.NAME_MAX
|
||||
val nameTooLong: Boolean get() = name.trim().let { it.codePointCount(0, it.length) } > DISPLAY_NAME_MAX
|
||||
val hasPicture: Boolean get() = me?.pictureVersion != null
|
||||
|
||||
/** "Fortsätt": disabled while the picture uploads and while the name is blank or too long. */
|
||||
@@ -126,7 +128,7 @@ class ProfileStepViewModel @Inject constructor(
|
||||
OnlineResult.Offline -> it.copy(saving = false, message = ProfileStepMessage.CouldNotReachServer)
|
||||
is OnlineResult.Failed -> it.copy(
|
||||
saving = false,
|
||||
message = if (result.code == "network") ProfileStepMessage.CouldNotReachServer else ProfileStepMessage.NameRefused,
|
||||
message = if (result.code == FAILURE_NETWORK) ProfileStepMessage.CouldNotReachServer else ProfileStepMessage.NameRefused,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -146,7 +148,7 @@ internal fun pictureMessage(result: OnlineResult<Unit>): ProfileStepMessage? = w
|
||||
"payload_too_large" -> ProfileStepMessage.PictureTooLarge
|
||||
"unsupported_media_type" -> ProfileStepMessage.PictureUnsupported
|
||||
"image_invalid" -> ProfileStepMessage.PictureInvalid
|
||||
"network" -> ProfileStepMessage.CouldNotReachServer
|
||||
FAILURE_NETWORK -> ProfileStepMessage.CouldNotReachServer
|
||||
else -> ProfileStepMessage.PictureFailed
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -25,7 +25,6 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
||||
import se.hagstedt.kvitt.design.KvittTheme
|
||||
import se.hagstedt.kvitt.design.component.BalanceHero
|
||||
import se.hagstedt.kvitt.design.component.ButtonSize
|
||||
@@ -44,8 +43,10 @@ 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.format.FAILURE_NETWORK
|
||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||
import kotlin.time.Instant
|
||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
||||
|
||||
/** What S-3 can be asked to do; the stateless [WaitingContent] calls these. */
|
||||
data class WaitingActions(
|
||||
@@ -83,7 +84,7 @@ fun WaitingContent(
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
) {
|
||||
val error = state.loadError?.let {
|
||||
if (it == "network") DesignR.string.common_could_not_reach_server else DesignR.string.error_server
|
||||
if (it == FAILURE_NETWORK) DesignR.string.common_could_not_reach_server else DesignR.string.error_server
|
||||
}
|
||||
val resources = LocalResources.current
|
||||
LaunchedEffect(error) {
|
||||
|
||||
+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