Share the unnamed-calendar label, visibility switch and manager row

This commit is contained in:
2026-10-16 13:35:45 +02:00
parent 4041a56ac8
commit d6e8981249
12 changed files with 93 additions and 80 deletions
@@ -53,6 +53,7 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip
import de.jeanlucmakiola.calendula.ui.common.CalendarFailure
import de.jeanlucmakiola.calendula.ui.common.LeadingAvatar
import de.jeanlucmakiola.calendula.ui.common.LocalManageCalendars
import de.jeanlucmakiola.calendula.ui.common.orUnnamed
import de.jeanlucmakiola.floret.components.CollapsingScaffold
import de.jeanlucmakiola.floret.components.DialogAmountField
import de.jeanlucmakiola.floret.components.DialogUnitDropdown
@@ -317,7 +318,7 @@ private fun ExportCalendarPicker(
calendars.forEachIndexed { index, calendar ->
val isSelected = calendar.id in selected
GroupedRow(
title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
title = calendar.displayName.orUnnamed(),
summary = calendar.description,
position = positionOf(index, calendars.size),
leading = { CalendarColorChip(calendar.color) },
@@ -40,7 +40,6 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -73,6 +72,7 @@ import de.jeanlucmakiola.calendula.domain.stateLabels
import de.jeanlucmakiola.calendula.data.calendar.CalendarColorPalette
import de.jeanlucmakiola.calendula.ui.common.AccountKey
import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip
import de.jeanlucmakiola.calendula.ui.common.CalendarVisibilitySwitch
import de.jeanlucmakiola.calendula.ui.common.accountGroupTitle
import de.jeanlucmakiola.calendula.ui.common.groupByAccount
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
@@ -82,6 +82,7 @@ import de.jeanlucmakiola.calendula.ui.common.eventInk
import de.jeanlucmakiola.calendula.ui.common.LeadingAvatar
import de.jeanlucmakiola.calendula.ui.common.SourceLogo
import de.jeanlucmakiola.calendula.ui.common.curatedSourcePackage
import de.jeanlucmakiola.calendula.ui.common.orUnnamed
import de.jeanlucmakiola.floret.components.CollapsingScaffold
import de.jeanlucmakiola.floret.components.GroupedHint
import de.jeanlucmakiola.floret.components.GroupedSectionHeader
@@ -238,21 +239,10 @@ private fun CalendarsList(
GroupedHint(stringResource(R.string.calendars_local_empty), style = MaterialTheme.typography.bodySmall)
} else {
local.forEachIndexed { index, calendar ->
val disabled = !calendar.isVisibleInSystem
GroupedRow(
title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
summary = calendarRowSummary(calendar),
position = if (index == local.lastIndex) Position.Bottom else Position.Middle,
container = MaterialTheme.colorScheme.surfaceContainerHighest,
dimmed = disabled,
leading = { CalendarColorChip(calendar.color, dimIf(disabled)) },
trailing = {
EnableSwitch(
calendarName = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
enabled = !disabled,
onToggle = { enabled -> onSetVisible(calendar.id, enabled) },
)
},
CalendarManagerRow(
calendar = calendar,
last = index == local.lastIndex,
onSetVisible = onSetVisible,
onClick = { onEdit(calendar) },
)
}
@@ -323,27 +313,10 @@ private fun CalendarsList(
// bottom, dimmed and switchless.
val ordered = cals.orderedForManager()
ordered.forEachIndexed { index, calendar ->
val disabled = !calendar.isVisibleInSystem || calendar.isNotSynced
GroupedRow(
title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
summary = calendarRowSummary(calendar),
position = if (index == ordered.lastIndex) Position.Bottom else Position.Middle,
container = MaterialTheme.colorScheme.surfaceContainerHighest,
dimmed = disabled,
leading = { CalendarColorChip(calendar.color, dimIf(disabled)) },
trailing = if (calendar.hasVisibilitySwitch) {
{
EnableSwitch(
calendarName = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
enabled = calendar.isVisibleInSystem,
onToggle = { enabled ->
onSetVisible(calendar.id, enabled)
},
)
}
} else {
null
},
CalendarManagerRow(
calendar = calendar,
last = index == ordered.lastIndex,
onSetVisible = onSetVisible,
)
}
}
@@ -682,17 +655,37 @@ private fun calendarRowSummary(calendar: CalendarSource): String? {
* `Calendars.VISIBLE`. Unchecked drops the calendar out of every surface and
* stops its reminders. Carries its own content description.
*/
/**
* One calendar in a group: dimmed while switched off or not syncing, with the
* visibility switch only where it can change anything.
*/
@Composable
private fun EnableSwitch(
calendarName: String,
enabled: Boolean,
onToggle: (Boolean) -> Unit,
private fun CalendarManagerRow(
calendar: CalendarSource,
last: Boolean,
onSetVisible: (Long, Boolean) -> Unit,
onClick: (() -> Unit)? = null,
) {
val label = stringResource(R.string.calendars_visibility_a11y, calendarName)
Switch(
checked = enabled,
onCheckedChange = onToggle,
modifier = Modifier.semantics { contentDescription = label },
val disabled = !calendar.isVisibleInSystem || calendar.isNotSynced
GroupedRow(
title = calendar.displayName.orUnnamed(),
summary = calendarRowSummary(calendar),
position = if (last) Position.Bottom else Position.Middle,
container = MaterialTheme.colorScheme.surfaceContainerHighest,
dimmed = disabled,
leading = { CalendarColorChip(calendar.color, dimIf(disabled)) },
trailing = if (calendar.hasVisibilitySwitch) {
{
CalendarVisibilitySwitch(
calendarName = calendar.displayName.orUnnamed(),
enabled = calendar.isVisibleInSystem,
onToggle = { enabled -> onSetVisible(calendar.id, enabled) },
)
}
} else {
null
},
onClick = onClick,
)
}
@@ -57,7 +57,7 @@ fun List<CalendarSource>.groupByAccount(): List<CalendarAccountGroup> {
*/
@Composable
fun accountGroupTitle(group: CalendarAccountGroup): String {
val label = group.label.ifBlank { stringResource(R.string.calendar_unnamed) }
val label = group.label.orUnnamed()
return if (!group.ambiguous) {
label
} else {
@@ -114,7 +114,7 @@ private fun CalendarPickerGroup(
)
calendars.forEachIndexed { index, calendar ->
GroupedRow(
title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
title = calendar.displayName.orUnnamed(),
position = if (index == calendars.lastIndex) Position.Bottom else Position.Middle,
checked = calendar.id == selectedId,
leading = { CalendarColorChip(calendar.color) },
@@ -3,10 +3,12 @@ package de.jeanlucmakiola.calendula.ui.common
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.Dp
import de.jeanlucmakiola.calendula.R
/**
* The calendar surfaces' line-height treatment. Material wraps an 11sp glyph in
@@ -55,3 +57,7 @@ fun rememberTrimmedLineHeight(style: TextStyle): Dp {
with(density) { measurer.measure(LINE_SAMPLE, style.trimmedLines()).size.height.toDp() }
}
}
/** A calendar or account name, or the "unnamed" label when it is blank. */
@Composable
fun String.orUnnamed(): String = ifBlank { stringResource(R.string.calendar_unnamed) }
@@ -0,0 +1,24 @@
package de.jeanlucmakiola.calendula.ui.common
import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import de.jeanlucmakiola.calendula.R
/** The per-row switch writing the device's own `Calendars.VISIBLE`, as Settings does (#75). */
@Composable
fun CalendarVisibilitySwitch(
calendarName: String,
enabled: Boolean,
onToggle: (Boolean) -> Unit,
) {
val label = stringResource(R.string.calendars_visibility_a11y, calendarName)
Switch(
checked = enabled,
onCheckedChange = onToggle,
modifier = Modifier.semantics { contentDescription = label },
)
}
@@ -90,6 +90,7 @@ import de.jeanlucmakiola.calendula.domain.TimeZoneOption
import de.jeanlucmakiola.calendula.domain.timeZoneOptionOf
import de.jeanlucmakiola.calendula.domain.zoneDescriptor
import de.jeanlucmakiola.calendula.domain.zoneOrNull
import de.jeanlucmakiola.calendula.ui.common.orUnnamed
import de.jeanlucmakiola.floret.components.IconInfoCard
import de.jeanlucmakiola.floret.identity.LocalFloretDarkTheme
import de.jeanlucmakiola.floret.identity.predictiveBack
@@ -446,7 +447,7 @@ private fun EventDetailContent(
iconContentDescription = stringResource(R.string.event_detail_calendar),
) {
Text(
text = state.calendarName?.ifBlank { stringResource(R.string.calendar_unnamed) }
text = state.calendarName?.orUnnamed()
?: stringResource(R.string.event_detail_calendar_unknown),
style = MaterialTheme.typography.titleMedium,
)
@@ -33,6 +33,9 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Notes
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import de.jeanlucmakiola.calendula.ui.common.orUnnamed
import de.jeanlucmakiola.floret.components.IconInfoCard
import de.jeanlucmakiola.floret.identity.LocalFloretDarkTheme
import kotlinx.coroutines.launch
@@ -85,11 +88,9 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
@@ -832,7 +833,7 @@ private fun EventEditContent(
text = stringResource(
R.string.event_edit_managed_hint,
selectedCalendar?.displayName
?.ifBlank { stringResource(R.string.calendar_unnamed) }
?.orUnnamed()
.orEmpty(),
),
style = MaterialTheme.typography.bodySmall,
@@ -939,7 +940,7 @@ private fun EventEditContent(
) {
Text(
text = selectedCalendar?.displayName
?.ifBlank { stringResource(R.string.calendar_unnamed) }
?.orUnnamed()
?: stringResource(R.string.event_edit_error_no_calendar),
style = MaterialTheme.typography.titleMedium,
color = if (selectedCalendar == null) {
@@ -21,6 +21,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import de.jeanlucmakiola.calendula.R
import de.jeanlucmakiola.calendula.domain.FailureReason
import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip
import de.jeanlucmakiola.calendula.ui.common.orUnnamed
import de.jeanlucmakiola.calendula.ui.common.sourceAppName
import de.jeanlucmakiola.floret.components.GroupedListInset
import de.jeanlucmakiola.floret.components.GroupedRow
@@ -62,7 +63,7 @@ private fun FilterList(
) {
Column(modifier = modifier.fillMaxWidth()) {
groups.forEach { group ->
val account = group.account.ifBlank { stringResource(R.string.calendar_unnamed) }
val account = group.account.orUnnamed()
Text(
text = if (group.ambiguous) {
stringResource(
@@ -84,7 +85,7 @@ private fun FilterList(
)
group.calendars.forEachIndexed { index, cal ->
GroupedRow(
title = cal.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
title = cal.displayName.orUnnamed(),
position = positionOf(index, group.calendars.size),
minHeight = 56.dp,
leading = { CalendarColorChip(cal.color) },
@@ -8,20 +8,19 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import de.jeanlucmakiola.calendula.R
import de.jeanlucmakiola.calendula.domain.CalendarSource
import de.jeanlucmakiola.calendula.domain.hasVisibilitySwitch
import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip
import de.jeanlucmakiola.calendula.ui.common.CalendarVisibilitySwitch
import de.jeanlucmakiola.calendula.ui.common.orUnnamed
import de.jeanlucmakiola.floret.components.GroupedListInset
import de.jeanlucmakiola.floret.components.GroupedRow
import de.jeanlucmakiola.floret.components.OnboardingPrimaryButton
@@ -122,7 +121,7 @@ internal fun HiddenCalendarsStep(
calendars.forEachIndexed { index, calendar ->
val off = !calendar.isVisibleInSystem
GroupedRow(
title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
title = calendar.displayName.orUnnamed(),
summary = calendar.accountName.takeIf { it.isNotBlank() },
position = when {
calendars.size == 1 -> Position.Alone
@@ -133,8 +132,8 @@ internal fun HiddenCalendarsStep(
dimmed = off,
trailing = if (calendar.hasVisibilitySwitch) {
{
CalendarSwitch(
calendarName = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
CalendarVisibilitySwitch(
calendarName = calendar.displayName.orUnnamed(),
enabled = !off,
onToggle = { on -> onSetVisible(calendar.id, on) },
)
@@ -149,18 +148,3 @@ internal fun HiddenCalendarsStep(
Spacer(Modifier.height(OnboardingSpace.md))
}
}
/** The per-row switch, writing the device's own `Calendars.VISIBLE` as Settings does. */
@Composable
private fun CalendarSwitch(
calendarName: String,
enabled: Boolean,
onToggle: (Boolean) -> Unit,
) {
val label = stringResource(R.string.calendars_visibility_a11y, calendarName)
Switch(
checked = enabled,
onCheckedChange = onToggle,
modifier = Modifier.semantics { contentDescription = label },
)
}
@@ -25,6 +25,7 @@ import de.jeanlucmakiola.calendula.ui.common.DurationPicker
import de.jeanlucmakiola.calendula.ui.common.durationLabel
import de.jeanlucmakiola.calendula.ui.common.eventFormFieldIcon
import de.jeanlucmakiola.calendula.ui.common.eventFormFieldLabel
import de.jeanlucmakiola.calendula.ui.common.orUnnamed
import de.jeanlucmakiola.floret.components.CollapsingScaffold
import de.jeanlucmakiola.floret.components.ExpandIcon
import de.jeanlucmakiola.floret.components.GroupedHint
@@ -129,7 +130,7 @@ internal fun EventFormScreen(
durationCalendars.forEachIndexed { index, calendar ->
val override = state.perCalendarEventDuration[calendar.id]
GroupedRow(
title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
title = calendar.displayName.orUnnamed(),
summary = override?.let { durationLabel(it) }
?: stringResource(
R.string.settings_calendar_duration_inherits,
@@ -37,6 +37,7 @@ import de.jeanlucmakiola.calendula.ui.common.REMINDER_PRESETS
import de.jeanlucmakiola.calendula.ui.common.ReminderDefaultPicker
import de.jeanlucmakiola.calendula.ui.common.TimePickerAlert
import de.jeanlucmakiola.calendula.ui.common.durationLabel
import de.jeanlucmakiola.calendula.ui.common.orUnnamed
import de.jeanlucmakiola.floret.android.hasPermission
import de.jeanlucmakiola.floret.components.CollapsingScaffold
import de.jeanlucmakiola.floret.components.ExpandIcon
@@ -172,7 +173,7 @@ internal fun NotificationsScreen(
// own section — link there instead.
if (calendar.id in state.managedCalendarIds) {
GroupedRow(
title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
title = calendar.displayName.orUnnamed(),
summary = stringResource(R.string.settings_calendar_reminders_managed_hint),
position = Position.Alone,
leading = { CalendarColorChip(calendar.color) },
@@ -189,7 +190,7 @@ internal fun NotificationsScreen(
}
val expanded = calendar.id in expandedCalendars
GroupedRow(
title = calendar.displayName.ifBlank { stringResource(R.string.calendar_unnamed) },
title = calendar.displayName.orUnnamed(),
position = if (expanded) Position.Top else Position.Alone,
leading = { CalendarColorChip(calendar.color) },
trailing = { ExpandIcon(expanded) },