feat(contacts): mirror contact special dates into local calendars (#15) #57
@@ -611,6 +611,18 @@ sealed interface CalendarReminderOverride {
|
|||||||
data class Minutes(val minutes: List<Int>) : CalendarReminderOverride
|
data class Minutes(val minutes: List<Int>) : 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<Long, List<Int>>.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
|
* 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
|
* if [calendarId] has one for this event kind, otherwise the global default for
|
||||||
|
|||||||
@@ -86,8 +86,10 @@ import androidx.lifecycle.LifecycleEventObserver
|
|||||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import de.jeanlucmakiola.calendula.R
|
import de.jeanlucmakiola.calendula.R
|
||||||
|
import de.jeanlucmakiola.calendula.data.contacts.hasContactsPermission
|
||||||
import de.jeanlucmakiola.calendula.data.crash.CrashReporter
|
import de.jeanlucmakiola.calendula.data.crash.CrashReporter
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.CalendarReminderOverride
|
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.PastEventDisplay
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.ThemeMode
|
import de.jeanlucmakiola.calendula.data.prefs.ThemeMode
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.TimeFormatPref
|
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.agendaRangeLabel
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CollapsingScaffold
|
import de.jeanlucmakiola.calendula.ui.common.CollapsingScaffold
|
||||||
import de.jeanlucmakiola.calendula.ui.common.GroupedRow
|
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.IMPLEMENTED_VIEWS
|
||||||
import de.jeanlucmakiola.calendula.ui.common.labelRes
|
import de.jeanlucmakiola.calendula.ui.common.labelRes
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip
|
import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip
|
||||||
@@ -1090,7 +1093,7 @@ private fun SpecialDatesScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
val requestOrEnable: () -> Unit = {
|
val requestOrEnable: () -> Unit = {
|
||||||
if (hasContactsPermission(context)) {
|
if (context.hasContactsPermission()) {
|
||||||
viewModel.setSpecialDatesEnabled(true)
|
viewModel.setSpecialDatesEnabled(true)
|
||||||
} else {
|
} else {
|
||||||
permissionLauncher.launch(Manifest.permission.READ_CONTACTS)
|
permissionLauncher.launch(Manifest.permission.READ_CONTACTS)
|
||||||
@@ -1290,12 +1293,21 @@ private fun SpecialDatesTemplateDialog(
|
|||||||
title = { Text(stringResource(R.string.settings_special_dates_template)) },
|
title = { Text(stringResource(R.string.settings_special_dates_template)) },
|
||||||
text = {
|
text = {
|
||||||
Column {
|
Column {
|
||||||
androidx.compose.material3.OutlinedTextField(
|
// 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,
|
value = text,
|
||||||
onValueChange = { text = it },
|
onValueChange = { text = it },
|
||||||
singleLine = true,
|
placeholder = stringResource(R.string.settings_special_dates_template),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.settings_special_dates_template_hint),
|
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
|
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
|
@Composable
|
||||||
private fun specialDatesLastRunLabel(context: Context, lastRun: Long): String =
|
private fun specialDatesLastRunLabel(context: Context, lastRun: Long): String =
|
||||||
if (lastRun <= 0L) {
|
if (lastRun <= 0L) {
|
||||||
@@ -1426,22 +1434,11 @@ private fun formatTimeOfDay(context: Context, minutesOfDay: Int): String {
|
|||||||
return DateFormat.getTimeFormat(context).format(time)
|
return DateFormat.getTimeFormat(context).format(time)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The stored override for [calendarId], as a picker choice (absent → inherit). */
|
|
||||||
private fun Map<Long, List<Int>>.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. */
|
/** Label for a global-default choice: empty → "None", else the lead times joined. */
|
||||||
@Composable
|
@Composable
|
||||||
private fun reminderChoiceLabel(minutes: List<Int>): String {
|
private fun reminderChoiceLabel(minutes: List<Int>): String {
|
||||||
if (minutes.isEmpty()) return stringResource(R.string.reminder_none)
|
if (minutes.isEmpty()) return stringResource(R.string.reminder_none)
|
||||||
// reminderLeadTimeLabel is @Composable, so resolve each part in a loop rather
|
return minutes.map { reminderLeadTimeLabel(it) }.joinToString(", ")
|
||||||
// than a (non-composable) joinToString transform.
|
|
||||||
val parts = ArrayList<String>(minutes.size)
|
|
||||||
for (m in minutes) parts.add(reminderLeadTimeLabel(m))
|
|
||||||
return parts.joinToString(", ")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Row summary for a calendar: its override, or the inherited global default. */
|
/** Row summary for a calendar: its override, or the inherited global default. */
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ import de.jeanlucmakiola.calendula.data.calendar.CalendarRepository
|
|||||||
import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesCalendarSpec
|
import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesCalendarSpec
|
||||||
import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesScheduler
|
import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesScheduler
|
||||||
import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesSyncEngine
|
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.di.IoDispatcher
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.CalendarReminderOverride
|
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.PastEventDisplay
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
|
import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.SpecialDatesStalledReason
|
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
|
// A blank stored template resolves to the localized default so the
|
||||||
// editor always shows something sensible.
|
// editor always shows something sensible.
|
||||||
titleTemplates = SpecialDateType.entries.associateWith { type ->
|
titleTemplates = SpecialDateType.entries.associateWith { type ->
|
||||||
templates[type]?.takeIf { it.isNotBlank() }
|
specialDatesSpec.resolveTitleTemplate(type, templates)
|
||||||
?: specialDatesSpec.defaultTitleTemplate(type)
|
|
||||||
},
|
},
|
||||||
showYear = showYear,
|
showYear = showYear,
|
||||||
stalledPermission = status.stalled == SpecialDatesStalledReason.PermissionRevoked,
|
stalledPermission = status.stalled == SpecialDatesStalledReason.PermissionRevoked,
|
||||||
@@ -207,13 +208,9 @@ class SettingsViewModel @Inject constructor(
|
|||||||
prefs.perCalendarAllDayReminderOverride,
|
prefs.perCalendarAllDayReminderOverride,
|
||||||
) { base, calendars, allDayOverrides ->
|
) { base, calendars, allDayOverrides ->
|
||||||
base.copy(
|
base.copy(
|
||||||
reminderChoices = calendars.mapValues { (_, calendarId) ->
|
|
||||||
// An override is always seeded on creation; present-empty = None.
|
// An override is always seeded on creation; present-empty = None.
|
||||||
when (val minutes = allDayOverrides[calendarId]) {
|
reminderChoices = calendars.mapValues { (_, calendarId) ->
|
||||||
null -> CalendarReminderOverride.Inherit
|
allDayOverrides.choiceFor(calendarId)
|
||||||
emptyList<Int>() -> CalendarReminderOverride.None
|
|
||||||
else -> CalendarReminderOverride.Minutes(minutes)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
|
|||||||
Reference in New Issue
Block a user