diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/L10n.kt b/app/src/main/java/com/scarriffle/calendarr/ui/L10n.kt index 8341d2b..1a84005 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/L10n.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/L10n.kt @@ -136,6 +136,9 @@ object L10n { "local.title" to "Lokaler Kalender", "local.name" to "Name", "local.color" to "Farbe", "local.create" to "Erstellen", "birthday.new" to "Neuer Geburtstag", "birthday.new_title" to "Neuen Geburtstag hinzufügen", + "birthday.calendar_name" to "Geburtstage", + "birthday.activate" to "Geburtstagskalender aktivieren", + "birthday.activate_hint" to "Aktiviere den Geburtstagskalender, um Geburtstage anzulegen. Er erscheint als eigener Kalender in der Seitenleiste.", "birthday.is_calendar" to "Geburtstagskalender", "birthday.person" to "Name", "birthday.person_ph" to "Name der Person", "birthday.date" to "Geburtstag", "birthday.year_unknown" to "Jahr unbekannt", @@ -292,6 +295,9 @@ object L10n { "local.title" to "Local calendar", "local.name" to "Name", "local.color" to "Color", "local.create" to "Create", "birthday.new" to "New birthday", "birthday.new_title" to "Add new birthday", + "birthday.calendar_name" to "Birthdays", + "birthday.activate" to "Enable birthday calendar", + "birthday.activate_hint" to "Enable the birthday calendar to add birthdays. It appears as its own calendar in the sidebar.", "birthday.is_calendar" to "Birthday calendar", "birthday.person" to "Name", "birthday.person_ph" to "Person's name", "birthday.date" to "Birthday", "birthday.year_unknown" to "Year unknown", diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/accounts/AccountsScreen.kt b/app/src/main/java/com/scarriffle/calendarr/ui/accounts/AccountsScreen.kt index f5f2a3f..66f905a 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/accounts/AccountsScreen.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/accounts/AccountsScreen.kt @@ -233,8 +233,8 @@ fun AccountsScreen( } when (addDialog) { - AddType.LOCAL -> LocalDialog(onDismiss = { addDialog = null }) { name, color, isBirthday, notify -> - vm.addLocal(name, color, onChanged, isBirthday, notify); addDialog = null + AddType.LOCAL -> LocalDialog(onDismiss = { addDialog = null }) { name, color -> + vm.addLocal(name, color, onChanged); addDialog = null } AddType.CALDAV -> CalDAVDialog(onDismiss = { addDialog = null }) { n, u, us, p, c -> vm.addCalDAV(n, u, us, p, c, onChanged); addDialog = null @@ -488,47 +488,14 @@ private fun SharingSheet(vm: AccountsViewModel, calendarId: Int, onDismiss: () - // ---- Add dialogs ---- @Composable -@OptIn(ExperimentalMaterial3Api::class) -private fun LocalDialog(onDismiss: () -> Unit, onConfirm: (String, String, Boolean, Int?) -> Unit) { +private fun LocalDialog(onDismiss: () -> Unit, onConfirm: (String, String) -> Unit) { var name by remember { mutableStateOf("") } - var birthday by remember { mutableStateOf(false) } - var notify by remember { mutableStateOf(-1) } // -1 off, 0 on the day, N days before - var notifyMenu by remember { mutableStateOf(false) } val color = "#34a853" - FormDialog( - tr("accounts.local.add"), onDismiss, - confirmEnabled = name.isNotBlank(), - onConfirm = { onConfirm(name.trim(), color, birthday, if (birthday && notify >= 0) notify else null) }, - ) { + FormDialog(tr("accounts.local.add"), onDismiss, confirmEnabled = name.isNotBlank(), onConfirm = { onConfirm(name.trim(), color) }) { OutlinedTextField(name, { name = it }, label = { Text(tr("local.name")) }, singleLine = true, modifier = Modifier.fillMaxWidth()) - Spacer(Modifier.size(8.dp)) - FilterChip(selected = birthday, onClick = { birthday = !birthday }, label = { Text(tr("birthday.is_calendar")) }) - if (birthday) { - Spacer(Modifier.size(8.dp)) - Box { - FilterChip( - selected = notify >= 0, - onClick = { notifyMenu = true }, - label = { Text("${tr("birthday.notify")}: ${notifyLabel(notify)}") }, - ) - DropdownMenu(expanded = notifyMenu, onDismissRequest = { notifyMenu = false }) { - listOf(-1, 0, 1, 2, 3, 7).forEach { d -> - DropdownMenuItem(text = { Text(notifyLabel(d)) }, onClick = { notify = d; notifyMenu = false }) - } - } - } - } } } -@Composable -private fun notifyLabel(d: Int): String = when { - d < 0 -> tr("birthday.notify.off") - d == 0 -> tr("birthday.notify.same_day") - d == 1 -> tr("birthday.notify.one_day") - else -> tr("birthday.notify.days", d) -} - @Composable private fun CalDAVDialog(onDismiss: () -> Unit, onConfirm: (String, String, String, String, String) -> Unit) { var name by remember { mutableStateOf("") } diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/BirthdayDialog.kt b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/BirthdayDialog.kt index b452937..df89c0d 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/BirthdayDialog.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/BirthdayDialog.kt @@ -1,6 +1,5 @@ package com.scarriffle.calendarr.ui.calendar -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -8,8 +7,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.material3.AlertDialog import androidx.compose.material3.DatePicker import androidx.compose.material3.DatePickerDialog -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip import androidx.compose.material3.MaterialTheme @@ -32,33 +29,31 @@ import java.time.ZoneOffset import java.time.format.DateTimeFormatter /** - * Minimal "new birthday" mask (parity with iOS/web): pick a birthday calendar, - * enter a name and a date (with an optional "year unknown"). Saves an all-day, - * yearly-recurring local event; the server adds the age suffix and cake icon. + * Minimal "new birthday" mask for the user's single birthday calendar: name + + * date (with an optional "year unknown"). Saves an all-day, yearly-recurring + * local event; the server adds the age suffix and cake icon. If no birthday + * calendar exists yet, offers to activate one. */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun BirthdayDialog( - calendars: List, + calendar: LocalCalendar?, onDismiss: () -> Unit, - onSave: (name: String, date: LocalDate, yearKnown: Boolean, calendarId: Int) -> Unit, + onActivate: () -> Unit, + onSave: (name: String, date: LocalDate, yearKnown: Boolean) -> Unit, ) { var name by remember { mutableStateOf("") } var yearUnknown by remember { mutableStateOf(false) } - var selectedCalId by remember { mutableStateOf(calendars.firstOrNull()?.id ?: -1) } var pickedDate by remember { mutableStateOf(LocalDate.now()) } var showPicker by remember { mutableStateOf(false) } - var calMenu by remember { mutableStateOf(false) } - - val hasCalendars = calendars.isNotEmpty() AlertDialog( onDismissRequest = onDismiss, title = { Text(tr("birthday.new_title")) }, text = { Column { - if (!hasCalendars) { - Text(tr("birthday.no_calendars"), color = MaterialTheme.colorScheme.onSurfaceVariant) + if (calendar == null) { + Text(tr("birthday.activate_hint"), color = MaterialTheme.colorScheme.onSurfaceVariant) } else { OutlinedTextField( name, { name = it }, @@ -75,28 +70,18 @@ fun BirthdayDialog( selected = yearUnknown, onClick = { yearUnknown = !yearUnknown }, label = { Text(tr("birthday.year_unknown")) }, ) - if (calendars.size > 1) { - Spacer(Modifier.size(8.dp)) - Box { - FilterChip( - selected = false, onClick = { calMenu = true }, - label = { Text(calendars.firstOrNull { it.id == selectedCalId }?.name ?: tr("birthday.target")) }, - ) - DropdownMenu(expanded = calMenu, onDismissRequest = { calMenu = false }) { - calendars.forEach { c -> - DropdownMenuItem(text = { Text(c.name) }, onClick = { selectedCalId = c.id; calMenu = false }) - } - } - } - } } } }, confirmButton = { - TextButton( - enabled = hasCalendars && name.isNotBlank(), - onClick = { onSave(name.trim(), pickedDate, !yearUnknown, selectedCalId) }, - ) { Text(tr("common.save")) } + if (calendar == null) { + TextButton(onClick = onActivate) { Text(tr("birthday.activate")) } + } else { + TextButton( + enabled = name.isNotBlank(), + onClick = { onSave(name.trim(), pickedDate, !yearUnknown) }, + ) { Text(tr("common.save")) } + } }, dismissButton = { TextButton(onClick = onDismiss) { Text(tr("common.cancel")) } }, ) diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt index 7ad8a32..c1128d5 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt @@ -49,6 +49,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -74,6 +75,7 @@ import com.scarriffle.calendarr.ui.menu.MenuSheet import com.scarriffle.calendarr.ui.profile.ProfileScreen import com.scarriffle.calendarr.ui.settings.SettingsScreen import com.scarriffle.calendarr.ui.tr +import kotlinx.coroutines.launch import java.time.LocalDate private enum class Overlay { NONE, PROFILE, SETTINGS, ACCOUNTS, GROUPS } @@ -133,7 +135,9 @@ fun CalendarScreen( var dayPreview by remember { mutableStateOf(null) } var fabMenuOpen by remember { mutableStateOf(false) } var showBirthday by remember { mutableStateOf(false) } - var birthdayCals by remember { mutableStateOf>(emptyList()) } + var birthdayCal by remember { mutableStateOf(null) } + val birthdayScope = rememberCoroutineScope() + val birthdayCalName = tr("birthday.calendar_name") // Continuous month scrolling val monthListState = rememberLazyListState() @@ -280,12 +284,13 @@ fun CalendarScreen( } if (showBirthday) { - androidx.compose.runtime.LaunchedEffect(Unit) { birthdayCals = vm.birthdayCalendars() } + androidx.compose.runtime.LaunchedEffect(Unit) { birthdayCal = vm.birthdayCalendar() } BirthdayDialog( - calendars = birthdayCals, + calendar = birthdayCal, onDismiss = { showBirthday = false }, - onSave = { name, date, yearKnown, calId -> - vm.createBirthday(calId, name, date, yearKnown) {} + onActivate = { birthdayScope.launch { birthdayCal = vm.ensureBirthdayCalendar(birthdayCalName) } }, + onSave = { name, date, yearKnown -> + birthdayCal?.let { vm.createBirthday(it.id, name, date, yearKnown) {} } showBirthday = false }, ) diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt index 9fa4515..bd837a9 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt @@ -503,10 +503,18 @@ class CalendarViewModel @Inject constructor( } } - /** Birthday calendars the user may write to (own or shared read/write). */ - suspend fun birthdayCalendars(): List = + /** The user's single birthday calendar, or null if not activated yet. */ + suspend fun birthdayCalendar(): LocalCalendar? = runCatching { repository.getLocalCalendars() }.getOrDefault(emptyList()) - .filter { it.isBirthday && (it.owned || it.permission == "read_write") } + .firstOrNull { it.isBirthday && it.owned } + + /** The birthday calendar, creating the single one (named [name]) if none. */ + suspend fun ensureBirthdayCalendar(name: String): LocalCalendar? { + birthdayCalendar()?.let { return it } + return runCatching { + repository.addLocalCalendar(name, "#E0407F", isBirthday = true) + }.getOrNull() + } /** * Create a manual birthday: an all-day, yearly-recurring local event whose diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/scarriffle/calendarr/ui/settings/SettingsViewModel.kt index b4878ad..43e146f 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/settings/SettingsViewModel.kt @@ -69,7 +69,9 @@ class SettingsViewModel @Inject constructor( groupVisibleId = s.groupVisibleCalendarId ?: 0 } runCatching { repository.getLocalCalendars() }.onSuccess { cals -> - ownLocalCalendars = cals.filter { it.owned && !it.group } + // A birthday calendar may be shared directly, but never stand in + // as the group-visible personal calendar. + ownLocalCalendars = cals.filter { it.owned && !it.group && !it.isBirthday } } } }