diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/InlineTextField.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/InlineTextField.kt index 6a541f1..5bf2f56 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/InlineTextField.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/InlineTextField.kt @@ -36,6 +36,7 @@ fun InlineTextField( textStyle: TextStyle = MaterialTheme.typography.titleMedium, singleLine: Boolean = true, minLines: Int = 1, + enabled: Boolean = true, keyboardType: KeyboardType = KeyboardType.Text, capitalization: KeyboardCapitalization = KeyboardCapitalization.None, imeAction: ImeAction = ImeAction.Default, @@ -43,15 +44,17 @@ fun InlineTextField( onImeAction: (() -> Unit)? = null, ) { val resolvedStyle = textStyle.copy( - color = if (textStyle.color.isSpecified) { - textStyle.color - } else { - MaterialTheme.colorScheme.onSurface + color = when { + // A disabled field reads as dimmed, like a locked value. + !enabled -> MaterialTheme.colorScheme.onSurfaceVariant + textStyle.color.isSpecified -> textStyle.color + else -> MaterialTheme.colorScheme.onSurface }, ) BasicTextField( value = value, onValueChange = onValueChange, + enabled = enabled, textStyle = resolvedStyle, singleLine = singleLine, minLines = minLines, 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 166c0fd..2fd6830 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 @@ -435,6 +435,10 @@ private fun EventEditContent( val form = state.form val locale = currentLocale() val dark = isSystemInDarkTheme() + // The title, date and recurrence are managed by the special-dates sync, so + // they're locked here; everything else (reminders, location, notes) is the + // user's to edit. + val locked = state.isManaged var picker by remember { mutableStateOf(null) } var showCalendarPicker by rememberSaveable { mutableStateOf(false) } var showReminderPicker by rememberSaveable { mutableStateOf(false) } @@ -519,6 +523,7 @@ private fun EventEditContent( placeholder = stringResource(R.string.event_edit_title_hint), textStyle = MaterialTheme.typography.headlineMedium .copy(fontWeight = FontWeight.SemiBold), + enabled = !locked, modifier = Modifier .fillMaxWidth() .padding(vertical = 4.dp) @@ -531,6 +536,17 @@ private fun EventEditContent( .height(3.dp) .background(accent, RoundedCornerShape(2.dp)), ) + if (locked) { + Spacer(Modifier.height(10.dp)) + Text( + text = stringResource( + R.string.event_edit_managed_hint, + selectedCalendar?.displayName.orEmpty(), + ), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } Spacer(Modifier.height(20.dp)) @@ -549,7 +565,11 @@ private fun EventEditContent( style = MaterialTheme.typography.titleMedium, modifier = Modifier.weight(1f), ) - Switch(checked = form.isAllDay, onCheckedChange = viewModel::setAllDay) + Switch( + checked = form.isAllDay, + onCheckedChange = viewModel::setAllDay, + enabled = !locked, + ) } }, ) { @@ -561,6 +581,7 @@ private fun EventEditContent( locale = locale, onDateClick = { picker = PickerTarget.StartDate }, onTimeClick = { picker = PickerTarget.StartTime }, + enabled = !locked, ) ScheduleRow( label = stringResource(R.string.event_edit_ends), @@ -570,6 +591,7 @@ private fun EventEditContent( onDateClick = { picker = PickerTarget.EndDate }, onTimeClick = { picker = PickerTarget.EndTime }, isError = EventFormProblem.EndBeforeStart in state.problems, + enabled = !locked, ) if (EventFormProblem.EndBeforeStart in state.problems) { Spacer(Modifier.height(2.dp)) @@ -700,7 +722,7 @@ private fun EventEditContent( EditCard( icon = Icons.Default.Repeat, iconContentDescription = null, - onClick = { showRecurrencePicker = true }, + onClick = if (locked) null else ({ showRecurrencePicker = true }), ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -1858,6 +1880,7 @@ private fun InlineField( textStyle: TextStyle = MaterialTheme.typography.titleMedium, singleLine: Boolean = true, minLines: Int = 1, + enabled: Boolean = true, keyboardType: KeyboardType = KeyboardType.Text, modifier: Modifier = Modifier .fillMaxWidth() @@ -1871,6 +1894,7 @@ private fun InlineField( textStyle = textStyle, singleLine = singleLine, minLines = minLines, + enabled = enabled, keyboardType = keyboardType, ) } @@ -1885,6 +1909,7 @@ private fun ScheduleRow( onDateClick: () -> Unit, onTimeClick: () -> Unit, isError: Boolean = false, + enabled: Boolean = true, ) { val dateFormat = remember(locale) { DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(locale) @@ -1894,11 +1919,12 @@ private fun ScheduleRow( timeOfDayFormatter(use24Hour, locale) } // Tappable values read as links (primary), like the location on the - // detail screen; errors flip them to the error colour. - val valueColor = if (isError) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.primary + // detail screen; errors flip them to the error colour; a locked (managed) + // row dims to a plain, non-tappable value. + val valueColor = when { + !enabled -> MaterialTheme.colorScheme.onSurfaceVariant + isError -> MaterialTheme.colorScheme.error + else -> MaterialTheme.colorScheme.primary } Row( verticalAlignment = Alignment.CenterVertically, @@ -1915,7 +1941,7 @@ private fun ScheduleRow( style = MaterialTheme.typography.titleMedium, color = valueColor, modifier = Modifier - .clickable(onClick = onDateClick) + .clickable(enabled = enabled, onClick = onDateClick) .padding(vertical = 8.dp, horizontal = 6.dp), ) if (!isAllDay) { @@ -1924,7 +1950,7 @@ private fun ScheduleRow( style = MaterialTheme.typography.titleMedium, color = valueColor, modifier = Modifier - .clickable(onClick = onTimeClick) + .clickable(enabled = enabled, onClick = onTimeClick) .padding(vertical = 8.dp, horizontal = 6.dp), ) } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditUiState.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditUiState.kt index 04b063d..934b792 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditUiState.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditUiState.kt @@ -29,6 +29,12 @@ data class EventEditUiState( val hiddenFields: List = emptyList(), /** True while editing an existing event (the calendar is then fixed). */ val isEditing: Boolean = false, + /** + * True when the event's calendar is a contact special-dates calendar the app + * manages. While editing such an event the title, date and recurrence are + * locked (sync overwrites them); reminders, location and notes stay editable. + */ + val isManaged: Boolean = false, /** * Whether to focus the title and raise the keyboard when this is a fresh * create form (issue #10). Mirrors the settings flag; the screen only acts 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 7190beb..673501a 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 @@ -165,7 +165,8 @@ class EventEditViewModel @Inject constructor( ::ExternalInputs, ).flowOn(io), colorPalette, - ) { local, external, palette -> + settingsPrefs.managedCalendarIds, + ) { local, external, palette, managedIds -> val form = local.form ?: return@combine null val resolvedId = form.calendarId ?: external.lastUsed?.takeIf { id -> external.writable.any { it.id == id } } @@ -180,6 +181,8 @@ class EventEditViewModel @Inject constructor( visibleFields = visibleFields, hiddenFields = (EventFormField.entries.toSet() - visibleFields).sorted(), isEditing = local.editTarget != null, + // Editing an event in a managed calendar locks its synced fields. + isManaged = local.editTarget != null && resolvedId != null && resolvedId in managedIds, autofocusTitle = external.autofocusTitle, // A modified-occurrence exception can't carry its own rule, so // the scope dialog drops "only this event" after a rule change. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 46446bd..e1218ea 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -68,6 +68,8 @@ Close Save Add title + + Managed by ā€œ%1$sā€ — the title, date and repeat are kept in sync from your contacts. Reminders, location and notes are yours to edit. Starts Ends Ends before it starts