Trim the onboarding steps to one screenful (#163)

The permission step said the same privacy point four times — in the body,
in two of the three benefit rows, and again in the footnote under the
button — which with the new step counter no longer fit without scrolling.
Body keeps the ask, the privacy row absorbs the tracking one, the footnote
goes, and the app-name eyebrow drops (the mark is right above it).

The reminder and backup steps share the shell and would have scrolled the
same way, so they lose their eyebrow and third row too.

Three permission strings and two reminder ones are left unused rather than
deleted: translations still carry them, and a base key a translation has is
a fatal ExtraTranslation. Weblate drops them.
This commit is contained in:
2026-08-08 21:15:31 +02:00
parent d277578e77
commit a35078f96d
4 changed files with 17 additions and 83 deletions

View File

@@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CloudOff import androidx.compose.material.icons.filled.CloudOff
import androidx.compose.material.icons.filled.FolderOpen import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material.icons.filled.Restore
import androidx.compose.material.icons.filled.Schedule import androidx.compose.material.icons.filled.Schedule
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -20,7 +19,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import de.jeanlucmakiola.calendula.R import de.jeanlucmakiola.calendula.R
import de.jeanlucmakiola.floret.components.BenefitRow import de.jeanlucmakiola.floret.components.BenefitRow
import de.jeanlucmakiola.floret.components.OnboardingScaffold import de.jeanlucmakiola.floret.components.OnboardingScaffold
@@ -52,6 +50,7 @@ internal fun BackupStep(
OnboardingScaffold( OnboardingScaffold(
modifier = modifier, modifier = modifier,
progress = progress, progress = progress,
topSpacing = OnboardingSpace.lg,
hero = { IconHero(Icons.Filled.CloudOff) }, hero = { IconHero(Icons.Filled.CloudOff) },
actions = { actions = {
Button( Button(
@@ -71,13 +70,6 @@ internal fun BackupStep(
} }
}, },
) { ) {
Text(
text = stringResource(R.string.app_name).uppercase(),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
letterSpacing = 2.sp,
)
Spacer(Modifier.height(OnboardingSpace.xs))
Text( Text(
text = stringResource(R.string.onboarding_backup_title), text = stringResource(R.string.onboarding_backup_title),
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
@@ -91,7 +83,7 @@ internal fun BackupStep(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
Spacer(Modifier.height(OnboardingSpace.xl)) Spacer(Modifier.height(OnboardingSpace.lg))
BenefitRow( BenefitRow(
icon = Icons.Filled.FolderOpen, icon = Icons.Filled.FolderOpen,
@@ -104,11 +96,5 @@ internal fun BackupStep(
title = stringResource(R.string.onboarding_backup_benefit_daily_title), title = stringResource(R.string.onboarding_backup_benefit_daily_title),
body = stringResource(R.string.onboarding_backup_benefit_daily_body), body = stringResource(R.string.onboarding_backup_benefit_daily_body),
) )
Spacer(Modifier.height(OnboardingSpace.sm))
BenefitRow(
icon = Icons.Filled.Restore,
title = stringResource(R.string.onboarding_backup_benefit_restore_title),
body = stringResource(R.string.onboarding_backup_benefit_restore_body),
)
} }
} }

View File

@@ -10,19 +10,15 @@ import android.net.Uri
import android.provider.Settings import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.CalendarMonth import androidx.compose.material.icons.filled.CalendarMonth
import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -32,13 +28,11 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import de.jeanlucmakiola.calendula.R import de.jeanlucmakiola.calendula.R
@@ -103,6 +97,7 @@ private fun RationaleContent(
OnboardingScaffold( OnboardingScaffold(
modifier = modifier, modifier = modifier,
progress = progress, progress = progress,
topSpacing = OnboardingSpace.lg,
hero = { BrandHero(denied = false) }, hero = { BrandHero(denied = false) },
actions = { actions = {
Button( Button(
@@ -121,16 +116,8 @@ private fun RationaleContent(
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
) )
} }
PrivacyFootnote()
}, },
) { ) {
Text(
text = stringResource(R.string.app_name).uppercase(),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
letterSpacing = 2.sp,
)
Spacer(Modifier.height(OnboardingSpace.xs))
Text( Text(
text = stringResource(R.string.permission_rationale_title), text = stringResource(R.string.permission_rationale_title),
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
@@ -144,14 +131,8 @@ private fun RationaleContent(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
Spacer(Modifier.height(OnboardingSpace.xl)) Spacer(Modifier.height(OnboardingSpace.lg))
BenefitRow(
icon = Icons.Filled.Lock,
title = stringResource(R.string.permission_benefit_private_title),
body = stringResource(R.string.permission_benefit_private_body),
)
Spacer(Modifier.height(OnboardingSpace.sm))
BenefitRow( BenefitRow(
icon = Icons.Filled.CalendarMonth, icon = Icons.Filled.CalendarMonth,
title = stringResource(R.string.permission_benefit_sync_title), title = stringResource(R.string.permission_benefit_sync_title),
@@ -159,9 +140,9 @@ private fun RationaleContent(
) )
Spacer(Modifier.height(OnboardingSpace.sm)) Spacer(Modifier.height(OnboardingSpace.sm))
BenefitRow( BenefitRow(
icon = Icons.Filled.VisibilityOff, icon = Icons.Filled.Lock,
title = stringResource(R.string.permission_benefit_privacy_title), title = stringResource(R.string.permission_benefit_private_title),
body = stringResource(R.string.permission_benefit_privacy_body), body = stringResource(R.string.permission_benefit_private_body),
) )
} }
} }
@@ -215,25 +196,3 @@ private fun DeniedContent(
) )
} }
} }
@Composable
private fun PrivacyFootnote() {
Row(
modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Filled.Lock,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(14.dp),
)
Spacer(Modifier.width(6.dp))
Text(
text = stringResource(R.string.permission_privacy_footnote),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}

View File

@@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.NotificationsActive import androidx.compose.material.icons.filled.NotificationsActive
import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Tune
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -24,7 +23,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import de.jeanlucmakiola.calendula.R import de.jeanlucmakiola.calendula.R
/** /**
@@ -50,6 +48,7 @@ fun ReminderStep(
OnboardingScaffold( OnboardingScaffold(
modifier = modifier, modifier = modifier,
progress = progress, progress = progress,
topSpacing = OnboardingSpace.lg,
hero = { IconHero(Icons.Filled.NotificationsActive) }, hero = { IconHero(Icons.Filled.NotificationsActive) },
actions = { actions = {
Button( Button(
@@ -75,13 +74,6 @@ fun ReminderStep(
} }
}, },
) { ) {
Text(
text = stringResource(R.string.app_name).uppercase(),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
letterSpacing = 2.sp,
)
Spacer(Modifier.height(OnboardingSpace.xs))
Text( Text(
text = stringResource(R.string.reminder_onboarding_title), text = stringResource(R.string.reminder_onboarding_title),
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
@@ -95,7 +87,7 @@ fun ReminderStep(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
Spacer(Modifier.height(OnboardingSpace.xl)) Spacer(Modifier.height(OnboardingSpace.lg))
BenefitRow( BenefitRow(
icon = Icons.Filled.NotificationsActive, icon = Icons.Filled.NotificationsActive,
@@ -108,11 +100,5 @@ fun ReminderStep(
title = stringResource(R.string.reminder_benefit_duplicates_title), title = stringResource(R.string.reminder_benefit_duplicates_title),
body = stringResource(R.string.reminder_benefit_duplicates_body), body = stringResource(R.string.reminder_benefit_duplicates_body),
) )
Spacer(Modifier.height(OnboardingSpace.sm))
BenefitRow(
icon = Icons.Filled.Tune,
title = stringResource(R.string.reminder_benefit_reversible_title),
body = stringResource(R.string.reminder_benefit_reversible_body),
)
} }
} }

View File

@@ -18,16 +18,20 @@
<!-- Permission flow (F1) --> <!-- Permission flow (F1) -->
<string name="permission_rationale_title">See all your events, beautifully</string> <string name="permission_rationale_title">See all your events, beautifully</string>
<string name="permission_rationale_body">Calendula needs access to your calendar to show and manage your events. That\'s all it asks for up front — and nothing ever leaves your device.</string> <string name="permission_rationale_body">Calendula needs access to your calendar to show and manage your events.</string>
<string name="permission_request_button">Grant calendar access</string> <string name="permission_request_button">Grant calendar access</string>
<string name="permission_denied_title">Calendar access denied</string> <string name="permission_denied_title">Calendar access denied</string>
<string name="permission_denied_body">Calendula cannot show events without calendar access. You can grant it again in the system settings.</string> <string name="permission_denied_body">Calendula cannot show events without calendar access. You can grant it again in the system settings.</string>
<string name="permission_open_settings_button">Open system settings</string> <string name="permission_open_settings_button">Open system settings</string>
<string name="permission_retry_button">Try again</string> <string name="permission_retry_button">Try again</string>
<string name="permission_benefit_private_title">Stays on your device</string> <string name="permission_benefit_private_title">Private by design</string>
<string name="permission_benefit_private_body">Your calendars are read locally and never leave the phone.</string> <string name="permission_benefit_private_body">No internet permission, no telemetry — nothing ever leaves your phone.</string>
<string name="permission_benefit_sync_title">All your calendars, together</string> <string name="permission_benefit_sync_title">All your calendars, together</string>
<string name="permission_benefit_sync_body">Google, CalDAV, local — anything synced to the device just appears.</string> <string name="permission_benefit_sync_body">Google, CalDAV, local — anything synced to the device just appears.</string>
<!-- Unused since the wizard trim (#163) — the privacy point is now
permission_benefit_private_*. Kept until Weblate drops them from the
translations: removing a base key a translation still carries is a
fatal ExtraTranslation. -->
<string name="permission_benefit_privacy_title">No tracking, ever</string> <string name="permission_benefit_privacy_title">No tracking, ever</string>
<string name="permission_benefit_privacy_body">Zero telemetry, zero analytics, no ads.</string> <string name="permission_benefit_privacy_body">Zero telemetry, zero analytics, no ads.</string>
<string name="permission_privacy_footnote">Stays on your device · no internet permission</string> <string name="permission_privacy_footnote">Stays on your device · no internet permission</string>
@@ -276,8 +280,6 @@
<string name="onboarding_backup_benefit_folder_body">Backups are plain .ics files — put them somewhere that syncs, or on an SD card.</string> <string name="onboarding_backup_benefit_folder_body">Backups are plain .ics files — put them somewhere that syncs, or on an SD card.</string>
<string name="onboarding_backup_benefit_daily_title">Once a day, by itself</string> <string name="onboarding_backup_benefit_daily_title">Once a day, by itself</string>
<string name="onboarding_backup_benefit_daily_body">Calendula exports your local calendars in the background. Change how often in Settings.</string> <string name="onboarding_backup_benefit_daily_body">Calendula exports your local calendars in the background. Change how often in Settings.</string>
<string name="onboarding_backup_benefit_restore_title">Restore anytime</string>
<string name="onboarding_backup_benefit_restore_body">Open a backup file to bring the events back — on this phone or a new one.</string>
<string name="onboarding_backup_enable_button">Choose folder and back up</string> <string name="onboarding_backup_enable_button">Choose folder and back up</string>
<string name="onboarding_backup_skip_button">Not now</string> <string name="onboarding_backup_skip_button">Not now</string>
<string name="onboarding_view_title">What should Calendula open on?</string> <string name="onboarding_view_title">What should Calendula open on?</string>
@@ -292,6 +294,7 @@
<string name="reminder_benefit_delivery_body">Every reminder on your events arrives as a notification, right on time.</string> <string name="reminder_benefit_delivery_body">Every reminder on your events arrives as a notification, right on time.</string>
<string name="reminder_benefit_duplicates_title">Using a second calendar app?</string> <string name="reminder_benefit_duplicates_title">Using a second calendar app?</string>
<string name="reminder_benefit_duplicates_body">If another app also posts reminders, you\'ll see them twice — turn them off there or here.</string> <string name="reminder_benefit_duplicates_body">If another app also posts reminders, you\'ll see them twice — turn them off there or here.</string>
<!-- Unused since the wizard trim (#163); see the note above. -->
<string name="reminder_benefit_reversible_title">Change it anytime</string> <string name="reminder_benefit_reversible_title">Change it anytime</string>
<string name="reminder_benefit_reversible_body">The switch lives in Settings, under Notifications.</string> <string name="reminder_benefit_reversible_body">The switch lives in Settings, under Notifications.</string>
<string name="reminder_onboarding_enable_button">Turn on reminders</string> <string name="reminder_onboarding_enable_button">Turn on reminders</string>