diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt index 2f7c87c..d2d2def 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt @@ -611,6 +611,18 @@ sealed interface CalendarReminderOverride { data class Minutes(val minutes: List) : CalendarReminderOverride } +/** + * The stored override for [calendarId] in an override map, as a + * [CalendarReminderOverride] (absent → [CalendarReminderOverride.Inherit], + * empty → [CalendarReminderOverride.None]) — the inverse of how + * [SettingsPrefs.setCalendarReminderOverride] stores a choice. + */ +fun Map>.choiceFor(calendarId: Long): CalendarReminderOverride = when { + !containsKey(calendarId) -> CalendarReminderOverride.Inherit + getValue(calendarId).isEmpty() -> CalendarReminderOverride.None + else -> CalendarReminderOverride.Minutes(getValue(calendarId)) +} + /** * The lead times to prefill on a new event: the matching per-calendar override * if [calendarId] has one for this event kind, otherwise the global default for diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt index b0b7fec..2a7d3f8 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt @@ -86,8 +86,10 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.jeanlucmakiola.calendula.R +import de.jeanlucmakiola.calendula.data.contacts.hasContactsPermission import de.jeanlucmakiola.calendula.data.crash.CrashReporter import de.jeanlucmakiola.calendula.data.prefs.CalendarReminderOverride +import de.jeanlucmakiola.calendula.data.prefs.choiceFor import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay import de.jeanlucmakiola.calendula.data.prefs.ThemeMode import de.jeanlucmakiola.calendula.data.prefs.TimeFormatPref @@ -104,6 +106,7 @@ import de.jeanlucmakiola.calendula.ui.common.AgendaRangePicker import de.jeanlucmakiola.calendula.ui.common.agendaRangeLabel import de.jeanlucmakiola.calendula.ui.common.CollapsingScaffold import de.jeanlucmakiola.calendula.ui.common.GroupedRow +import de.jeanlucmakiola.calendula.ui.common.InlineTextField import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS import de.jeanlucmakiola.calendula.ui.common.labelRes import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip @@ -1090,7 +1093,7 @@ private fun SpecialDatesScreen( } } val requestOrEnable: () -> Unit = { - if (hasContactsPermission(context)) { + if (context.hasContactsPermission()) { viewModel.setSpecialDatesEnabled(true) } else { permissionLauncher.launch(Manifest.permission.READ_CONTACTS) @@ -1290,12 +1293,21 @@ private fun SpecialDatesTemplateDialog( title = { Text(stringResource(R.string.settings_special_dates_template)) }, text = { Column { - androidx.compose.material3.OutlinedTextField( - value = text, - onValueChange = { text = it }, - singleLine = true, - modifier = Modifier.fillMaxWidth(), - ) + // The app's borderless input over a tonal surface (the dialog + // convention — see DialogControls), not Material's outlined field. + Surface( + color = MaterialTheme.colorScheme.surfaceContainerHighest, + shape = RoundedCornerShape(12.dp), + ) { + InlineTextField( + value = text, + onValueChange = { text = it }, + placeholder = stringResource(R.string.settings_special_dates_template), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 12.dp), + ) + } Spacer(Modifier.height(8.dp)) Text( text = stringResource(R.string.settings_special_dates_template_hint), @@ -1324,10 +1336,6 @@ private fun specialDateTypeLabel(type: SpecialDateType): Int = when (type) { SpecialDateType.Custom -> R.string.settings_special_dates_type_custom } -private fun hasContactsPermission(context: Context): Boolean = - ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) == - PackageManager.PERMISSION_GRANTED - @Composable private fun specialDatesLastRunLabel(context: Context, lastRun: Long): String = if (lastRun <= 0L) { @@ -1426,22 +1434,11 @@ private fun formatTimeOfDay(context: Context, minutesOfDay: Int): String { return DateFormat.getTimeFormat(context).format(time) } -/** The stored override for [calendarId], as a picker choice (absent → inherit). */ -private fun Map>.choiceFor(calendarId: Long): CalendarReminderOverride = when { - !containsKey(calendarId) -> CalendarReminderOverride.Inherit - this.getValue(calendarId).isEmpty() -> CalendarReminderOverride.None - else -> CalendarReminderOverride.Minutes(this.getValue(calendarId)) -} - /** Label for a global-default choice: empty → "None", else the lead times joined. */ @Composable private fun reminderChoiceLabel(minutes: List): String { if (minutes.isEmpty()) return stringResource(R.string.reminder_none) - // reminderLeadTimeLabel is @Composable, so resolve each part in a loop rather - // than a (non-composable) joinToString transform. - val parts = ArrayList(minutes.size) - for (m in minutes) parts.add(reminderLeadTimeLabel(m)) - return parts.joinToString(", ") + return minutes.map { reminderLeadTimeLabel(it) }.joinToString(", ") } /** Row summary for a calendar: its override, or the inherited global default. */ diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt index 8b75a1a..a29f814 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt @@ -13,8 +13,10 @@ import de.jeanlucmakiola.calendula.data.calendar.CalendarRepository import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesCalendarSpec import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesScheduler import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesSyncEngine +import de.jeanlucmakiola.calendula.data.contacts.resolveTitleTemplate import de.jeanlucmakiola.calendula.data.di.IoDispatcher import de.jeanlucmakiola.calendula.data.prefs.CalendarReminderOverride +import de.jeanlucmakiola.calendula.data.prefs.choiceFor import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs import de.jeanlucmakiola.calendula.data.prefs.SpecialDatesStalledReason @@ -195,8 +197,7 @@ class SettingsViewModel @Inject constructor( // A blank stored template resolves to the localized default so the // editor always shows something sensible. titleTemplates = SpecialDateType.entries.associateWith { type -> - templates[type]?.takeIf { it.isNotBlank() } - ?: specialDatesSpec.defaultTitleTemplate(type) + specialDatesSpec.resolveTitleTemplate(type, templates) }, showYear = showYear, stalledPermission = status.stalled == SpecialDatesStalledReason.PermissionRevoked, @@ -207,13 +208,9 @@ class SettingsViewModel @Inject constructor( prefs.perCalendarAllDayReminderOverride, ) { base, calendars, allDayOverrides -> base.copy( + // An override is always seeded on creation; present-empty = None. reminderChoices = calendars.mapValues { (_, calendarId) -> - // An override is always seeded on creation; present-empty = None. - when (val minutes = allDayOverrides[calendarId]) { - null -> CalendarReminderOverride.Inherit - emptyList() -> CalendarReminderOverride.None - else -> CalendarReminderOverride.Minutes(minutes) - } + allDayOverrides.choiceFor(calendarId) }, ) }.stateIn(