diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt index 457b998..42cee09 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt @@ -184,6 +184,9 @@ class EventEditViewModel @Inject constructor( * [state]). Managed special-dates calendars are excluded too: their events * are owned by the contact sync, which would delete any user event created * there. + * + * This is the list of *targets*. An event already living in an excluded + * calendar keeps it — [state] adds it back to the picker. */ private val writableCalendars: Flow> = allCalendars.map { calendars -> calendars.filter { it.canModifyContents && it.isVisibleInSystem && !it.isManaged } @@ -232,11 +235,16 @@ class EventEditViewModel @Inject constructor( // off the calendar's durable marker, not a stored id, so it holds after a // backup restore too. val isManaged = local.editTarget != null && resolvedCalendar?.isManaged == true - // The picker offers writable calendars only; when editing a managed event - // its own (excluded) calendar is added back so the row still names it. + // The picker offers writable calendars only; the event's own calendar is + // added back whenever it isn't among them — a managed special-dates one, + // or one switched off on this device — so the row keeps naming it instead + // of reading as the "no calendar" error, and saving can leave the event + // where it is. A calendar the app may not write to is still no target. + val ownCalendar = resolvedCalendar?.takeIf { own -> + own.canModifyContents && external.writable.none { it.id == own.id } + } val pickerCalendars = - if (isManaged && resolvedCalendar != null) external.writable + resolvedCalendar - else external.writable + if (ownCalendar != null) external.writable + ownCalendar else external.writable // An all-day event is date-anchored, so a zone is meaningless on it — // the field is withheld from both lists rather than shown as a no-op. val offerableFields = EventFormField.entries.toSet() - 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 a549089..9adcf3a 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 @@ -74,9 +74,14 @@ class SettingsViewModel @Inject constructor( private val dynamicColorAvailable = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S - /** Writable calendars — the only ones that take a per-calendar reminder override. */ + /** + * Writable calendars that are switched on — the only ones that take a + * per-calendar reminder override. A calendar switched off in Settings → + * Calendars is `VISIBLE = 0`, so the provider schedules no alarms for it and + * a default reminder configured there could never fire (#75). + */ private val writableCalendars: Flow> = repository.calendars() - .map { calendars -> calendars.filter { it.canModifyContents } } + .map { calendars -> calendars.filter { it.canModifyContents && it.isVisibleInSystem } } .catch { emit(emptyList()) } val state: StateFlow = diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModelTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModelTest.kt index 898399f..184be1b 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModelTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModelTest.kt @@ -108,6 +108,27 @@ class EventEditViewModelTest { job.cancel() } + @Test + fun `editing an event in a switched-off calendar keeps it in the picker`( + @TempDir tempDir: Path, + ) = runTest(dispatcher) { + // Otherwise the calendar row renders as the "no calendar" error and any + // pick routes the save through a move the user never asked for. + val fake = FakeCalendarDataSource().apply { + calendarsResult = listOf(cal(1L), cal(2L, visible = false)) + eventDetailResult = { detail(calendarId = 2L) } + } + val vm = viewModel(tempDir, fake) + val job = activate(vm) + + vm.openForEdit(eventId = 42L, beginMillis = beginMillis, endMillis = endMillis) + advanceUntilIdle() + + assertThat(vm.state.value?.calendars?.map { it.id }).containsExactly(1L, 2L) + assertThat(vm.state.value?.form?.calendarId).isEqualTo(2L) + job.cancel() + } + @Test fun `changing the calendar routes the save through a move, not an update`( @TempDir tempDir: Path,