feat(calendars): account header overflow menu + restyle
Replace the per-account toggle switch and nested manage row with a single trailing overflow (⋮) menu holding both account-level actions: - "Enable all" / "Disable all" — toggle every calendar in the group - "Manage in app" (synced) / "Add calendar" (local) The dropdown is styled to fit: rounded corners, a distinct floating surface (surfaceContainerLowest + lifted shadow) so it stands clear of the cards, and a divider separating the two actions. Shortened the manage label to "Manage in app" and dropped the now-unused account a11y strings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,11 +36,17 @@ import androidx.compose.material.icons.filled.Close
|
|||||||
import androidx.compose.material.icons.filled.Cloud
|
import androidx.compose.material.icons.filled.Cloud
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.FileDownload
|
import androidx.compose.material.icons.filled.FileDownload
|
||||||
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.filled.Palette
|
import androidx.compose.material.icons.filled.Palette
|
||||||
import androidx.compose.material.icons.filled.PhoneAndroid
|
import androidx.compose.material.icons.filled.PhoneAndroid
|
||||||
|
import androidx.compose.material.icons.filled.Visibility
|
||||||
|
import androidx.compose.material.icons.filled.VisibilityOff
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -232,19 +238,15 @@ private fun CalendarsList(
|
|||||||
CalendarGroup(
|
CalendarGroup(
|
||||||
title = stringResource(R.string.calendars_local_header),
|
title = stringResource(R.string.calendars_local_header),
|
||||||
expanded = localExpanded,
|
expanded = localExpanded,
|
||||||
|
bodyHasRows = local.isNotEmpty(),
|
||||||
headerDisabled = localDisabled,
|
headerDisabled = localDisabled,
|
||||||
leading = { Box(dimIf(localDisabled)) { LeadingAvatar(Icons.Default.PhoneAndroid) } },
|
leading = { Box(dimIf(localDisabled)) { LeadingAvatar(Icons.Default.PhoneAndroid) } },
|
||||||
manageIcon = Icons.Default.Add,
|
manageIcon = Icons.Default.Add,
|
||||||
manageLabel = stringResource(R.string.calendars_add),
|
manageLabel = stringResource(R.string.calendars_add),
|
||||||
onManage = onAdd,
|
onManage = onAdd,
|
||||||
manageConnectsBelow = local.isNotEmpty(),
|
|
||||||
onToggleExpand = { localExpanded = !localExpanded },
|
onToggleExpand = { localExpanded = !localExpanded },
|
||||||
showToggleAll = local.isNotEmpty(),
|
showToggleAll = local.isNotEmpty(),
|
||||||
allEnabled = local.none { it.id in disabledIds },
|
allEnabled = local.none { it.id in disabledIds },
|
||||||
toggleAllLabel = stringResource(
|
|
||||||
R.string.calendars_account_toggle_all_a11y,
|
|
||||||
stringResource(R.string.calendars_local_header),
|
|
||||||
),
|
|
||||||
onToggleAll = { enabled -> onSetAccountDisabled(local.map { it.id }, !enabled) },
|
onToggleAll = { enabled -> onSetAccountDisabled(local.map { it.id }, !enabled) },
|
||||||
) {
|
) {
|
||||||
if (local.isEmpty()) {
|
if (local.isEmpty()) {
|
||||||
@@ -305,14 +307,14 @@ private fun CalendarsList(
|
|||||||
CalendarGroup(
|
CalendarGroup(
|
||||||
title = account,
|
title = account,
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
|
bodyHasRows = true,
|
||||||
headerDisabled = accountDisabled,
|
headerDisabled = accountDisabled,
|
||||||
leading = { Box(dimIf(accountDisabled)) { SourceLogo(accountType) } },
|
leading = { Box(dimIf(accountDisabled)) { SourceLogo(accountType) } },
|
||||||
manageIcon = Icons.AutoMirrored.Filled.OpenInNew,
|
manageIcon = Icons.AutoMirrored.Filled.OpenInNew,
|
||||||
manageLabel = stringResource(R.string.calendars_manage_account_a11y, account),
|
manageLabel = stringResource(R.string.calendars_manage_in_app),
|
||||||
onManage = {
|
onManage = {
|
||||||
runCatching { context.startActivity(sourceAppIntent(context, accountType)) }
|
runCatching { context.startActivity(sourceAppIntent(context, accountType)) }
|
||||||
},
|
},
|
||||||
manageConnectsBelow = true,
|
|
||||||
onToggleExpand = {
|
onToggleExpand = {
|
||||||
collapsedAccounts = if (expanded) {
|
collapsedAccounts = if (expanded) {
|
||||||
collapsedAccounts + account
|
collapsedAccounts + account
|
||||||
@@ -322,7 +324,6 @@ private fun CalendarsList(
|
|||||||
},
|
},
|
||||||
showToggleAll = true,
|
showToggleAll = true,
|
||||||
allEnabled = cals.none { it.id in disabledIds },
|
allEnabled = cals.none { it.id in disabledIds },
|
||||||
toggleAllLabel = stringResource(R.string.calendars_account_toggle_all_a11y, account),
|
|
||||||
onToggleAll = { enabled -> onSetAccountDisabled(cals.map { it.id }, !enabled) },
|
onToggleAll = { enabled -> onSetAccountDisabled(cals.map { it.id }, !enabled) },
|
||||||
) {
|
) {
|
||||||
cals.forEachIndexed { index, calendar ->
|
cals.forEachIndexed { index, calendar ->
|
||||||
@@ -552,53 +553,50 @@ private fun EditorCard(
|
|||||||
/**
|
/**
|
||||||
* One collapsible calendar group rendered as a connected card. The header row is
|
* One collapsible calendar group rendered as a connected card. The header row is
|
||||||
* the card's top (tap to expand/collapse): a [leading] source mark (the account
|
* the card's top (tap to expand/collapse): a [leading] source mark (the account
|
||||||
* app's logo, or a device chip for local), the group [title] and — when
|
* app's logo, or a device chip for local), the group [title] and a trailing
|
||||||
* [showToggleAll] — a "toggle all" [Switch] that enables or disables every
|
* overflow (⋮) menu holding the account-level actions — enable/disable every
|
||||||
* calendar in the group at once.
|
* calendar at once (when [showToggleAll]) and the manage/add action
|
||||||
|
* ([manageIcon] / [manageLabel] → [onManage]). The group's calendars render as
|
||||||
|
* the rows below via [body].
|
||||||
*
|
*
|
||||||
* When expanded, the body opens with a differentiated management row
|
* The header keeps the standard row colour; the calendars it reveals sit one tone
|
||||||
* ([manageIcon] / [manageLabel] → [onManage]: add a calendar, or open the source
|
* darker, so the group reads as a header over nested content. Once [headerDisabled]
|
||||||
* app) above the plain calendar rows supplied by [body]; [manageConnectsBelow]
|
* (every calendar switched off) the header fades to the disabled emphasis so the
|
||||||
* is false when no calendar rows follow it, so it rounds off as the card's foot.
|
* whole group reads as off — not just via the menu. [bodyHasRows] is false when no
|
||||||
*
|
* calendar rows follow (e.g. an empty local group), so the header stays a
|
||||||
* An active group gets a tinted header; once [headerDisabled] (every calendar in
|
* standalone card rather than a top edge with nothing beneath it.
|
||||||
* the group switched off) the header drops to a plain, dimmed row so the whole
|
|
||||||
* group reads as off — not just its switch.
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun CalendarGroup(
|
private fun CalendarGroup(
|
||||||
title: String,
|
title: String,
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
|
bodyHasRows: Boolean,
|
||||||
headerDisabled: Boolean,
|
headerDisabled: Boolean,
|
||||||
leading: @Composable () -> Unit,
|
leading: @Composable () -> Unit,
|
||||||
manageIcon: ImageVector,
|
manageIcon: ImageVector,
|
||||||
manageLabel: String,
|
manageLabel: String,
|
||||||
onManage: () -> Unit,
|
onManage: () -> Unit,
|
||||||
manageConnectsBelow: Boolean,
|
|
||||||
onToggleExpand: () -> Unit,
|
onToggleExpand: () -> Unit,
|
||||||
showToggleAll: Boolean,
|
showToggleAll: Boolean,
|
||||||
allEnabled: Boolean,
|
allEnabled: Boolean,
|
||||||
toggleAllLabel: String,
|
|
||||||
onToggleAll: (Boolean) -> Unit,
|
onToggleAll: (Boolean) -> Unit,
|
||||||
body: @Composable ColumnScope.() -> Unit,
|
body: @Composable ColumnScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
GroupedRow(
|
GroupedRow(
|
||||||
title = title,
|
title = title,
|
||||||
position = if (expanded) Position.Top else Position.Alone,
|
position = if (expanded && bodyHasRows) Position.Top else Position.Alone,
|
||||||
// The account header keeps the standard row colour; the options it reveals
|
|
||||||
// sit one tone darker, so the group reads as a header over nested content.
|
|
||||||
dimmed = headerDisabled,
|
dimmed = headerDisabled,
|
||||||
leading = leading,
|
leading = leading,
|
||||||
trailing = if (showToggleAll) {
|
trailing = {
|
||||||
{
|
CalendarGroupMenu(
|
||||||
Switch(
|
title = title,
|
||||||
checked = allEnabled,
|
showToggleAll = showToggleAll,
|
||||||
onCheckedChange = onToggleAll,
|
allEnabled = allEnabled,
|
||||||
modifier = Modifier.semantics { contentDescription = toggleAllLabel },
|
onToggleAll = onToggleAll,
|
||||||
)
|
manageIcon = manageIcon,
|
||||||
}
|
manageLabel = manageLabel,
|
||||||
} else {
|
onManage = onManage,
|
||||||
null
|
)
|
||||||
},
|
},
|
||||||
onClick = onToggleExpand,
|
onClick = onToggleExpand,
|
||||||
)
|
)
|
||||||
@@ -607,16 +605,76 @@ private fun CalendarGroup(
|
|||||||
enter = calendarExpandEnter(),
|
enter = calendarExpandEnter(),
|
||||||
exit = calendarCollapseExit(),
|
exit = calendarCollapseExit(),
|
||||||
) {
|
) {
|
||||||
Column {
|
Column(content = body)
|
||||||
GroupedRow(
|
}
|
||||||
title = manageLabel,
|
}
|
||||||
position = if (manageConnectsBelow) Position.Middle else Position.Bottom,
|
|
||||||
container = MaterialTheme.colorScheme.surfaceContainerHighest,
|
/**
|
||||||
dimmed = headerDisabled,
|
* The account header's overflow (⋮) menu: the two account-level actions that
|
||||||
leading = { Icon(manageIcon, contentDescription = null) },
|
* don't fit on one line — "Enable/Disable all" (when [showToggleAll]) and the
|
||||||
onClick = onManage,
|
* manage/add action. A rounded, tonal dropdown matching the app's surfaces, with
|
||||||
|
* the two actions divided for clear separation.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun CalendarGroupMenu(
|
||||||
|
title: String,
|
||||||
|
showToggleAll: Boolean,
|
||||||
|
allEnabled: Boolean,
|
||||||
|
onToggleAll: (Boolean) -> Unit,
|
||||||
|
manageIcon: ImageVector,
|
||||||
|
manageLabel: String,
|
||||||
|
onManage: () -> Unit,
|
||||||
|
) {
|
||||||
|
var open by remember { mutableStateOf(false) }
|
||||||
|
Box {
|
||||||
|
IconButton(onClick = { open = true }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.MoreVert,
|
||||||
|
contentDescription = stringResource(R.string.calendars_account_menu_a11y, title),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = open,
|
||||||
|
onDismissRequest = { open = false },
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
// A distinct tone + a lifted shadow so the menu reads as floating
|
||||||
|
// above the cards (which sit at surfaceContainerHigh) rather than
|
||||||
|
// blending into them.
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerLowest,
|
||||||
|
tonalElevation = 0.dp,
|
||||||
|
shadowElevation = 6.dp,
|
||||||
|
) {
|
||||||
|
if (showToggleAll) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
stringResource(
|
||||||
|
if (allEnabled) R.string.calendars_disable_all
|
||||||
|
else R.string.calendars_enable_all,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
if (allEnabled) Icons.Default.VisibilityOff else Icons.Default.Visibility,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
open = false
|
||||||
|
onToggleAll(!allEnabled)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
HorizontalDivider(Modifier.padding(horizontal = 12.dp, vertical = 4.dp))
|
||||||
|
}
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(manageLabel) },
|
||||||
|
leadingIcon = { Icon(manageIcon, contentDescription = null) },
|
||||||
|
onClick = {
|
||||||
|
open = false
|
||||||
|
onManage()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
body()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -344,11 +344,12 @@
|
|||||||
<string name="calendars_add">Add calendar</string>
|
<string name="calendars_add">Add calendar</string>
|
||||||
<string name="calendars_disable_hint">Turn a calendar off to remove it from the app — its events, filters and pickers. Nothing is deleted, and you can turn it back on here anytime.</string>
|
<string name="calendars_disable_hint">Turn a calendar off to remove it from the app — its events, filters and pickers. Nothing is deleted, and you can turn it back on here anytime.</string>
|
||||||
<string name="calendars_show_in_app_a11y">Show \"%1$s\" in the app</string>
|
<string name="calendars_show_in_app_a11y">Show \"%1$s\" in the app</string>
|
||||||
<string name="calendars_account_toggle_all_a11y">Show all calendars from %1$s in the app</string>
|
|
||||||
<string name="calendars_synced_header">Synced calendars</string>
|
<string name="calendars_synced_header">Synced calendars</string>
|
||||||
<string name="calendars_synced_hint">These come from accounts on your device. Create and edit them in their own app.</string>
|
<string name="calendars_synced_hint">These come from accounts on your device. Create and edit them in their own app.</string>
|
||||||
<string name="calendars_manage_in_app">Manage</string>
|
<string name="calendars_manage_in_app">Manage in app</string>
|
||||||
<string name="calendars_manage_account_a11y">Manage %1$s in its app</string>
|
<string name="calendars_account_menu_a11y">More options for %1$s</string>
|
||||||
|
<string name="calendars_enable_all">Enable all</string>
|
||||||
|
<string name="calendars_disable_all">Disable all</string>
|
||||||
<string name="calendars_add_account">Add account</string>
|
<string name="calendars_add_account">Add account</string>
|
||||||
<string name="calendars_new_title">New calendar</string>
|
<string name="calendars_new_title">New calendar</string>
|
||||||
<string name="calendars_edit_title">Edit calendar</string>
|
<string name="calendars_edit_title">Edit calendar</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user