Use floret's info card, onboarding headline and language row

This commit is contained in:
2026-10-15 17:35:30 +02:00
parent 4501f15999
commit cce831a2ad
10 changed files with 106 additions and 415 deletions
@@ -8,7 +8,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
@@ -17,7 +16,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -58,7 +56,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
@@ -92,6 +89,7 @@ import de.jeanlucmakiola.calendula.domain.RichText
import de.jeanlucmakiola.calendula.domain.TimeZoneOption
import de.jeanlucmakiola.calendula.domain.timeZoneOptionOf
import de.jeanlucmakiola.calendula.domain.zoneDescriptor
import de.jeanlucmakiola.floret.components.IconInfoCard
import de.jeanlucmakiola.floret.identity.LocalFloretDarkTheme
import de.jeanlucmakiola.floret.identity.predictiveBack
import de.jeanlucmakiola.calendula.ui.common.CalendarFailure
@@ -384,7 +382,7 @@ private fun EventDetailContent(
// "When" — date/all-day plus the time range.
val (whenPrimary, whenSecondary) = rememberWhen(instance, TimeZone.currentSystemDefault(), locale)
DetailCard(icon = Icons.Default.Schedule, iconContentDescription = null) {
IconInfoCard(icon = Icons.Default.Schedule, iconContentDescription = null) {
Text(text = whenPrimary, style = MaterialTheme.typography.titleMedium)
if (whenSecondary != null) {
Spacer(Modifier.height(2.dp))
@@ -409,7 +407,7 @@ private fun EventDetailContent(
}
foreignZone?.let { zoneOption ->
Spacer(Modifier.height(gap))
DetailCard(
IconInfoCard(
icon = Icons.Default.Public,
iconContentDescription = stringResource(R.string.event_detail_timezone),
) {
@@ -441,7 +439,7 @@ private fun EventDetailContent(
// Calendar — icon tinted in the calendar colour conveys identity, so no
// separate colour dot is needed.
Spacer(Modifier.height(gap))
DetailCard(
IconInfoCard(
icon = Icons.Default.CalendarMonth,
iconTint = accent,
iconContentDescription = stringResource(R.string.event_detail_calendar),
@@ -459,7 +457,7 @@ private fun EventDetailContent(
val context = LocalContext.current
val locationLabel = stringResource(R.string.event_detail_location)
Spacer(Modifier.height(gap))
DetailCard(
IconInfoCard(
icon = Icons.Default.Place,
iconContentDescription = locationLabel,
) {
@@ -486,7 +484,7 @@ private fun EventDetailContent(
state.description?.let { description ->
val descriptionLabel = stringResource(R.string.event_detail_description)
Spacer(Modifier.height(gap))
DetailCard(
IconInfoCard(
icon = Icons.AutoMirrored.Filled.Notes,
iconContentDescription = descriptionLabel,
// The gesture sits on the card so the icon and padding answer
@@ -505,7 +503,7 @@ private fun EventDetailContent(
// each attendee with their role and reply.
detail.attendees.takeIf { it.isNotEmpty() }?.let { attendees ->
Spacer(Modifier.height(gap))
DetailCard(
IconInfoCard(
icon = Icons.Default.People,
iconContentDescription = stringResource(R.string.event_detail_attendees),
) {
@@ -527,7 +525,7 @@ private fun EventDetailContent(
// Reminders (conditional) — list each lead time before the event.
detail.reminders.takeIf { it.isNotEmpty() }?.let { reminders ->
Spacer(Modifier.height(gap))
DetailCard(
IconInfoCard(
icon = Icons.Default.Notifications,
iconContentDescription = stringResource(R.string.event_detail_reminders),
) {
@@ -547,7 +545,7 @@ private fun EventDetailContent(
// Recurrence (conditional) — humanised from the RRULE.
detail.rrule?.takeIf { it.isNotBlank() }?.let { rrule ->
Spacer(Modifier.height(gap))
DetailCard(
IconInfoCard(
icon = Icons.Default.Repeat,
iconContentDescription = stringResource(R.string.event_detail_recurrence),
) {
@@ -560,36 +558,6 @@ private fun EventDetailContent(
}
}
/** One info card: tonal container, leading icon in the gutter, value to the right. */
@Composable
private fun DetailCard(
icon: ImageVector,
iconContentDescription: String?,
modifier: Modifier = Modifier,
iconTint: Color = MaterialTheme.colorScheme.onSurfaceVariant,
content: @Composable ColumnScope.() -> Unit,
) {
Surface(
color = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = RoundedCornerShape(16.dp),
modifier = modifier.fillMaxWidth(),
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = icon,
contentDescription = iconContentDescription,
tint = iconTint,
modifier = Modifier.size(24.dp),
)
Spacer(Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f), content = content)
}
}
}
@Composable
private fun AttendeeRow(attendee: Attendee) {
Row(
@@ -33,6 +33,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Notes
import androidx.compose.material.icons.filled.Add
import de.jeanlucmakiola.floret.components.IconInfoCard
import de.jeanlucmakiola.floret.identity.LocalFloretDarkTheme
import kotlinx.coroutines.launch
import de.jeanlucmakiola.calendula.ui.common.sendEventShare
@@ -843,7 +844,7 @@ private fun EventEditContent(
// "When" card: the icon centres on the all-day row (header); the
// start/end rows continue below in the same text column.
EditCard(
IconInfoCard(
icon = Icons.Default.Schedule,
iconContentDescription = null,
header = {
@@ -929,7 +930,7 @@ private fun EventEditContent(
// different one while editing *moves* the event (copy+delete on save,
// since CALENDAR_ID can't be updated in place). Managed special-dates
// events stay locked: the contact sync owns their calendar.
EditCard(
IconInfoCard(
icon = Icons.Default.CalendarMonth,
iconContentDescription = stringResource(R.string.event_detail_calendar),
iconTint = accent,
@@ -960,7 +961,7 @@ private fun EventEditContent(
// rest unfold behind the "more fields" button below.
OptionalFormSection(visible = EventFormField.Timezone in state.visibleFields) {
Spacer(Modifier.height(gap))
EditCard(
IconInfoCard(
icon = Icons.Default.Public,
iconContentDescription = null,
onClick = { showTimezonePicker = true },
@@ -1008,10 +1009,11 @@ private fun EventEditContent(
OptionalFormSection(visible = EventFormField.Location in state.visibleFields) {
Spacer(Modifier.height(gap))
EditCard(
IconInfoCard(
icon = Icons.Default.Place,
iconContentDescription = stringResource(R.string.event_detail_location),
iconAtTop = true,
iconAlignment = Alignment.Top,
iconTopPadding = ((firstLineHeight() - 24.dp) / 2).coerceAtLeast(0.dp),
) {
Row(verticalAlignment = Alignment.Top) {
Column(modifier = Modifier.weight(1f)) {
@@ -1077,10 +1079,11 @@ private fun EventEditContent(
OptionalFormSection(visible = EventFormField.Description in state.visibleFields) {
Spacer(Modifier.height(gap))
EditCard(
IconInfoCard(
icon = Icons.AutoMirrored.Filled.Notes,
iconContentDescription = stringResource(R.string.event_detail_description),
iconAtTop = true,
iconAlignment = Alignment.Top,
iconTopPadding = ((firstLineHeight() - 24.dp) / 2).coerceAtLeast(0.dp),
) {
InlineField(
value = form.description,
@@ -1116,7 +1119,7 @@ private fun EventEditContent(
OptionalFormSection(visible = EventFormField.Recurrence in state.visibleFields) {
Spacer(Modifier.height(gap))
EditCard(
IconInfoCard(
icon = Icons.Default.Repeat,
iconContentDescription = null,
onClick = if (locked) null else ({ showRecurrencePicker = true }),
@@ -1156,7 +1159,7 @@ private fun EventEditContent(
OptionalFormSection(visible = EventFormField.Availability in state.visibleFields) {
Spacer(Modifier.height(gap))
EditCard(
IconInfoCard(
icon = Icons.Default.EventAvailable,
iconContentDescription = null,
header = {
@@ -1184,7 +1187,7 @@ private fun EventEditContent(
OptionalFormSection(visible = EventFormField.Visibility in state.visibleFields) {
Spacer(Modifier.height(gap))
EditCard(
IconInfoCard(
icon = Icons.Default.Lock,
iconContentDescription = null,
onClick = { showVisibilityPicker = true },
@@ -1218,7 +1221,7 @@ private fun EventEditContent(
// The swatch the event will paint with: its own colour, else the
// calendar's. The Palette icon takes that colour as a preview.
val swatch = form.color ?: selectedCalendar?.color
EditCard(
IconInfoCard(
icon = Icons.Default.Palette,
iconContentDescription = stringResource(R.string.event_edit_color),
iconTint = if (colorSupported && swatch != null) {
@@ -2344,85 +2347,6 @@ private fun accessLevelSummary(level: AccessLevel): Int = when (level) {
@Composable
private fun reminderLabel(minutes: Int): String = reminderLeadTimeLabel(minutes)
/**
* One info card mirroring the detail screen's DetailCard: tonal container,
* leading icon in the gutter, value to the right. Optionally clickable as a
* whole (calendar picker).
*/
@Composable
private fun EditCard(
icon: ImageVector,
iconContentDescription: String?,
iconTint: Color = MaterialTheme.colorScheme.onSurfaceVariant,
onClick: (() -> Unit)? = null,
// Multiline-input cards align the icon with the field's first text line.
iconAtTop: Boolean = false,
// Tall cards pass their first row here: the icon centres on it, and
// [content] continues below, indented to the same text column.
header: (@Composable ColumnScope.() -> Unit)? = null,
content: @Composable ColumnScope.() -> Unit,
) {
val shape = RoundedCornerShape(16.dp)
val color = MaterialTheme.colorScheme.surfaceContainerHigh
val inner: @Composable () -> Unit = {
if (header != null) {
Column(modifier = Modifier.padding(16.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = icon,
contentDescription = iconContentDescription,
tint = iconTint,
modifier = Modifier.size(24.dp),
)
Spacer(Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f), content = header)
}
Column(
modifier = Modifier.padding(start = 40.dp),
content = content,
)
}
} else {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = if (iconAtTop) Alignment.Top else Alignment.CenterVertically,
) {
// A top-aligned icon centres on the first text line, whatever
// that line measures at the user's font scale.
val iconOffset = if (iconAtTop) {
((firstLineHeight() - 24.dp) / 2).coerceAtLeast(0.dp)
} else {
0.dp
}
Icon(
imageVector = icon,
contentDescription = iconContentDescription,
tint = iconTint,
modifier = Modifier
.padding(top = iconOffset)
.size(24.dp),
)
Spacer(Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f), content = content)
}
}
}
if (onClick != null) {
Surface(
onClick = onClick,
color = color,
shape = shape,
modifier = Modifier.fillMaxWidth(),
) { inner() }
} else {
Surface(
color = color,
shape = shape,
modifier = Modifier.fillMaxWidth(),
) { inner() }
}
}
/**
* "2:00 PM – 3:00 PM" in [zone], honouring the user's 12/24-hour setting. Collapses
* to one time for an instant event, which shouldn't read as a range against
@@ -10,17 +10,14 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CloudOff
import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material.icons.filled.Schedule
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import de.jeanlucmakiola.calendula.R
import de.jeanlucmakiola.floret.components.BenefitRow
import de.jeanlucmakiola.floret.components.OnboardingPrimaryButton
import de.jeanlucmakiola.floret.components.OnboardingScaffold
import de.jeanlucmakiola.floret.components.OnboardingSpace
@@ -53,15 +50,10 @@ internal fun BackupStep(
topSpacing = OnboardingSpace.lg,
hero = { IconHero(Icons.Filled.CloudOff) },
actions = {
Button(
OnboardingPrimaryButton(
text = stringResource(R.string.onboarding_backup_enable_button),
onClick = { runCatching { pickFolder.launch(null) } },
modifier = Modifier.fillMaxWidth().height(56.dp),
) {
Text(
text = stringResource(R.string.onboarding_backup_enable_button),
style = MaterialTheme.typography.titleMedium,
)
}
)
TextButton(
onClick = onSkip,
modifier = Modifier.fillMaxWidth(),
@@ -69,20 +61,9 @@ internal fun BackupStep(
Text(stringResource(R.string.onboarding_backup_skip_button))
}
},
headline = stringResource(R.string.onboarding_backup_title),
supporting = stringResource(R.string.onboarding_backup_body),
) {
Text(
text = stringResource(R.string.onboarding_backup_title),
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(12.dp))
Text(
text = stringResource(R.string.onboarding_backup_body),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(OnboardingSpace.lg))
BenefitRow(
@@ -9,7 +9,6 @@ import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.CloudSync
import androidx.compose.material.icons.filled.PhoneAndroid
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
@@ -30,6 +29,7 @@ import de.jeanlucmakiola.calendula.data.calendar.CalendarColorPalette
import de.jeanlucmakiola.calendula.ui.calendars.CalendarEditor
import de.jeanlucmakiola.calendula.ui.settings.openUrl
import de.jeanlucmakiola.floret.components.BenefitRow
import de.jeanlucmakiola.floret.components.OnboardingPrimaryButton
import de.jeanlucmakiola.floret.components.OnboardingScaffold
import de.jeanlucmakiola.floret.components.OnboardingSpace
@@ -108,18 +108,13 @@ internal fun CalendarsStep(
topSpacing = OnboardingSpace.lg,
hero = { IconHero(Icons.Filled.CalendarMonth) },
actions = {
Button(
OnboardingPrimaryButton(
text = stringResource(R.string.onboarding_calendars_local_button),
onClick = {
onClearCreation()
creating = true
},
modifier = Modifier.fillMaxWidth().height(56.dp),
) {
Text(
text = stringResource(R.string.onboarding_calendars_local_button),
style = MaterialTheme.typography.titleMedium,
)
}
)
OutlinedButton(
onClick = { openUrl(context, DAVX5_URL) },
modifier = Modifier.fillMaxWidth().height(56.dp),
@@ -133,20 +128,9 @@ internal fun CalendarsStep(
Text(stringResource(R.string.onboarding_calendars_skip_button))
}
},
headline = stringResource(R.string.onboarding_calendars_title),
supporting = stringResource(R.string.onboarding_calendars_body),
) {
Text(
text = stringResource(R.string.onboarding_calendars_title),
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(12.dp))
Text(
text = stringResource(R.string.onboarding_calendars_body),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
if (creation is CalendarCreation.Failed) {
Spacer(Modifier.height(12.dp))
Text(
@@ -189,30 +173,14 @@ private fun CalendarCreated(
topSpacing = OnboardingSpace.lg,
hero = { IconHero(Icons.Filled.CheckCircle) },
actions = {
Button(
OnboardingPrimaryButton(
text = stringResource(R.string.onboarding_calendars_created_continue),
onClick = onContinue,
modifier = Modifier.fillMaxWidth().height(56.dp),
) {
Text(
text = stringResource(R.string.onboarding_calendars_created_continue),
style = MaterialTheme.typography.titleMedium,
)
}
)
},
headline = stringResource(R.string.onboarding_calendars_created_title, name),
supporting = stringResource(R.string.onboarding_calendars_created_body),
) {
Text(
text = stringResource(R.string.onboarding_calendars_created_title, name),
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(12.dp))
Text(
text = stringResource(R.string.onboarding_calendars_created_body),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(OnboardingSpace.lg))
BenefitRow(
@@ -1,17 +1,10 @@
package de.jeanlucmakiola.calendula.ui.onboarding
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import de.jeanlucmakiola.calendula.R
import de.jeanlucmakiola.floret.components.OnboardingPrimaryButton
import de.jeanlucmakiola.floret.components.OnboardingScaffold
import de.jeanlucmakiola.floret.components.OnboardingSpace
@@ -34,28 +27,12 @@ internal fun DoneStep(
topSpacing = OnboardingSpace.lg,
hero = { BrandHero(HeroBadge.Done) },
actions = {
Button(
OnboardingPrimaryButton(
text = stringResource(R.string.onboarding_done_button),
onClick = onFinished,
modifier = Modifier.fillMaxWidth().height(56.dp),
) {
Text(
text = stringResource(R.string.onboarding_done_button),
style = MaterialTheme.typography.titleMedium,
)
}
)
},
) {
Text(
text = stringResource(R.string.onboarding_done_title),
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(12.dp))
Text(
text = stringResource(R.string.onboarding_done_body),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
}
headline = stringResource(R.string.onboarding_done_title),
supporting = stringResource(R.string.onboarding_done_body),
)
}
@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
@@ -25,6 +24,7 @@ import de.jeanlucmakiola.calendula.domain.hasVisibilitySwitch
import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip
import de.jeanlucmakiola.floret.components.GroupedListInset
import de.jeanlucmakiola.floret.components.GroupedRow
import de.jeanlucmakiola.floret.components.OnboardingPrimaryButton
import de.jeanlucmakiola.floret.components.OnboardingScaffold
import de.jeanlucmakiola.floret.components.OnboardingSpace
import de.jeanlucmakiola.floret.components.Position
@@ -65,24 +65,17 @@ internal fun HiddenCalendarsStep(
scrollingActions = true,
hero = { IconHero(Icons.Filled.VisibilityOff) },
actions = {
Button(
OnboardingPrimaryButton(
text = stringResource(
if (anyVisible) {
R.string.onboarding_view_continue_button
} else {
R.string.onboarding_hidden_switch_all_button
},
),
onClick = if (anyVisible) onFinished else onSwitchAllOn,
modifier = Modifier
.padding(horizontal = GroupedListInset)
.fillMaxWidth()
.height(56.dp),
) {
Text(
text = stringResource(
if (anyVisible) {
R.string.onboarding_view_continue_button
} else {
R.string.onboarding_hidden_switch_all_button
},
),
style = MaterialTheme.typography.titleMedium,
)
}
modifier = Modifier.padding(horizontal = GroupedListInset),
)
// Only while nothing is on: once something is, the step has been
// answered and the one button left to press says so.
if (!anyVisible) {
@@ -1,6 +1,7 @@
package de.jeanlucmakiola.calendula.ui.onboarding
import de.jeanlucmakiola.floret.components.BenefitRow
import de.jeanlucmakiola.floret.components.OnboardingPrimaryButton
import de.jeanlucmakiola.floret.components.OnboardingScaffold
import de.jeanlucmakiola.floret.components.OnboardingSpace
@@ -13,16 +14,10 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.CalendarMonth
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -33,8 +28,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import de.jeanlucmakiola.calendula.R
private val CALENDAR_PERMISSIONS = arrayOf(
@@ -93,37 +86,15 @@ private fun RationaleContent(
topSpacing = OnboardingSpace.lg,
hero = { BrandHero() },
actions = {
Button(
OnboardingPrimaryButton(
text = stringResource(R.string.permission_request_button),
onClick = onRequest,
modifier = Modifier.fillMaxWidth().height(56.dp),
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
) {
Text(
text = stringResource(R.string.permission_request_button),
style = MaterialTheme.typography.titleMedium,
)
Spacer(Modifier.width(OnboardingSpace.xs))
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = null,
modifier = Modifier.size(20.dp),
)
}
trailingIcon = Icons.AutoMirrored.Filled.ArrowForward,
)
},
headline = stringResource(R.string.permission_rationale_title),
supporting = stringResource(R.string.permission_rationale_body),
) {
Text(
text = stringResource(R.string.permission_rationale_title),
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(12.dp))
Text(
text = stringResource(R.string.permission_rationale_body),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(OnboardingSpace.lg))
BenefitRow(
@@ -152,7 +123,8 @@ private fun DeniedContent(
progress = progress,
hero = { BrandHero(HeroBadge.Denied) },
actions = {
Button(
OnboardingPrimaryButton(
text = stringResource(R.string.permission_open_settings_button),
onClick = {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
@@ -160,13 +132,7 @@ private fun DeniedContent(
}
context.startActivity(intent)
},
modifier = Modifier.fillMaxWidth().height(56.dp),
) {
Text(
text = stringResource(R.string.permission_open_settings_button),
style = MaterialTheme.typography.titleMedium,
)
}
)
TextButton(
onClick = onRetry,
modifier = Modifier.fillMaxWidth(),
@@ -174,18 +140,7 @@ private fun DeniedContent(
Text(stringResource(R.string.permission_retry_button))
}
},
) {
Text(
text = stringResource(R.string.permission_denied_title),
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(12.dp))
Text(
text = stringResource(R.string.permission_denied_body),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
}
headline = stringResource(R.string.permission_denied_title),
supporting = stringResource(R.string.permission_denied_body),
)
}
@@ -1,6 +1,7 @@
package de.jeanlucmakiola.calendula.ui.onboarding
import de.jeanlucmakiola.floret.components.BenefitRow
import de.jeanlucmakiola.floret.components.OnboardingPrimaryButton
import de.jeanlucmakiola.floret.components.OnboardingScaffold
import de.jeanlucmakiola.floret.components.OnboardingSpace
@@ -14,15 +15,11 @@ import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.NotificationsActive
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import de.jeanlucmakiola.calendula.R
/**
@@ -53,7 +50,8 @@ fun ReminderStep(
topSpacing = OnboardingSpace.lg,
hero = { IconHero(Icons.Filled.NotificationsActive) },
actions = {
Button(
OnboardingPrimaryButton(
text = stringResource(R.string.reminder_onboarding_enable_button),
onClick = {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
@@ -61,13 +59,7 @@ fun ReminderStep(
onFinished(true)
}
},
modifier = Modifier.fillMaxWidth().height(56.dp),
) {
Text(
text = stringResource(R.string.reminder_onboarding_enable_button),
style = MaterialTheme.typography.titleMedium,
)
}
)
TextButton(
onClick = { onFinished(false) },
modifier = Modifier.fillMaxWidth(),
@@ -75,20 +67,9 @@ fun ReminderStep(
Text(stringResource(R.string.reminder_onboarding_skip_button))
}
},
headline = stringResource(R.string.reminder_onboarding_title),
supporting = stringResource(R.string.reminder_onboarding_body),
) {
Text(
text = stringResource(R.string.reminder_onboarding_title),
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(12.dp))
Text(
text = stringResource(R.string.reminder_onboarding_body),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(OnboardingSpace.lg))
BenefitRow(
@@ -1,19 +1,12 @@
package de.jeanlucmakiola.calendula.ui.onboarding
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import de.jeanlucmakiola.calendula.R
import de.jeanlucmakiola.floret.components.OnboardingPrimaryButton
import de.jeanlucmakiola.floret.components.OnboardingScaffold
import de.jeanlucmakiola.floret.components.OnboardingSpace
@@ -38,28 +31,12 @@ internal fun VisibilityStep(
topSpacing = OnboardingSpace.lg,
hero = { IconHero(Icons.Filled.VisibilityOff) },
actions = {
Button(
OnboardingPrimaryButton(
text = stringResource(R.string.onboarding_visibility_button),
onClick = onFinished,
modifier = Modifier.fillMaxWidth().height(56.dp),
) {
Text(
text = stringResource(R.string.onboarding_visibility_button),
style = MaterialTheme.typography.titleMedium,
)
}
)
},
) {
Text(
text = stringResource(R.string.calendars_visibility_notice_title),
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(12.dp))
Text(
text = stringResource(R.string.calendars_visibility_notice_message),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
}
headline = stringResource(R.string.calendars_visibility_notice_title),
supporting = stringResource(R.string.calendars_visibility_notice_message),
)
}
@@ -59,14 +59,13 @@ import de.jeanlucmakiola.floret.components.CollapsingScaffold
import de.jeanlucmakiola.floret.components.GroupedListInset
import de.jeanlucmakiola.floret.components.GroupedRow
import de.jeanlucmakiola.floret.components.GroupedSectionHeader
import de.jeanlucmakiola.floret.components.OptionPicker
import de.jeanlucmakiola.floret.components.LanguagePickerRow
import de.jeanlucmakiola.floret.components.Position
import de.jeanlucmakiola.floret.crash.CrashReportDialog
import de.jeanlucmakiola.floret.crash.CrashReporter
import de.jeanlucmakiola.floret.crash.openIssueTracker
import de.jeanlucmakiola.floret.crash.submitCrashReport
import de.jeanlucmakiola.floret.identity.predictiveBackExit
import de.jeanlucmakiola.floret.locale.AppLanguage
/** The settings sub-screens reached from the hub's category rows. */
private enum class SettingsSection {
@@ -256,7 +255,27 @@ private fun SettingsHub(
leading = { CategoryIcon(Icons.Default.Widgets, ChipAccent.Tertiary) },
onClick = { onOpenSection(SettingsSection.Widgets) },
)
LanguageRow(position = Position.Middle)
LanguagePickerRow(
position = Position.Middle,
title = stringResource(R.string.settings_language),
autoLabel = stringResource(R.string.settings_language_auto),
localesConfig = R.xml.locales_config,
predictiveBack = true,
// Invite contributions right where users pick their language.
header = {
val context = LocalContext.current
val translateUrl = stringResource(R.string.about_translate_url)
GroupedRow(
title = stringResource(R.string.settings_translate),
summary = stringResource(R.string.settings_translate_hint),
position = Position.Alone,
leading = { CategoryIcon(Icons.Default.Translate, ChipAccent.Neutral) },
onClick = { openUrl(context, translateUrl) },
)
Spacer(Modifier.height(16.dp))
},
leading = { CategoryIcon(Icons.Default.Language, ChipAccent.Secondary) },
)
ReportProblemRow(position = Position.Bottom)
// Source, licence and privacy sit at the bottom as reference material
@@ -328,58 +347,6 @@ private fun ReportProblemRow(position: Position) {
}
}
@Composable
private fun LanguageRow(position: Position) {
val context = LocalContext.current
val supported = remember { AppLanguage.supportedTags(context, R.xml.locales_config) }
// Setting a locale recreates the activity; mirror the choice locally so the
// row updates instantly even before the recreation lands.
var current by remember { mutableStateOf(AppLanguage.currentTag(supported)) }
var showDialog by remember { mutableStateOf(false) }
// null = follow the system; the rest are BCP-47 tags from locales_config.xml.
val options = remember(supported) { listOf<String?>(null) + supported }
GroupedRow(
title = stringResource(R.string.settings_language),
summary = languageLabel(current),
position = position,
leading = { CategoryIcon(Icons.Default.Language, ChipAccent.Secondary) },
onClick = { showDialog = true },
)
if (showDialog) {
OptionPicker(
title = stringResource(R.string.settings_language),
predictiveBack = true,
options = options,
selected = current,
label = { languageLabel(it) },
onSelect = {
current = it
AppLanguage.apply(it)
},
onDismiss = { showDialog = false },
// Invite contributions right where users pick their language.
header = {
val translateUrl = stringResource(R.string.about_translate_url)
GroupedRow(
title = stringResource(R.string.settings_translate),
summary = stringResource(R.string.settings_translate_hint),
position = Position.Alone,
leading = { CategoryIcon(Icons.Default.Translate, ChipAccent.Neutral) },
onClick = { openUrl(context, translateUrl) },
)
Spacer(Modifier.height(16.dp))
},
)
}
}
@Composable
private fun languageLabel(tag: String?): String =
if (tag == null) stringResource(R.string.settings_language_auto) else AppLanguage.displayName(tag)
@Composable
private fun AboutCard() {
// Layout lives in floret-kit (components.AboutCard); Calendula supplies