feat: reminders data layer (Android) — model, settings, repository

CalEvent gains `reminders` (parsed from the server); AppSettings/SettingsStore
gain `defaultReminderMinutes` (null = off) and it's sent via updateSettings;
createLocalEvent/updateLocalEvent/eventBody and CalendarViewModel.saveEvent
thread `reminders` through. UI (editor + settings picker) and the local
notification scheduling follow next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Guido Schmit
2026-06-06 16:31:32 +02:00
parent 807db6a57b
commit 6654012cbb
5 changed files with 24 additions and 9 deletions

View File

@@ -127,6 +127,8 @@ class CalendarRepository @Inject constructor(
"month_divider_color" to s.monthDividerColor,
"month_label_color" to s.monthLabelColor,
"private_event_visibility" to s.privateEventVisibility,
// Explicit JSON null clears it (off); jsonBody drops Kotlin nulls.
"default_reminder_minutes" to (s.defaultReminderMinutes ?: org.json.JSONObject.NULL),
)
).ensureSuccess()
}
@@ -298,18 +300,18 @@ class CalendarRepository @Inject constructor(
suspend fun createLocalEvent(
calendarId: Int, title: String, start: Instant, end: Instant,
isAllDay: Boolean, location: String, description: String, color: String?,
isPrivate: Boolean = false,
isPrivate: Boolean = false, reminders: List<Int>? = null,
) = guarded {
api.createLocalEvent(eventBody(calendarId, title, start, end, isAllDay, location, description, color, isPrivate))
api.createLocalEvent(eventBody(calendarId, title, start, end, isAllDay, location, description, color, isPrivate, reminders))
.ensureSuccess()
}
suspend fun updateLocalEvent(
uid: String, title: String, start: Instant, end: Instant,
isAllDay: Boolean, location: String, description: String, color: String?,
isPrivate: Boolean = false,
isPrivate: Boolean = false, reminders: List<Int>? = null,
) = guarded {
api.updateLocalEvent(uid, eventBody(null, title, start, end, isAllDay, location, description, color, isPrivate))
api.updateLocalEvent(uid, eventBody(null, title, start, end, isAllDay, location, description, color, isPrivate, reminders))
.ensureSuccess()
}
@@ -555,7 +557,7 @@ class CalendarRepository @Inject constructor(
private fun eventBody(
calendarId: Int?, title: String, start: Instant, end: Instant,
isAllDay: Boolean, location: String, description: String, color: String?,
isPrivate: Boolean = false,
isPrivate: Boolean = false, reminders: List<Int>? = null,
) = jsonBody(
buildMap {
calendarId?.let { put("calendar_id", it) }
@@ -567,6 +569,7 @@ class CalendarRepository @Inject constructor(
put("description", description)
if (!color.isNullOrBlank()) put("color", color)
put("private", isPrivate)
if (reminders != null) put("reminders", org.json.JSONArray(reminders))
}
)
}

View File

@@ -34,6 +34,7 @@ class SettingsStore @Inject constructor(
language = prefs.getString(K_LANGUAGE, null) ?: "de",
monthDividerColor = prefs.getString(K_DIVIDER, null) ?: "#7090c0",
monthLabelColor = prefs.getString(K_LABEL, null) ?: "#7090c0",
defaultReminderMinutes = prefs.getInt(K_DEFAULT_REMINDER, -1).takeIf { it >= 0 },
)
fun saveSettings(s: AppSettings) {
@@ -50,6 +51,7 @@ class SettingsStore @Inject constructor(
.putString(K_LANGUAGE, s.language)
.putString(K_DIVIDER, s.monthDividerColor)
.putString(K_LABEL, s.monthLabelColor)
.putInt(K_DEFAULT_REMINDER, s.defaultReminderMinutes ?: -1)
.apply()
}
@@ -83,6 +85,7 @@ class SettingsStore @Inject constructor(
const val K_LANGUAGE = "language"
const val K_DIVIDER = "month_divider_color"
const val K_LABEL = "month_label_color"
const val K_DEFAULT_REMINDER = "default_reminder_minutes"
const val K_CACHE_MONTHS = "cache_months"
const val K_HIDDEN = "hidden_calendar_keys"
const val K_BANISHED = "banished_calendar_keys"

View File

@@ -25,6 +25,8 @@ data class AppSettings(
// How this user's private events appear to other group members: 'hidden' | 'busy'.
@Json(name = "private_event_visibility") val privateEventVisibility: String = "busy",
@Json(name = "group_visible_calendar_id") val groupVisibleCalendarId: Int? = null,
// Minutes-before-start applied to all events client-side; null = off.
@Json(name = "default_reminder_minutes") val defaultReminderMinutes: Int? = null,
) {
val weekStartsOnMonday: Boolean get() = weekStartDay != "sunday"
}

View File

@@ -34,6 +34,8 @@ data class CalEvent(
// Server-decorated title for the group combined view (group icon / owner
// prefix); rendered in group mode while `title` stays raw for editing.
val displayTitle: String? = null,
// Reminder offsets in minutes-before-start (0 = at start). Local events only.
val reminders: List<Int> = emptyList(),
) {
/**
* Group view supplies a server-resolved colour (display_color); otherwise
@@ -121,6 +123,9 @@ data class CalEvent(
isGroupEvent = json.optBoolean("is_group_event", false),
displayColor = json.strOrNull("display_color"),
displayTitle = json.strOrNull("display_title"),
reminders = json.optJSONArray("reminders")?.let { arr ->
(0 until arr.length()).mapNotNull { (arr.opt(it) as? Number)?.toInt() }
} ?: emptyList(),
)
}
}

View File

@@ -401,20 +401,21 @@ class CalendarViewModel @Inject constructor(
description: String,
color: String?,
isPrivate: Boolean,
reminders: List<Int> = emptyList(),
onResult: (String?) -> Unit,
) {
viewModelScope.launch {
val result = runCatching {
if (existing != null && existing.source == calendar.source) {
when (existing.source) {
"local" -> repository.updateLocalEvent(existing.id, title, start, end, isAllDay, location, description, color, isPrivate)
"local" -> repository.updateLocalEvent(existing.id, title, start, end, isAllDay, location, description, color, isPrivate, reminders)
"caldav" -> repository.updateCalDAVEvent(existing.id, existing.url, calendar.numericId, title, start, end, isAllDay, location, description, color)
"homeassistant" -> repository.updateHAEvent(calendar.numericId, existing.id, title, start, end, isAllDay, location, description)
"google" -> repository.updateGoogleEvent(calendar.numericId, existing.id, title, start, end, isAllDay, location, description)
else -> createForSource(calendar, title, start, end, isAllDay, location, description, color, isPrivate)
else -> createForSource(calendar, title, start, end, isAllDay, location, description, color, isPrivate, reminders)
}
} else {
createForSource(calendar, title, start, end, isAllDay, location, description, color, isPrivate)
createForSource(calendar, title, start, end, isAllDay, location, description, color, isPrivate, reminders)
}
}
result.onSuccess { afterMutation(); onResult(null) }
@@ -425,9 +426,10 @@ class CalendarViewModel @Inject constructor(
private suspend fun createForSource(
calendar: WritableCalendar, title: String, start: Instant, end: Instant,
isAllDay: Boolean, location: String, description: String, color: String?, isPrivate: Boolean,
reminders: List<Int> = emptyList(),
) {
when (calendar.source) {
"local" -> repository.createLocalEvent(calendar.numericId, title, start, end, isAllDay, location, description, color, isPrivate)
"local" -> repository.createLocalEvent(calendar.numericId, title, start, end, isAllDay, location, description, color, isPrivate, reminders)
"caldav" -> repository.createCalDAVEvent(calendar.numericId, title, start, end, isAllDay, location, description, color)
"google" -> repository.createGoogleEvent(calendar.numericId, title, start, end, isAllDay, location, description)
"homeassistant" -> repository.createHAEvent(calendar.numericId, title, start, end, isAllDay, location, description)