diff --git a/CHANGELOG.md b/CHANGELOG.md index 543bf84..f1577ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Settings → Calendars now says what is different about a calendar instead of + leaving you to guess. Ones you can only view — a subscribed calendar, a + calendar shared with you read-only — are marked **Read-only**. Ones whose + account isn't syncing events to this device are marked **Not synced**, moved + to the bottom of their account and left without a switch: none of their events + are on the device, so switching one on could not show you anything ([#76]). +- The calendar picker in the event form and in the .ics import screen now ends + with a **"Missing a calendar?"** row that opens Settings → Calendars, where + those marks then explain why a calendar isn't offered ([#76]). + ### Fixed - Reminders now arrive for every calendar you have switched on. A calendar that was hidden at system level — switched off in another calendar app, or never @@ -1135,3 +1146,4 @@ automatically, with zero telemetry and no internet permission. [#44]: https://codeberg.org/jlmakiola/calendula/issues/44 [#70]: https://codeberg.org/jlmakiola/calendula/issues/70 [#75]: https://codeberg.org/jlmakiola/calendula/issues/75 +[#76]: https://codeberg.org/jlmakiola/calendula/issues/76 diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt index b830f74..def81c5 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt @@ -415,6 +415,7 @@ fun CalendarHost( initialStartMinutes = createStartMinutes ?: heldCreateMinutes, onClose = { createDateIso = null }, onSaved = { createDateIso = null }, + onManageCalendars = { showCalendars = true }, ) } } @@ -434,6 +435,7 @@ fun CalendarHost( editKey = null detailKey = null }, + onManageCalendars = { showCalendars = true }, ) } } @@ -450,18 +452,6 @@ fun CalendarHost( ) } - // Calendar manager — slides over Settings. - AnimatedVisibility( - visible = showCalendars, - enter = slideInHorizontally(slideSpec) { it } + fadeIn(), - exit = slideOutHorizontally(slideSpec) { it } + fadeOut(), - ) { - CalendarsScreen( - onBack = { showCalendars = false }, - onImport = { importUri = it; importForceMany = true }, - ) - } - // Import flow for an opened/received .ics file. A single event routes // into the create form (prefilled, for review); many open the picker. importUri?.let { uri -> @@ -469,6 +459,7 @@ fun CalendarHost( uri = uri, forceMany = importForceMany, onClose = { importUri = null }, + onManageCalendars = { showCalendars = true }, onOpenSingle = { form -> importUri = null importFormSource = ImportSource.File @@ -483,6 +474,22 @@ fun CalendarHost( initialFormSource = importFormSource, onClose = { importForm = null }, onSaved = { importForm = null }, + onManageCalendars = { showCalendars = true }, + ) + } + + // Calendar manager — declared last so it covers every overlay that can + // open it: Settings, both event forms, and the .ics import picker (#76). + // Coming back from it leaves the caller exactly as it was, with the + // calendar list already refreshed by the provider's notification. + AnimatedVisibility( + visible = showCalendars, + enter = slideInHorizontally(slideSpec) { it } + fadeIn(), + exit = slideOutHorizontally(slideSpec) { it } + fadeOut(), + ) { + CalendarsScreen( + onBack = { showCalendars = false }, + onImport = { importUri = it; importForceMany = true }, ) } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarPickerGroups.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarPickerGroups.kt index 8a25102..4d00227 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarPickerGroups.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarPickerGroups.kt @@ -11,8 +11,10 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.Cloud import androidx.compose.material.icons.filled.PhoneAndroid +import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -42,12 +44,19 @@ import de.jeanlucmakiola.floret.components.SelectedCheck * account — with the calendars beneath it as a connected card, a colour chip on * each and a check on the selected one. Emits into the caller's [ColumnScope] * (a scrolling column), so the caller owns the surrounding chrome. + * + * The list holds event *targets* only, so a calendar that is switched off, + * read-only or managed is silently absent — which reads as a missing calendar + * rather than an excluded one (#76). [onManageCalendars], when given, adds the + * footer row that names the possible reasons and opens the calendar manager, + * where each row then says which one applies. */ @Composable fun ColumnScope.CalendarPickerGroups( calendars: List, selectedId: Long?, onSelect: (Long) -> Unit, + onManageCalendars: (() -> Unit)? = null, ) { val local = remember(calendars) { calendars.filter { it.isLocal } } val syncedGroups = remember(calendars) { @@ -75,6 +84,23 @@ fun ColumnScope.CalendarPickerGroups( onSelect = onSelect, ) } + if (onManageCalendars != null) { + Spacer(Modifier.height(16.dp)) + GroupedRow( + title = stringResource(R.string.calendar_picker_missing_title), + summary = stringResource(R.string.calendar_picker_missing_summary), + position = Position.Alone, + leading = { LeadingAvatar(Icons.Default.VisibilityOff) }, + trailing = { + Icon( + Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + onClick = onManageCalendars, + ) + } } /** One account's category header (avatar + name) atop its selectable calendars. */ diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt index b064a35..b05edfa 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt @@ -193,6 +193,7 @@ fun EventEditScreen( initialStartMinutes: Int? = null, initialForm: EventForm? = null, initialFormSource: ImportSource = ImportSource.File, + onManageCalendars: (() -> Unit)? = null, viewModel: EventEditViewModel = hiltViewModel(), ) { LaunchedEffect(initialDateIso, editKey, initialForm) { @@ -309,6 +310,7 @@ fun EventEditScreen( modifier = Modifier .fillMaxSize() .padding(innerPadding), + onManageCalendars = onManageCalendars, ) } } @@ -500,6 +502,7 @@ private fun EventEditContent( state: EventEditUiState, viewModel: EventEditViewModel, modifier: Modifier = Modifier, + onManageCalendars: (() -> Unit)? = null, ) { val form = state.form val locale = currentLocale() @@ -1122,6 +1125,10 @@ private fun EventEditContent( viewModel.setCalendar(it) showCalendarPicker = false }, + // Leaves the form and its picker standing underneath: the manager + // slides over, and a calendar switched on there is in the list on + // the way back (the provider notifies, the flow re-queries). + onManageCalendars = onManageCalendars, onDismiss = { showCalendarPicker = false }, ) } @@ -2277,6 +2284,7 @@ private fun CalendarPicker( selectedId: Long?, onSelect: (Long) -> Unit, onDismiss: () -> Unit, + onManageCalendars: (() -> Unit)? = null, ) { FullScreenPicker( title = stringResource(R.string.event_detail_calendar), @@ -2286,6 +2294,7 @@ private fun CalendarPicker( calendars = calendars, selectedId = selectedId, onSelect = onSelect, + onManageCalendars = onManageCalendars, ) } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/imports/ImportScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/imports/ImportScreen.kt index c54b45f..32e60e8 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/imports/ImportScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/imports/ImportScreen.kt @@ -75,6 +75,7 @@ fun ImportScreen( onClose: () -> Unit, onOpenSingle: (EventForm) -> Unit, forceMany: Boolean = false, + onManageCalendars: (() -> Unit)? = null, // Key the VM by the file uri. This screen has no nav backstack, so an // unkeyed hiltViewModel() resolves to the Activity's store and is retained // across imports — its one-shot `load` guard would then show the *previous* @@ -155,7 +156,12 @@ fun ImportScreen( ImportUiState.Empty -> CenteredMessage(stringResource(R.string.import_empty), onClose) ImportUiState.Failed -> CenteredMessage(stringResource(R.string.import_failed), onClose) - is ImportUiState.Many -> ManyContent(s, selected, onSelect = { selected = it }) + is ImportUiState.Many -> ManyContent( + state = s, + selected = selected, + onSelect = { selected = it }, + onManageCalendars = onManageCalendars, + ) is ImportUiState.Done -> DoneContent(s, onClose) } } @@ -163,7 +169,12 @@ fun ImportScreen( } @Composable -private fun ManyContent(state: ImportUiState.Many, selected: Long?, onSelect: (Long) -> Unit) { +private fun ManyContent( + state: ImportUiState.Many, + selected: Long?, + onSelect: (Long) -> Unit, + onManageCalendars: (() -> Unit)? = null, +) { // No writable calendar to import into — tell the user honestly. if (state.calendars.isEmpty()) { CenteredMessage(stringResource(R.string.import_no_calendar), onClose = null) @@ -178,6 +189,7 @@ private fun ManyContent(state: ImportUiState.Many, selected: Long?, onSelect: (L calendars = state.calendars, selectedId = selected, onSelect = onSelect, + onManageCalendars = onManageCalendars, ) if (state.warnings.isNotEmpty()) { Column(