M6b: core:design fixes after the M7 wave
Bugs: viewerPercentOf reads basis points (Samples in bp); FullSplitPicker and the
split labels use editor_split_full_on_you ("Allt på dig"); AmountInput.of restores
money with both decimals (AmountInput.percent for ratio fields), replacing M7c's
moneyInput; quickRowCategories does the D-060 fill itself; KvittScaffold resolves
resource-backed snackbars (showKvittSnackbar) while shown.
Gaps: ConfirmDialog optional and styled body; KvittButton loading form (M7a uses it,
ProgressBottomButton removed); InviteCodeField wraps after a group, placeholder,
imeAction; EmptyState icon tile and actionEnabled; DateFormatter.monthName and
dayRangeLong; AnalyticsFilterBar periodOwnRow; segmentedItem builder; screen tokens
in KvittDimens (OnboardingDimens removed); common_ok, common_undo; module-proposed
keys recorded in strings.csv; quick-row labels end in an ellipsis on line two.
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
@@ -131,7 +131,7 @@ fun AccountEditorContent(ui: AccountEditorUi, actions: AccountEditorActions, mod
|
||||
ConfirmDialog(
|
||||
icon = KvittIcon.Delete,
|
||||
title = stringResource(DesignR.string.common_discard_title),
|
||||
body = "",
|
||||
body = null,
|
||||
confirm = stringResource(DesignR.string.common_discard),
|
||||
dismiss = stringResource(DesignR.string.common_keep_editing),
|
||||
onConfirm = actions.onDiscard,
|
||||
|
||||
+5
-2
@@ -58,6 +58,7 @@ import se.hagstedt.kvitt.model.Granularity
|
||||
import se.hagstedt.kvitt.model.GroupBy
|
||||
import se.hagstedt.kvitt.model.GroupKey
|
||||
import se.hagstedt.kvitt.model.Measure
|
||||
import se.hagstedt.kvitt.model.RangePreset
|
||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
||||
|
||||
/**
|
||||
@@ -141,13 +142,15 @@ private fun Filters(state: AnalyticsUiState, events: AnalyticsEvents) {
|
||||
listOf(stringResource(R.string.analytics_group_by_category), stringResource(R.string.analytics_group_by_account)),
|
||||
GroupBy.entries.indexOf(filter.groupBy),
|
||||
) { events.onGroupBy(GroupBy.entries[it]) },
|
||||
// A custom range label ("Anpassad · 24 aug.–17 okt.") gets a row of its own (design frame S-11-analytics-week).
|
||||
periodOwnRow = filter.preset == RangePreset.CUSTOM,
|
||||
)
|
||||
}
|
||||
|
||||
/** The range caption: "maj–okt 2026 · Hushållet", or "24 aug.–17 okt. · 8 veckor" in week granularity. */
|
||||
@Composable
|
||||
private fun totalCaption(data: AnalyticsState): String {
|
||||
val range = rangeText(data.start, data.end, data.filter, LocalKvittClock.current.today, kvittLanguage())
|
||||
val range = rangeText(data.start, data.end, data.filter, LocalKvittClock.current.today, kvittLanguage(), long = true)
|
||||
val second = when {
|
||||
data.filter.granularity == Granularity.WEEK -> pluralStringResource(R.plurals.analytics_weeks, data.buckets.size, data.buckets.size)
|
||||
data.filter.measure == Measure.HOUSEHOLD -> stringResource(R.string.analytics_measure_household)
|
||||
@@ -192,7 +195,7 @@ private fun Ready(content: AnalyticsContent.Ready, events: AnalyticsEvents, open
|
||||
private fun OverTime(content: AnalyticsContent.Ready, onBucket: (String) -> Unit) {
|
||||
val data = content.data
|
||||
val groups = chartGroups(data, content.categories, content.accounts)
|
||||
val range = rangeText(data.start, data.end, data.filter, LocalKvittClock.current.today, kvittLanguage())
|
||||
val range = rangeText(data.start, data.end, data.filter, LocalKvittClock.current.today, kvittLanguage(), long = true)
|
||||
val highest = data.buckets.maxByOrNull { it.totalOre }
|
||||
val lowest = data.buckets.minByOrNull { it.totalOre }
|
||||
val summary = if (highest != null && lowest != null) {
|
||||
|
||||
+10
-6
@@ -31,14 +31,18 @@ internal fun presetLabel(preset: RangePreset): Int = when (preset) {
|
||||
internal fun measureLabel(measure: Measure): Int =
|
||||
if (measure == Measure.HOUSEHOLD) R.string.analytics_measure_household else R.string.analytics_measure_my_share
|
||||
|
||||
/** The range in a caption: whole months "maj–okt 2026" for a month preset, else days "24 aug.–17 okt.". */
|
||||
internal fun rangeText(start: String, end: String, filter: AnalyticsFilter, today: LocalDate, language: KvittLanguage): String {
|
||||
/**
|
||||
* The range in a title or caption: whole months "maj–okt 2026" for a month preset, else days: short "24 aug.–17 okt."
|
||||
* or, with [long] (the total card caption and the chart summary, design frame S-11-analytics-week), written out with the
|
||||
* year "24 augusti–17 oktober 2026" (`DateFormatter.dayRangeLong`).
|
||||
*/
|
||||
internal fun rangeText(start: String, end: String, filter: AnalyticsFilter, today: LocalDate, language: KvittLanguage, long: Boolean = false): String {
|
||||
val first = LocalDate.parse(start)
|
||||
val last = LocalDate.parse(end)
|
||||
return if (filter.granularity == Granularity.MONTH && filter.preset != RangePreset.CUSTOM) {
|
||||
DateFormatter.monthRange(first, last, language)
|
||||
} else {
|
||||
DateFormatter.dayRange(first, last, today, language)
|
||||
return when {
|
||||
filter.granularity == Granularity.MONTH && filter.preset != RangePreset.CUSTOM -> DateFormatter.monthRange(first, last, language)
|
||||
long -> DateFormatter.dayRangeLong(first, last, language)
|
||||
else -> DateFormatter.dayRange(first, last, today, language)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,12 +42,8 @@
|
||||
<item quantity="one">%d utgift</item>
|
||||
<item quantity="other">%d utgifter</item>
|
||||
</plurals>
|
||||
<!-- proposed (M7e): radio-group name of the month/week toggle -->
|
||||
<string name="analytics_granularity">Indelning</string>
|
||||
<!-- proposed (M7e): radio-group name of the measure toggle -->
|
||||
<string name="analytics_measure">Mått</string>
|
||||
<!-- proposed (M7e): radio-group name of the group-by toggle -->
|
||||
<string name="analytics_group_by">Gruppera efter</string>
|
||||
<!-- proposed (M7e): confirm button of the custom range picker -->
|
||||
<string name="analytics_range_apply">Använd</string>
|
||||
</resources>
|
||||
|
||||
@@ -42,12 +42,8 @@
|
||||
<item quantity="one">%d expense</item>
|
||||
<item quantity="other">%d expenses</item>
|
||||
</plurals>
|
||||
<!-- proposed (M7e): radio-group name of the month/week toggle -->
|
||||
<string name="analytics_granularity">Interval</string>
|
||||
<!-- proposed (M7e): radio-group name of the measure toggle -->
|
||||
<string name="analytics_measure">Measure</string>
|
||||
<!-- proposed (M7e): radio-group name of the group-by toggle -->
|
||||
<string name="analytics_group_by">Group by</string>
|
||||
<!-- proposed (M7e): confirm button of the custom range picker -->
|
||||
<string name="analytics_range_apply">Apply</string>
|
||||
</resources>
|
||||
|
||||
+3
-3
@@ -43,7 +43,6 @@ import se.hagstedt.kvitt.design.component.KvittIconImage
|
||||
import se.hagstedt.kvitt.design.component.KvittTopAppBar
|
||||
import se.hagstedt.kvitt.design.component.LocalKvittClock
|
||||
import se.hagstedt.kvitt.design.component.MoneyText
|
||||
import se.hagstedt.kvitt.design.component.NeedsConnectionHint
|
||||
import se.hagstedt.kvitt.design.component.PersonAvatar
|
||||
import se.hagstedt.kvitt.design.component.ProvideHousehold
|
||||
import se.hagstedt.kvitt.design.component.SegmentedGroup
|
||||
@@ -141,12 +140,13 @@ private fun ColumnScope.Missing(state: ExpenseDetailUiState, actions: ExpenseDet
|
||||
EmptyState(
|
||||
title = stringResource(R.string.expense_detail_missing),
|
||||
body = stringResource(R.string.expense_detail_missing_body),
|
||||
icon = KvittIcon.SyncProblem,
|
||||
action = stringResource(R.string.expense_detail_sync_now),
|
||||
actionIcon = KvittIcon.Sync,
|
||||
actionKind = ButtonKind.Tonal,
|
||||
actionEnabled = state.online,
|
||||
onAction = { if (state.online && !state.syncing) actions.syncNow() },
|
||||
)
|
||||
if (!state.online) NeedsConnectionHint(Modifier.align(Alignment.CenterHorizontally))
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -256,7 +256,7 @@ private fun modeLabel(detail: ExpenseDetailState, me: User, partner: User): Stri
|
||||
SplitMode.EXACT -> splitModeLabel(SplitMode.EXACT, "")
|
||||
SplitMode.FULL -> {
|
||||
val bearer = detail.shares.firstOrNull { it.shareOre > 0 }?.userId
|
||||
if (bearer == me.id) stringResource(R.string.expense_split_full_on_you) else stringResource(DesignR.string.editor_split_full_on, partner.displayName)
|
||||
if (bearer == me.id) stringResource(DesignR.string.editor_split_full_on_you) else stringResource(DesignR.string.editor_split_full_on, partner.displayName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-7
@@ -86,6 +86,7 @@ import se.hagstedt.kvitt.design.component.defaultModeLabel
|
||||
import se.hagstedt.kvitt.design.component.kvittLanguage
|
||||
import se.hagstedt.kvitt.design.component.money
|
||||
import se.hagstedt.kvitt.design.component.splitModeLabel
|
||||
import se.hagstedt.kvitt.design.component.viewerPercentOf
|
||||
import se.hagstedt.kvitt.design.format.AmountInput
|
||||
import se.hagstedt.kvitt.design.format.AmountKey
|
||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||
@@ -354,9 +355,9 @@ internal fun EditorDialog(state: ExpenseEditorState, actions: ExpenseEditorActio
|
||||
val confirm = stringResource(DesignR.string.common_discard)
|
||||
val dismiss = stringResource(DesignR.string.common_keep_editing)
|
||||
if (inline) {
|
||||
ConfirmDialogContent(KvittIcon.Delete, title, "", confirm, dismiss, actions::discard, actions::dismissOverlay, destructive = true)
|
||||
ConfirmDialogContent(KvittIcon.Delete, title, null, confirm, dismiss, actions::discard, actions::dismissOverlay, destructive = true)
|
||||
} else {
|
||||
ConfirmDialog(KvittIcon.Delete, title, "", confirm, dismiss, actions::discard, actions::dismissOverlay, destructive = true)
|
||||
ConfirmDialog(KvittIcon.Delete, title, null, confirm, dismiss, actions::discard, actions::dismissOverlay, destructive = true)
|
||||
}
|
||||
}
|
||||
EditorOverlay.Delete -> DeleteExpenseDialog(actions::confirmDelete, actions::dismissOverlay, inline)
|
||||
@@ -399,7 +400,7 @@ private fun LocalizedDatePicker(date: String, today: String, actions: ExpenseEdi
|
||||
)
|
||||
val confirm: @Composable () -> Unit = {
|
||||
TextButton({ picker.selectedDateMillis?.let { actions.onDate(LocalDate.fromEpochDays(it / DAY_MILLIS).toString()) } ?: actions.dismissOverlay() }) {
|
||||
Text(stringResource(android.R.string.ok))
|
||||
Text(stringResource(DesignR.string.common_ok))
|
||||
}
|
||||
}
|
||||
val dismiss: @Composable () -> Unit = { TextButton(actions::dismissOverlay) { Text(stringResource(DesignR.string.common_cancel)) } }
|
||||
@@ -426,8 +427,8 @@ private fun ErrorLine(text: String) {
|
||||
/** The viewer's percent of the account's default ratio (bp in `Account.defaultShares`, §14.2.4). */
|
||||
private fun accountViewerPercent(state: ExpenseEditorState): Int {
|
||||
val me = state.me ?: return HALF
|
||||
val bp = state.account?.defaultShares?.get(me.id) ?: return HALF
|
||||
return (bp / PERCENT_HUNDREDTHS).toInt()
|
||||
val account = state.account ?: return HALF
|
||||
return viewerPercentOf(account.defaultShares, me.id)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -442,14 +443,14 @@ private fun modeChipLabel(state: ExpenseEditorState): String {
|
||||
SplitMode.RATIO -> {
|
||||
val language = kvittLanguage()
|
||||
val viewer = form.ratioViewer.ore ?: return splitModeLabel(form.mode, "")
|
||||
val ratio = stringResource(DesignR.string.common_ratio, form.ratioViewer.display(language), AmountInput.of(AmountInput.MAX_PERCENT_HUNDREDTHS - viewer).display(language))
|
||||
val ratio = stringResource(DesignR.string.common_ratio, form.ratioViewer.display(language), AmountInput.percent(AmountInput.MAX_PERCENT_HUNDREDTHS - viewer).display(language))
|
||||
splitModeLabel(form.mode, "") + " " + ratio
|
||||
}
|
||||
SplitMode.EXACT -> splitModeLabel(form.mode, "")
|
||||
SplitMode.FULL -> {
|
||||
val partner = state.partner
|
||||
when {
|
||||
form.fullOn == state.me?.id -> stringResource(R.string.expense_split_full_on_you)
|
||||
form.fullOn == state.me?.id -> stringResource(DesignR.string.editor_split_full_on_you)
|
||||
partner == null -> splitModeLabel(form.mode, "")
|
||||
else -> stringResource(DesignR.string.editor_split_full_on, partner.displayName)
|
||||
}
|
||||
|
||||
+10
-16
@@ -197,16 +197,16 @@ class ExpenseEditorViewModel @Inject constructor(
|
||||
id = draft.id,
|
||||
accountId = draft.accountId,
|
||||
categoryId = draft.categoryId,
|
||||
amount = draft.amountOre?.let(::moneyInput) ?: AmountInput(),
|
||||
amount = draft.amountOre?.let(AmountInput::of) ?: AmountInput(),
|
||||
description = draft.description,
|
||||
notes = draft.notes,
|
||||
date = draft.date,
|
||||
payer = draft.payer,
|
||||
mode = draft.mode,
|
||||
ratioViewer = viewerBp?.let { AmountInput.of(it.toLong()) } ?: AmountInput(),
|
||||
ratioPartner = viewerBp?.let { AmountInput.of(BP_TOTAL - it) } ?: AmountInput(),
|
||||
ratioViewer = viewerBp?.let { AmountInput.percent(it.toLong()) } ?: AmountInput(),
|
||||
ratioPartner = viewerBp?.let { AmountInput.percent(BP_TOTAL - it) } ?: AmountInput(),
|
||||
exactTyped = me,
|
||||
exactInput = exactMine?.let(::moneyInput) ?: AmountInput(),
|
||||
exactInput = exactMine?.let(AmountInput::of) ?: AmountInput(),
|
||||
fullOn = draft.fullOn ?: if (draft.payer == me) partner else me,
|
||||
)
|
||||
ui.update {
|
||||
@@ -255,8 +255,9 @@ class ExpenseEditorViewModel @Inject constructor(
|
||||
val choices = options.accounts.map { a -> AccountChoice(summaries[a.id] ?: AccountSummary(a, 0, 0), colorIndex[a.id] ?: 0) }
|
||||
val account = form.accountId?.let { id -> options.accounts.firstOrNull { it.id == id } ?: env.all.firstOrNull { it.id == id } }
|
||||
val active = options.categories.filter { !it.archived }.sortedBy { it.sortOrder }
|
||||
// D-059/D-060: the account's used categories, filled up to five from sort_order; the selected one takes the fifth place.
|
||||
val frequent = u.frequent.ifEmpty { active.take(QUICK_ROW).map { it.id } }
|
||||
// D-059/D-060: the account's used categories as the repository gives them; quickRowCategories fills up to five
|
||||
// from sort_order and puts the selected one fifth.
|
||||
val frequent = u.frequent
|
||||
return ExpenseEditorState(
|
||||
ready = true,
|
||||
isEdit = form.id != null,
|
||||
@@ -306,7 +307,7 @@ class ExpenseEditorViewModel @Inject constructor(
|
||||
KeypadTarget.RatioViewer, KeypadTarget.RatioPartner -> {
|
||||
val typed = if (target == KeypadTarget.RatioViewer) form.ratioViewer else form.ratioPartner
|
||||
val next = typed.press(key, AmountInput.MAX_PERCENT_HUNDREDTHS) ?: return false
|
||||
val other = next.ore?.let { AmountInput.of(AmountInput.MAX_PERCENT_HUNDREDTHS - it) } ?: AmountInput()
|
||||
val other = next.ore?.let { AmountInput.percent(AmountInput.MAX_PERCENT_HUNDREDTHS - it) } ?: AmountInput()
|
||||
editForm {
|
||||
if (target == KeypadTarget.RatioViewer) it.copy(ratioViewer = next, ratioPartner = other) else it.copy(ratioPartner = next, ratioViewer = other)
|
||||
}
|
||||
@@ -375,7 +376,7 @@ class ExpenseEditorViewModel @Inject constructor(
|
||||
val form = u.form ?: return@update u
|
||||
val next = when (mode) {
|
||||
SplitMode.DEFAULT -> form.copy(mode = mode)
|
||||
SplitMode.RATIO -> form.copy(mode = mode, ratioViewer = AmountInput.of(viewerBp), ratioPartner = AmountInput.of(BP_TOTAL - viewerBp))
|
||||
SplitMode.RATIO -> form.copy(mode = mode, ratioViewer = AmountInput.percent(viewerBp), ratioPartner = AmountInput.percent(BP_TOTAL - viewerBp))
|
||||
SplitMode.EXACT -> form.copy(mode = mode, exactTyped = me, exactInput = AmountInput())
|
||||
SplitMode.FULL -> form.copy(mode = mode, fullOn = if (form.payer == me) partner else me)
|
||||
}
|
||||
@@ -394,7 +395,7 @@ class ExpenseEditorViewModel @Inject constructor(
|
||||
/** Ratio slider and presets: whole percent for the viewer. */
|
||||
override fun onViewerPercent(percent: Int) {
|
||||
val bp = percent.coerceIn(0, PERCENT) * PERCENT.toLong()
|
||||
editForm { it.copy(ratioViewer = AmountInput.of(bp), ratioPartner = AmountInput.of(BP_TOTAL - bp)) }
|
||||
editForm { it.copy(ratioViewer = AmountInput.percent(bp), ratioPartner = AmountInput.percent(BP_TOTAL - bp)) }
|
||||
clearError(FIELD_SPLIT)
|
||||
}
|
||||
|
||||
@@ -525,15 +526,8 @@ class ExpenseEditorViewModel @Inject constructor(
|
||||
private const val BP_TOTAL = 10_000L
|
||||
private const val HALF_BP = 5000
|
||||
private const val PERCENT = 100
|
||||
private const val QUICK_ROW = 5
|
||||
private val SPLIT_TARGETS = setOf(KeypadTarget.RatioViewer, KeypadTarget.RatioPartner, KeypadTarget.Exact)
|
||||
|
||||
/** A stored amount as editable text with both decimals when it has öre: 124 760 -> "1247.60", 41 200 -> "412". */
|
||||
fun moneyInput(ore: Long): AmountInput {
|
||||
val cents = ore % ORE_PER_KRONA
|
||||
return AmountInput(if (cents == 0L) (ore / ORE_PER_KRONA).toString() else "${ore / ORE_PER_KRONA}." + cents.toString().padStart(2, '0'))
|
||||
}
|
||||
|
||||
private const val ORE_PER_KRONA = 100L
|
||||
|
||||
/** The draft of §14.2.4 from the form: exact derives the other share as amount minus the typed one (AC-8.7). */
|
||||
|
||||
@@ -19,7 +19,5 @@
|
||||
<string name="expense_detail_deleted_body">Den räknas inte längre i saldon eller statistik.</string>
|
||||
<string name="expense_detail_missing">Utgiften finns inte på telefonen än</string>
|
||||
<string name="expense_detail_missing_body">Den kan ha lagts till nyss på en annan telefon. Synka för att hämta den.</string>
|
||||
<!-- Not in strings.csv yet (proposed in the M7c report): S-8 "Synka nu" and the split label "Allt på dig". -->
|
||||
<string name="expense_detail_sync_now">Synka nu</string>
|
||||
<string name="expense_split_full_on_you">Allt på dig</string>
|
||||
</resources>
|
||||
|
||||
@@ -19,7 +19,5 @@
|
||||
<string name="expense_detail_deleted_body">It no longer counts in balances or analytics.</string>
|
||||
<string name="expense_detail_missing">This expense is not on the phone yet</string>
|
||||
<string name="expense_detail_missing_body">It may have just been added on another phone. Sync to fetch it.</string>
|
||||
<!-- Not in strings.csv yet (proposed in the M7c report): S-8 "Synka nu" and the split label "Allt på dig". -->
|
||||
<string name="expense_detail_sync_now">Sync now</string>
|
||||
<string name="expense_split_full_on_you">All on you</string>
|
||||
</resources>
|
||||
|
||||
+4
-2
@@ -17,6 +17,7 @@ import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import se.hagstedt.kvitt.design.format.AmountInput
|
||||
import se.hagstedt.kvitt.design.format.AmountKey
|
||||
import se.hagstedt.kvitt.fakes.FakeRepositories
|
||||
import se.hagstedt.kvitt.fakes.FakeScenario
|
||||
@@ -269,8 +270,9 @@ class ExpenseEditorViewModelTest {
|
||||
fun editedAmounts_keepBothDecimals() = runTest(dispatcher) {
|
||||
val (vm, _) = editor(args = mapOf(ExpenseEditorViewModel.ARG_EXPENSE_ID to ExpenseScenarios.icaId.value))
|
||||
assertEquals("1247.60", vm.state.value.form?.amount?.raw)
|
||||
assertEquals("412", ExpenseEditorViewModel.moneyInput(41_200).raw)
|
||||
assertEquals("0.05", ExpenseEditorViewModel.moneyInput(5).raw)
|
||||
// The feature's own moneyInput is gone: :core:design's AmountInput.of restores money with both decimals (M6b).
|
||||
assertEquals("412.00", AmountInput.of(41_200).raw)
|
||||
assertEquals("0.05", AmountInput.of(5).raw)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -53,6 +53,7 @@ import se.hagstedt.kvitt.design.component.Skeleton
|
||||
import se.hagstedt.kvitt.design.component.SyncStatusPill
|
||||
import se.hagstedt.kvitt.design.component.UpdateBanner
|
||||
import se.hagstedt.kvitt.design.component.kvittLanguage
|
||||
import se.hagstedt.kvitt.design.component.styledStringResource
|
||||
import se.hagstedt.kvitt.design.format.DateFormatter
|
||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||
@@ -174,7 +175,7 @@ private fun HomeList(ui: HomeUi.Ready, partner: User, actions: HomeActions, padd
|
||||
item(key = "hero") { BalanceHero(HeroState.Loading, ui.home.me, words, actions.onSettleUp) }
|
||||
item(key = "pill") {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
Skeleton(spacing.s1, KvittDimens.pillMinHeight, Modifier.fillMaxWidth(SKELETON_PILL_FRACTION), pill = true)
|
||||
Skeleton(KvittDimens.skeletonPillWidth, KvittDimens.pillMinHeight, pill = true)
|
||||
}
|
||||
}
|
||||
item(key = "header") { SectionHeader(stringResource(R.string.home_accounts)) }
|
||||
@@ -234,7 +235,6 @@ private fun HomeList(ui: HomeUi.Ready, partner: User, actions: HomeActions, padd
|
||||
}
|
||||
|
||||
private const val SKELETON_CARDS = 3
|
||||
private const val SKELETON_PILL_FRACTION = 0.5f
|
||||
|
||||
@Composable
|
||||
private fun UpdateBannerItem(ui: HomeUi.Ready, actions: HomeActions) {
|
||||
@@ -252,8 +252,7 @@ private fun HomeAccountCard(summary: AccountSummary, partner: User, colorIndex:
|
||||
private fun expensesLine(count: Int): String {
|
||||
val today = LocalKvittClock.current.today
|
||||
val period = today.year.toString() + "-" + today.month.ordinal.plus(1).toString().padStart(2, '0')
|
||||
// monthYear is "oktober 2026" / "October 2026"; running text needs the month alone.
|
||||
val month = DateFormatter.monthYear(period, kvittLanguage()).substringBeforeLast(' ')
|
||||
val month = DateFormatter.monthName(period, kvittLanguage())
|
||||
return if (count > 0) pluralStringResource(R.plurals.home_account_expenses_month, count, count, month) else stringResource(R.string.home_account_no_expenses_month, month)
|
||||
}
|
||||
|
||||
@@ -263,8 +262,8 @@ private fun WelcomeCard(me: User, partner: User) {
|
||||
KvittCard(Modifier.fillMaxWidth().semantics(mergeDescendants = true) {}) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(KvittTheme.spacing.cardTileGap)) {
|
||||
Box {
|
||||
PersonAvatar(me, KvittDimens.accountTile)
|
||||
PersonAvatar(partner, KvittDimens.accountTile, Modifier.padding(start = KvittDimens.accountTile - KvittTheme.spacing.s2))
|
||||
PersonAvatar(me, KvittDimens.welcomeAvatar)
|
||||
PersonAvatar(partner, KvittDimens.welcomeAvatar, Modifier.padding(start = KvittDimens.welcomeAvatar - KvittDimens.welcomeAvatarOverlap))
|
||||
}
|
||||
Column(verticalArrangement = Arrangement.spacedBy(KvittTheme.spacing.s1)) {
|
||||
Text(stringResource(R.string.home_empty_welcome, partner.displayName), style = MaterialTheme.typography.titleMedium)
|
||||
@@ -280,7 +279,8 @@ private fun InstallDialog(actions: HomeActions) {
|
||||
ConfirmDialog(
|
||||
icon = KvittIcon.InstallMobile,
|
||||
title = stringResource(DesignR.string.update_permission_title),
|
||||
body = stringResource(DesignR.string.update_permission_body),
|
||||
body = null,
|
||||
styledBody = styledStringResource(DesignR.string.update_permission_body),
|
||||
confirm = stringResource(DesignR.string.common_continue),
|
||||
dismiss = stringResource(DesignR.string.update_later),
|
||||
onConfirm = actions.onInstallContinue,
|
||||
|
||||
@@ -57,8 +57,6 @@
|
||||
<!-- opens the system date settings -->
|
||||
<string name="home_sync_status_open_date_settings">Datum och tid</string>
|
||||
<string name="home_sync_status_last_attempt_clock">Senaste försöket nekades: klockan</string>
|
||||
<!-- PROPOSED (not in strings.csv): TalkBack state of the "Archived (n)" row -->
|
||||
<string name="home_archived_expanded">utfälld</string>
|
||||
<!-- PROPOSED (not in strings.csv): TalkBack state of the "Archived (n)" row -->
|
||||
<string name="home_archived_collapsed">hopfälld</string>
|
||||
</resources>
|
||||
|
||||
@@ -57,8 +57,6 @@
|
||||
<!-- opens the system date settings -->
|
||||
<string name="home_sync_status_open_date_settings">Date & time</string>
|
||||
<string name="home_sync_status_last_attempt_clock">Last attempt refused: the clock</string>
|
||||
<!-- PROPOSED (not in strings.csv): TalkBack state of the "Archived (n)" row -->
|
||||
<string name="home_archived_expanded">expanded</string>
|
||||
<!-- PROPOSED (not in strings.csv): TalkBack state of the "Archived (n)" row -->
|
||||
<string name="home_archived_collapsed">collapsed</string>
|
||||
</resources>
|
||||
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
package se.hagstedt.kvitt.feature.onboarding
|
||||
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Measures of design-spec §4 S-1, S-2 and S-18 that `:core:design`'s `KvittDimens` does not name yet. Kept in this one
|
||||
* file so they move to `KvittDimens` in one step (contract request in the M7a report); nothing else in this module
|
||||
* writes a size.
|
||||
*/
|
||||
internal object OnboardingDimens {
|
||||
// S-1 brand block: padding 28 / 0 / 12, app mark 88 with `receipt_long` 44.
|
||||
val brandTop = 28.dp
|
||||
val brandBottom = 12.dp
|
||||
val appMark = 88.dp
|
||||
val appMarkIcon = 44.dp
|
||||
|
||||
// S-1 column gap 14; bottom bar padding 12 / 16; progress indicator in the button 20 (stroke 2.5).
|
||||
val columnGap = 14.dp
|
||||
val bottomBarVertical = 12.dp
|
||||
val bottomBarHorizontal = 16.dp
|
||||
val buttonProgress = 20.dp
|
||||
val buttonProgressStroke = 2.5.dp
|
||||
|
||||
// S-1 help line icon 20.
|
||||
val helpIcon = 20.dp
|
||||
|
||||
// S-2 intro padding 24 / 4 / 4; card padding 22 / 20, gap 14; avatar badge 40 with icon 20 and a 3 dp card ring.
|
||||
val introTop = 24.dp
|
||||
val introSide = 4.dp
|
||||
val introBottom = 4.dp
|
||||
val cardVertical = 22.dp
|
||||
val cardHorizontal = 20.dp
|
||||
val avatarBadge = 40.dp
|
||||
val avatarBadgeIcon = 20.dp
|
||||
val avatarBadgeRing = 3.dp
|
||||
|
||||
// S-18: padding top 80, gap 14, tile 112 with `phonelink_erase` 52.
|
||||
val removedTop = 80.dp
|
||||
val removedTile = 112.dp
|
||||
val removedIcon = 52.dp
|
||||
}
|
||||
+5
-46
@@ -1,27 +1,11 @@
|
||||
package se.hagstedt.kvitt.feature.onboarding
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import se.hagstedt.kvitt.design.KvittTheme
|
||||
import se.hagstedt.kvitt.design.component.ButtonKind
|
||||
import se.hagstedt.kvitt.design.component.ButtonSize
|
||||
import se.hagstedt.kvitt.design.component.KvittButton
|
||||
@@ -35,40 +19,15 @@ internal fun BottomActionBar(content: @Composable () -> Unit) {
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding()
|
||||
.padding(horizontal = OnboardingDimens.bottomBarHorizontal, vertical = OnboardingDimens.bottomBarVertical),
|
||||
.padding(horizontal = KvittDimens.bottomBarHorizontal, vertical = KvittDimens.bottomBarVertical),
|
||||
) { content() }
|
||||
}
|
||||
|
||||
/** The full-width filled medium button of the bottom bars. */
|
||||
@Composable
|
||||
internal fun BottomButton(text: String, onClick: () -> Unit, icon: KvittIcon?, enabled: Boolean = true) {
|
||||
KvittButton(text, onClick, Modifier.fillMaxWidth(), kind = ButtonKind.Filled, size = ButtonSize.Medium, icon = icon, enabled = enabled)
|
||||
}
|
||||
|
||||
/**
|
||||
* S-1's "Para ihop" while pairing (design-spec S-1 "loading"): the filled medium button keeps its `primary` fill and
|
||||
* shows a 20 dp progress indicator in `on-primary` with "Parar ihop…"; it is not clickable. `KvittButton` has no
|
||||
* progress form (contract request in the M7a report), so this draws the same button with Material's `Button`.
|
||||
* The full-width filled medium button of the bottom bars. [loading] is S-1's "Parar ihop…" (design-spec S-1
|
||||
* "loading"): the fill kept, a 20 dp progress indicator in `on-primary`, not clickable ([KvittButton]'s loading form).
|
||||
*/
|
||||
@Composable
|
||||
internal fun ProgressBottomButton(text: String) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
Button(
|
||||
onClick = {},
|
||||
modifier = Modifier.fillMaxWidth().heightIn(min = KvittDimens.buttonMedium),
|
||||
enabled = true,
|
||||
shape = CircleShape,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = scheme.primary, contentColor = scheme.onPrimary),
|
||||
contentPadding = PaddingValues(horizontal = KvittDimens.buttonMediumHorizontal),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) {
|
||||
CircularProgressIndicator(
|
||||
Modifier.size(OnboardingDimens.buttonProgress),
|
||||
color = scheme.onPrimary,
|
||||
strokeWidth = OnboardingDimens.buttonProgressStroke,
|
||||
)
|
||||
Spacer(Modifier.width(KvittTheme.spacing.s2))
|
||||
Text(text, style = KvittTheme.typography.buttonMedium, textAlign = TextAlign.Center, maxLines = 2)
|
||||
}
|
||||
}
|
||||
internal fun BottomButton(text: String, onClick: () -> Unit, icon: KvittIcon?, enabled: Boolean = true, loading: Boolean = false) {
|
||||
KvittButton(text, onClick, Modifier.fillMaxWidth(), kind = ButtonKind.Filled, size = ButtonSize.Medium, icon = icon, enabled = enabled, loading = loading)
|
||||
}
|
||||
|
||||
+8
-6
@@ -130,7 +130,7 @@ fun PairingContent(
|
||||
bottomBar = {
|
||||
BottomActionBar {
|
||||
if (state.pairing) {
|
||||
ProgressBottomButton(stringResource(R.string.onboarding_pairing))
|
||||
BottomButton(stringResource(R.string.onboarding_pairing), {}, KvittIcon.Link, loading = true)
|
||||
} else {
|
||||
BottomButton(stringResource(R.string.onboarding_pair), actions.onPair, KvittIcon.Link, enabled = state.canPair)
|
||||
}
|
||||
@@ -143,7 +143,7 @@ fun PairingContent(
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = KvittTheme.spacing.gutter),
|
||||
verticalArrangement = Arrangement.spacedBy(OnboardingDimens.columnGap),
|
||||
verticalArrangement = Arrangement.spacedBy(KvittDimens.onboardingGap),
|
||||
) {
|
||||
Brand(state.mode)
|
||||
val codeError = when (state.error) {
|
||||
@@ -160,6 +160,8 @@ fun PairingContent(
|
||||
modifier = Modifier.announced(codeError),
|
||||
error = codeError,
|
||||
enabled = !state.pairing,
|
||||
placeholder = stringResource(R.string.onboarding_code_placeholder),
|
||||
imeAction = if (state.nameRequired) ImeAction.Next else ImeAction.Done,
|
||||
)
|
||||
if (state.nameRequired) {
|
||||
KvittTextField(
|
||||
@@ -211,15 +213,15 @@ private fun Modifier.announced(error: String?): Modifier =
|
||||
private fun Brand(mode: PairingMode) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
Column(
|
||||
Modifier.fillMaxWidth().padding(top = OnboardingDimens.brandTop, bottom = OnboardingDimens.brandBottom),
|
||||
Modifier.fillMaxWidth().padding(top = KvittDimens.brandTop, bottom = KvittDimens.brandBottom),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(KvittTheme.spacing.s3),
|
||||
) {
|
||||
Box(
|
||||
Modifier.size(OnboardingDimens.appMark).background(scheme.primary, KvittTheme.shapes.avatarLarge),
|
||||
Modifier.size(KvittDimens.appMark).background(scheme.primary, KvittTheme.shapes.avatarLarge),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
KvittIconImage(KvittIcon.ReceiptLong, null, filled = true, size = OnboardingDimens.appMarkIcon, tint = scheme.onPrimary)
|
||||
KvittIconImage(KvittIcon.ReceiptLong, null, filled = true, size = KvittDimens.appMarkIcon, tint = scheme.onPrimary)
|
||||
}
|
||||
Text(stringResource(DesignR.string.common_wordmark), style = MaterialTheme.typography.displaySmall, color = scheme.primary)
|
||||
if (mode == PairingMode.Again) {
|
||||
@@ -252,7 +254,7 @@ private fun HelpLine() {
|
||||
Modifier.fillMaxWidth().padding(horizontal = KvittTheme.spacing.s2),
|
||||
horizontalArrangement = Arrangement.spacedBy(KvittTheme.spacing.s3),
|
||||
) {
|
||||
KvittIconImage(KvittIcon.Info, null, size = OnboardingDimens.helpIcon, tint = muted)
|
||||
KvittIconImage(KvittIcon.Info, null, size = KvittDimens.helpIcon, tint = muted)
|
||||
Text(stringResource(R.string.onboarding_code_help_generic), style = MaterialTheme.typography.bodyMedium, color = muted)
|
||||
}
|
||||
}
|
||||
|
||||
+10
-10
@@ -169,10 +169,10 @@ private fun Intro(owner: User) {
|
||||
val ownerName = owner.displayName
|
||||
Column(
|
||||
Modifier.fillMaxWidth().padding(
|
||||
start = OnboardingDimens.introSide,
|
||||
end = OnboardingDimens.introSide,
|
||||
top = OnboardingDimens.introTop,
|
||||
bottom = OnboardingDimens.introBottom,
|
||||
start = KvittDimens.profileIntroSide,
|
||||
end = KvittDimens.profileIntroSide,
|
||||
top = KvittDimens.profileIntroTop,
|
||||
bottom = KvittDimens.profileIntroBottom,
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(KvittTheme.spacing.s2),
|
||||
@@ -197,9 +197,9 @@ private fun ProfileCard(state: ProfileStepUiState, actions: ProfileStepActions)
|
||||
val me = state.me ?: return
|
||||
KvittCard(
|
||||
Modifier.fillMaxWidth(),
|
||||
contentPadding = PaddingValues(horizontal = OnboardingDimens.cardHorizontal, vertical = OnboardingDimens.cardVertical),
|
||||
contentPadding = PaddingValues(horizontal = KvittDimens.profileCardHorizontal, vertical = KvittDimens.profileCardVertical),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(OnboardingDimens.columnGap),
|
||||
verticalArrangement = Arrangement.spacedBy(KvittDimens.onboardingGap),
|
||||
) {
|
||||
Box {
|
||||
PersonAvatar(
|
||||
@@ -212,14 +212,14 @@ private fun ProfileCard(state: ProfileStepUiState, actions: ProfileStepActions)
|
||||
Box(
|
||||
Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.size(OnboardingDimens.avatarBadge)
|
||||
.border(OnboardingDimens.avatarBadgeRing, KvittTheme.colors.card, CircleShape)
|
||||
.padding(OnboardingDimens.avatarBadgeRing)
|
||||
.size(KvittDimens.avatarBadge)
|
||||
.border(KvittDimens.avatarBadgeRing, KvittTheme.colors.card, CircleShape)
|
||||
.padding(KvittDimens.avatarBadgeRing)
|
||||
.background(MaterialTheme.colorScheme.primary, CircleShape)
|
||||
.clearAndSetSemantics {},
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
KvittIconImage(KvittIcon.AddAPhoto, null, size = OnboardingDimens.avatarBadgeIcon, tint = MaterialTheme.colorScheme.onPrimary)
|
||||
KvittIconImage(KvittIcon.AddAPhoto, null, size = KvittDimens.avatarBadgeIcon, tint = MaterialTheme.colorScheme.onPrimary)
|
||||
}
|
||||
}
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(KvittTheme.spacing.s1)) {
|
||||
|
||||
+5
-4
@@ -26,6 +26,7 @@ import se.hagstedt.kvitt.design.KvittTheme
|
||||
import se.hagstedt.kvitt.design.component.KvittIconImage
|
||||
import se.hagstedt.kvitt.design.component.KvittScaffold
|
||||
import se.hagstedt.kvitt.design.icon.KvittIcon
|
||||
import se.hagstedt.kvitt.design.theme.KvittDimens
|
||||
|
||||
/** S-18 Device removed (design-spec S-18, AC-3.3). [onPairAgain] opens S-1 after the revocation is acknowledged. */
|
||||
@Composable
|
||||
@@ -57,15 +58,15 @@ fun RemovedContent(onPairAgain: () -> Unit, modifier: Modifier = Modifier) {
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(start = KvittTheme.spacing.gutter, end = KvittTheme.spacing.gutter, top = OnboardingDimens.removedTop),
|
||||
.padding(start = KvittTheme.spacing.gutter, end = KvittTheme.spacing.gutter, top = KvittDimens.removedTop),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(OnboardingDimens.columnGap),
|
||||
verticalArrangement = Arrangement.spacedBy(KvittDimens.onboardingGap),
|
||||
) {
|
||||
Box(
|
||||
Modifier.size(OnboardingDimens.removedTile).background(scheme.errorContainer, KvittTheme.shapes.illustration),
|
||||
Modifier.size(KvittDimens.removedTile).background(scheme.errorContainer, KvittTheme.shapes.illustration),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
KvittIconImage(KvittIcon.PhonelinkErase, null, size = OnboardingDimens.removedIcon, tint = scheme.onErrorContainer)
|
||||
KvittIconImage(KvittIcon.PhonelinkErase, null, size = KvittDimens.removedIcon, tint = scheme.onErrorContainer)
|
||||
}
|
||||
Text(
|
||||
stringResource(R.string.onboarding_removed_title),
|
||||
|
||||
+2
-2
@@ -93,8 +93,8 @@ fun AboutContent(state: AboutScreenState?, actions: AboutActions, updateCheckAva
|
||||
verticalArrangement = Arrangement.spacedBy(KvittTheme.spacing.detailGap),
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth().padding(vertical = KvittTheme.spacing.s4), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(KvittTheme.spacing.s2)) {
|
||||
Box(Modifier.size(KvittTheme.spacing.s12 * 2).background(MaterialTheme.colorScheme.primary, KvittTheme.shapes.avatarLarge).clearAndSetSemantics { }, contentAlignment = Alignment.Center) {
|
||||
KvittIconImage(KvittIcon.ReceiptLong, null, filled = true, size = KvittTheme.spacing.s12, tint = MaterialTheme.colorScheme.onPrimary)
|
||||
Box(Modifier.size(KvittDimens.aboutAppMark).background(MaterialTheme.colorScheme.primary, KvittTheme.shapes.avatarLarge).clearAndSetSemantics { }, contentAlignment = Alignment.Center) {
|
||||
KvittIconImage(KvittIcon.ReceiptLong, null, filled = true, size = KvittDimens.aboutAppMarkIcon, tint = MaterialTheme.colorScheme.onPrimary)
|
||||
}
|
||||
Text(stringResource(R.string.settings_about_version, about.versionName, about.versionCode), style = MaterialTheme.typography.headlineMedium, textAlign = TextAlign.Center)
|
||||
Text(stringResource(DesignR.string.common_tagline), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
|
||||
+3
-3
@@ -246,8 +246,8 @@ 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.of(form.viewerBp.toLong()).display(language)
|
||||
val partnerText = if (viewerTyped) AmountInput.of((10_000 - form.viewerBp).toLong()).display(language) else form.ratioInput.display(language)
|
||||
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 focused = when (form.focus) {
|
||||
EditorFocus.RatioViewer -> RatioField.Viewer
|
||||
EditorFocus.RatioPartner -> RatioField.Partner
|
||||
@@ -370,7 +370,7 @@ private fun EditorDialogs(state: RecurringEditorState, actions: RecurringEditorA
|
||||
EditorDialog.Discard -> ConfirmDialog(
|
||||
icon = KvittIcon.Close,
|
||||
title = stringResource(DesignR.string.common_discard_title),
|
||||
body = "",
|
||||
body = null,
|
||||
confirm = stringResource(DesignR.string.common_discard),
|
||||
dismiss = stringResource(DesignR.string.common_keep_editing),
|
||||
onConfirm = actions.onDiscard,
|
||||
|
||||
+2
-1
@@ -14,6 +14,7 @@ import kotlinx.coroutines.launch
|
||||
import se.hagstedt.kvitt.design.component.AccountChoice
|
||||
import se.hagstedt.kvitt.design.component.RatioField
|
||||
import se.hagstedt.kvitt.design.component.quickRowCategories
|
||||
import se.hagstedt.kvitt.design.component.viewerPercentOf
|
||||
import se.hagstedt.kvitt.design.format.AmountInput
|
||||
import se.hagstedt.kvitt.design.format.AmountKey
|
||||
import se.hagstedt.kvitt.feature.settings.common.UiMessage
|
||||
@@ -64,7 +65,7 @@ data class RecurringEditorState(
|
||||
val canSave: Boolean get() = online && !busy && changed && valid && !errors.any
|
||||
|
||||
/** The account's default ratio for "Kontots standard 60/40", viewer first. */
|
||||
val viewerDefaultPercent: Int get() = (account?.defaultShares?.get(options.me.id) ?: BP_TOTAL / 2) / 100
|
||||
val viewerDefaultPercent: Int get() = viewerPercentOf(account?.defaultShares.orEmpty(), options.me.id)
|
||||
val dayClampHint: Boolean get() = (form.dayOfMonth ?: 0) >= 29
|
||||
val resumeHint: Boolean get() = original?.paused == true && !form.paused
|
||||
}
|
||||
|
||||
+1
-1
@@ -139,7 +139,7 @@ data class RecurringForm(
|
||||
categoryPicked = true,
|
||||
payer = d.payer,
|
||||
mode = d.mode,
|
||||
ratioInput = AmountInput.of((viewerBp ?: BP_TOTAL / 2).toLong()),
|
||||
ratioInput = AmountInput.percent((viewerBp ?: BP_TOTAL / 2).toLong()),
|
||||
exactTyped = typed,
|
||||
exactInput = d.exactOre?.get(typed)?.let { AmountInput.of(it) } ?: AmountInput(),
|
||||
fullOn = d.fullOn ?: me,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Strings of :feature:settings (M7f), keys from docs/ux/strings.csv (module=settings). Keys marked PROPOSED are
|
||||
not in strings.csv yet; the M7f report proposes them. Generated; change the CSV first. -->
|
||||
<!-- Strings of :feature:settings (M7f), keys from docs/ux/strings.csv (module=settings). Generated; change the CSV
|
||||
first. -->
|
||||
<resources>
|
||||
<string name="settings_profile_offline_banner">Kräver anslutning. Profilen kan bara ändras när telefonen är online.</string>
|
||||
<string name="settings_title">Inställningar</string>
|
||||
@@ -179,16 +179,10 @@
|
||||
<string name="settings_about_server_version">Serverversion</string>
|
||||
<string name="settings_about_server_version_unknown">Okänd</string>
|
||||
<string name="settings_about_licences">Licenser för öppen källkod</string>
|
||||
<!-- PROPOSED (M7f report): S-15 snackbar after Arkivera; %1$s category name -->
|
||||
<string name="settings_category_archived_snackbar">%1$s arkiverades</string>
|
||||
<!-- PROPOSED (M7f report): S-15 snackbar after Återställ från arkiv -->
|
||||
<string name="settings_category_unarchived_snackbar">%1$s återställdes</string>
|
||||
<!-- PROPOSED (M7f report): S-13 recurring row subtitle without templates (design-spec S-13) -->
|
||||
<string name="settings_recurring_summary_none">Inga</string>
|
||||
<!-- PROPOSED (M7f report): TalkBack state of the archived section (design-spec §3.11 CollapsibleRow) -->
|
||||
<string name="settings_expanded">utfälld</string>
|
||||
<!-- PROPOSED (M7f report): TalkBack state of the archived section -->
|
||||
<string name="settings_collapsed">hopfälld</string>
|
||||
<!-- PROPOSED (M7f report): S-13 partner row before the partner joins -->
|
||||
<string name="settings_partner_missing">Partner saknas</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Strings of :feature:settings (M7f), keys from docs/ux/strings.csv (module=settings). Keys marked PROPOSED are
|
||||
not in strings.csv yet; the M7f report proposes them. Generated; change the CSV first. -->
|
||||
<!-- Strings of :feature:settings (M7f), keys from docs/ux/strings.csv (module=settings). Generated; change the CSV
|
||||
first. -->
|
||||
<resources>
|
||||
<string name="settings_profile_offline_banner">Needs connection. Your profile can only be changed online.</string>
|
||||
<string name="settings_title">Settings</string>
|
||||
@@ -179,16 +179,10 @@
|
||||
<string name="settings_about_server_version">Server version</string>
|
||||
<string name="settings_about_server_version_unknown">Unknown</string>
|
||||
<string name="settings_about_licences">Open-source licences</string>
|
||||
<!-- PROPOSED (M7f report): S-15 snackbar after Arkivera; %1$s category name -->
|
||||
<string name="settings_category_archived_snackbar">%1$s was archived</string>
|
||||
<!-- PROPOSED (M7f report): S-15 snackbar after Återställ från arkiv -->
|
||||
<string name="settings_category_unarchived_snackbar">%1$s was restored</string>
|
||||
<!-- PROPOSED (M7f report): S-13 recurring row subtitle without templates (design-spec S-13) -->
|
||||
<string name="settings_recurring_summary_none">None</string>
|
||||
<!-- PROPOSED (M7f report): TalkBack state of the archived section (design-spec §3.11 CollapsibleRow) -->
|
||||
<string name="settings_expanded">expanded</string>
|
||||
<!-- PROPOSED (M7f report): TalkBack state of the archived section -->
|
||||
<string name="settings_collapsed">collapsed</string>
|
||||
<!-- PROPOSED (M7f report): S-13 partner row before the partner joins -->
|
||||
<string name="settings_partner_missing">Partner has not joined</string>
|
||||
</resources>
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ class RecurringScreenshotTest(private val state: String) {
|
||||
editor("edit_exact_over", SettingsScenarios.loaded, SettingsScenarios.netflix) { _, vm ->
|
||||
vm.toggleSplit()
|
||||
vm.focus(EditorFocus.Exact)
|
||||
repeat(4) { vm.onKey(AmountKey.Delete) }
|
||||
vm.onKey(AmountKey.Clear)
|
||||
vm.type("200")
|
||||
},
|
||||
editor("edit_full", SettingsScenarios.loaded, SettingsScenarios.spotify) { _, vm -> vm.toggleSplit() },
|
||||
|
||||
+2
-2
@@ -125,7 +125,7 @@ fun SettleContent(state: SettleUiState, actions: SettleActions, modifier: Modifi
|
||||
ConfirmDialogContent(
|
||||
icon = KvittIcon.Delete,
|
||||
title = stringResource(DesignR.string.common_discard_title),
|
||||
body = "",
|
||||
body = null,
|
||||
confirm = stringResource(DesignR.string.common_discard),
|
||||
dismiss = stringResource(DesignR.string.common_keep_editing),
|
||||
onConfirm = actions::onDiscard,
|
||||
@@ -457,7 +457,7 @@ private fun SettleDatePicker(isoDate: String, actions: SettleActions) {
|
||||
DatePickerDialog(
|
||||
onDismissRequest = actions::onDismissDialog,
|
||||
confirmButton = {
|
||||
KvittButton(resources.getString(DesignR.string.common_save), {
|
||||
KvittButton(resources.getString(DesignR.string.common_ok), {
|
||||
picker.selectedDateMillis?.let { actions.onDate(LocalDate.fromEpochDays(it.floorDiv(MILLIS_PER_DAY)).toString()) } ?: actions.onDismissDialog()
|
||||
}, kind = ButtonKind.Text)
|
||||
},
|
||||
|
||||
+3
-2
@@ -119,6 +119,7 @@ fun SettlementDetailContent(
|
||||
detail == null -> EmptyState(
|
||||
title = stringResource(R.string.settle_detail_missing),
|
||||
body = stringResource(R.string.settle_detail_missing_body),
|
||||
icon = KvittIcon.SyncProblem,
|
||||
action = stringResource(R.string.settle_detail_sync_now),
|
||||
actionIcon = KvittIcon.Sync,
|
||||
actionKind = ButtonKind.Tonal,
|
||||
@@ -210,13 +211,13 @@ private fun Hero(detail: SettlementDetailState, modifier: Modifier) {
|
||||
KvittCard(
|
||||
modifier.fillMaxWidth().clearAndSetSemantics { contentDescription = spoken },
|
||||
color = KvittTheme.colors.settlementHero,
|
||||
contentPadding = PaddingValues(horizontal = KvittTheme.spacing.s5, vertical = KvittTheme.spacing.s5),
|
||||
contentPadding = PaddingValues(horizontal = KvittDimens.settlementHeroHorizontal, vertical = KvittDimens.settlementHeroVertical),
|
||||
verticalArrangement = Arrangement.spacedBy(KvittTheme.spacing.s2),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(KvittTheme.spacing.s3)) {
|
||||
PersonAvatar(detail.from, KvittDimens.avatarSettings, shape = AvatarShape.Large)
|
||||
KvittIconImage(KvittIcon.ArrowForward, null, size = KvittDimens.accountHeaderSettledIcon, tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
KvittIconImage(KvittIcon.ArrowForward, null, size = KvittDimens.settlementArrow, tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
PersonAvatar(detail.to, KvittDimens.avatarSettings, shape = AvatarShape.Large)
|
||||
}
|
||||
Text(text, style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- :feature:settle strings (S-9, S-10). Source: docs/ux/strings.csv, keys with module=settle; the last three keys are
|
||||
proposed in the M7d report and not yet in strings.csv. -->
|
||||
<!-- :feature:settle strings (S-9, S-10). Source: docs/ux/strings.csv, keys with module=settle. -->
|
||||
<resources>
|
||||
<string name="settle_title">Gör upp</string>
|
||||
<string name="settle_title_edit">Ändra betalning</string>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- :feature:settle strings (S-9, S-10). Source: docs/ux/strings.csv, keys with module=settle; the last three keys are
|
||||
proposed in the M7d report and not yet in strings.csv. -->
|
||||
<!-- :feature:settle strings (S-9, S-10). Source: docs/ux/strings.csv, keys with module=settle. -->
|
||||
<resources>
|
||||
<string name="settle_title">Settle up</string>
|
||||
<string name="settle_title_edit">Edit payment</string>
|
||||
|
||||
Reference in New Issue
Block a user