refactor: one shared calendar picker with settings-page category headers
Extract CalendarPickerGroups into ui/common: the calendar-manager screen's grouped-card system (device chip for local calendars, the owning app's launcher icon per synced account, colour chip + check per calendar) as a single reusable picker. Use it in both the event editor and the .ics import screen so all 'which calendar' lists match. Moves LeadingAvatar/SourceLogo/curatedSourcePackage out of CalendarsScreen into common as the shared source of truth. Drops the redundant 'Add to calendar' caption from the import picker. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -97,6 +97,9 @@ import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
|
|||||||
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
||||||
import de.jeanlucmakiola.calendula.data.calendar.CalendarColorPalette
|
import de.jeanlucmakiola.calendula.data.calendar.CalendarColorPalette
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip
|
import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip
|
||||||
|
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.DialogAmountField
|
import de.jeanlucmakiola.calendula.ui.common.DialogAmountField
|
||||||
import de.jeanlucmakiola.calendula.ui.common.DialogUnitDropdown
|
import de.jeanlucmakiola.calendula.ui.common.DialogUnitDropdown
|
||||||
import de.jeanlucmakiola.calendula.ui.common.calendarCollapseExit
|
import de.jeanlucmakiola.calendula.ui.common.calendarCollapseExit
|
||||||
@@ -771,66 +774,6 @@ private fun CalendarGroupMenu(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The source app's launcher icon (Google Calendar, DAVx5, Nextcloud, …) as a
|
|
||||||
* round 40dp chip, so each synced account is recognisable at a glance. We load
|
|
||||||
* whatever app owns the account from [PackageManager] rather than bundling brand
|
|
||||||
* logos — always accurate, nothing to license. Falls back to a neutral cloud
|
|
||||||
* chip when no installed app resolves for the account.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun SourceLogo(accountType: String) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val logo = remember(accountType) { sourceAppLogo(context, accountType) }
|
|
||||||
if (logo != null) {
|
|
||||||
Image(
|
|
||||||
bitmap = logo,
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = Modifier
|
|
||||||
.size(40.dp)
|
|
||||||
.clip(CircleShape),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
LeadingAvatar(Icons.Default.Cloud)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The launcher icon of the app backing [accountType], preferring the human-facing app. */
|
|
||||||
private fun sourceAppLogo(context: Context, accountType: String): ImageBitmap? {
|
|
||||||
val pm = context.packageManager
|
|
||||||
val candidates = buildList {
|
|
||||||
curatedSourcePackage(accountType)?.let { add(it) }
|
|
||||||
AccountManager.get(context).authenticatorTypes
|
|
||||||
.firstOrNull { it.type.equals(accountType, ignoreCase = true) }
|
|
||||||
?.packageName
|
|
||||||
?.let { add(it) }
|
|
||||||
}
|
|
||||||
for (pkg in candidates) {
|
|
||||||
val bitmap = runCatching { pm.getApplicationIcon(pkg).toBitmap() }.getOrNull()
|
|
||||||
if (bitmap != null) return bitmap.asImageBitmap()
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Neutral circular chip carrying an arbitrary icon (e.g. the local-device mark). */
|
|
||||||
@Composable
|
|
||||||
private fun LeadingAvatar(icon: ImageVector) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(40.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainerHighest),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.size(22.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SectionHeader(text: String) {
|
private fun SectionHeader(text: String) {
|
||||||
@@ -965,9 +908,3 @@ private fun sourceAppIntent(context: Context, accountType: String): Intent {
|
|||||||
return Intent(Settings.ACTION_SYNC_SETTINGS)
|
return Intent(Settings.ACTION_SYNC_SETTINGS)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Preferred app for account types whose authenticator isn't the app to open. */
|
|
||||||
private fun curatedSourcePackage(accountType: String): String? = when {
|
|
||||||
accountType.equals("com.google", ignoreCase = true) -> "com.google.android.calendar"
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.ui.common
|
||||||
|
|
||||||
|
import android.accounts.AccountManager
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
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.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.Cloud
|
||||||
|
import androidx.compose.material.icons.filled.PhoneAndroid
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
|
import de.jeanlucmakiola.calendula.R
|
||||||
|
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The app's single "which calendar" selection list, shared by the event editor
|
||||||
|
* and the .ics import screen. Renders the same grouped-card system as the
|
||||||
|
* calendar-manager screen: a category header per source — the device chip for
|
||||||
|
* the app's own calendars, the owning app's launcher icon for each synced
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ColumnScope.CalendarPickerGroups(
|
||||||
|
calendars: List<CalendarSource>,
|
||||||
|
selectedId: Long?,
|
||||||
|
onSelect: (Long) -> Unit,
|
||||||
|
) {
|
||||||
|
val local = remember(calendars) { calendars.filter { it.isLocal } }
|
||||||
|
val syncedGroups = remember(calendars) {
|
||||||
|
calendars.filterNot { it.isLocal }
|
||||||
|
.groupBy { it.accountName.ifBlank { it.accountType } }
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (local.isNotEmpty()) {
|
||||||
|
CalendarPickerGroup(
|
||||||
|
title = stringResource(R.string.calendars_local_header),
|
||||||
|
leading = { LeadingAvatar(Icons.Default.PhoneAndroid) },
|
||||||
|
calendars = local,
|
||||||
|
selectedId = selectedId,
|
||||||
|
onSelect = onSelect,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
syncedGroups.forEachIndexed { index, (account, cals) ->
|
||||||
|
if (local.isNotEmpty() || index > 0) Spacer(Modifier.height(16.dp))
|
||||||
|
CalendarPickerGroup(
|
||||||
|
title = account,
|
||||||
|
leading = { SourceLogo(cals.first().accountType) },
|
||||||
|
calendars = cals,
|
||||||
|
selectedId = selectedId,
|
||||||
|
onSelect = onSelect,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One account's category header (avatar + name) atop its selectable calendars. */
|
||||||
|
@Composable
|
||||||
|
private fun CalendarPickerGroup(
|
||||||
|
title: String,
|
||||||
|
leading: @Composable () -> Unit,
|
||||||
|
calendars: List<CalendarSource>,
|
||||||
|
selectedId: Long?,
|
||||||
|
onSelect: (Long) -> Unit,
|
||||||
|
) {
|
||||||
|
GroupedRow(
|
||||||
|
title = title,
|
||||||
|
position = Position.Top,
|
||||||
|
leading = leading,
|
||||||
|
)
|
||||||
|
calendars.forEachIndexed { index, calendar ->
|
||||||
|
val isSelected = calendar.id == selectedId
|
||||||
|
GroupedRow(
|
||||||
|
title = calendar.displayName,
|
||||||
|
position = if (index == calendars.lastIndex) Position.Bottom else Position.Middle,
|
||||||
|
selected = isSelected,
|
||||||
|
leading = { CalendarColorChip(calendar.color) },
|
||||||
|
trailing = if (isSelected) {
|
||||||
|
{
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
onClick = { onSelect(calendar.id) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The source app's launcher icon (Google Calendar, DAVx5, Nextcloud, …) as a
|
||||||
|
* round 40dp chip, so each synced account is recognisable at a glance. We load
|
||||||
|
* whatever app owns the account from [android.content.pm.PackageManager] rather
|
||||||
|
* than bundling brand logos — always accurate, nothing to license. Falls back to
|
||||||
|
* a neutral cloud chip when no installed app resolves for the account.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun SourceLogo(accountType: String) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val logo = remember(accountType) { sourceAppLogo(context, accountType) }
|
||||||
|
if (logo != null) {
|
||||||
|
Image(
|
||||||
|
bitmap = logo,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LeadingAvatar(Icons.Default.Cloud)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Neutral circular chip carrying an arbitrary icon (e.g. the local-device mark). */
|
||||||
|
@Composable
|
||||||
|
fun LeadingAvatar(icon: ImageVector) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainerHighest),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.size(22.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The launcher icon of the app backing [accountType], preferring the human-facing app. */
|
||||||
|
private fun sourceAppLogo(context: Context, accountType: String): ImageBitmap? {
|
||||||
|
val pm = context.packageManager
|
||||||
|
val candidates = buildList {
|
||||||
|
curatedSourcePackage(accountType)?.let { add(it) }
|
||||||
|
AccountManager.get(context).authenticatorTypes
|
||||||
|
.firstOrNull { it.type.equals(accountType, ignoreCase = true) }
|
||||||
|
?.packageName
|
||||||
|
?.let { add(it) }
|
||||||
|
}
|
||||||
|
for (pkg in candidates) {
|
||||||
|
val bitmap = runCatching { pm.getApplicationIcon(pkg).toBitmap() }.getOrNull()
|
||||||
|
if (bitmap != null) return bitmap.asImageBitmap()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Preferred app for account types whose authenticator isn't the app to open. */
|
||||||
|
internal fun curatedSourcePackage(accountType: String): String? = when {
|
||||||
|
accountType.equals("com.google", ignoreCase = true) -> "com.google.android.calendar"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
@@ -122,6 +122,7 @@ import de.jeanlucmakiola.calendula.ui.common.calendarExpandEnter
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.predictiveBack
|
import de.jeanlucmakiola.calendula.ui.common.predictiveBack
|
||||||
import de.jeanlucmakiola.calendula.data.calendar.CalendarColorPalette
|
import de.jeanlucmakiola.calendula.data.calendar.CalendarColorPalette
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip
|
import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.CalendarPickerGroups
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarDatePickerDialog
|
import de.jeanlucmakiola.calendula.ui.common.CalendarDatePickerDialog
|
||||||
import de.jeanlucmakiola.calendula.ui.common.ColorSwatchRow
|
import de.jeanlucmakiola.calendula.ui.common.ColorSwatchRow
|
||||||
import de.jeanlucmakiola.calendula.ui.common.DialogAmountField
|
import de.jeanlucmakiola.calendula.ui.common.DialogAmountField
|
||||||
@@ -1975,50 +1976,14 @@ private fun CalendarPicker(
|
|||||||
onSelect: (Long) -> Unit,
|
onSelect: (Long) -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
) {
|
) {
|
||||||
// Group by owning account (name, else type, else the calendar's own name),
|
|
||||||
// preserving the provider's order within and across groups — the same
|
|
||||||
// grouping [groupByAccount] applies for the drawer filter.
|
|
||||||
val groups = remember(calendars) {
|
|
||||||
calendars
|
|
||||||
.groupBy {
|
|
||||||
it.accountName.takeIf(String::isNotBlank)
|
|
||||||
?: it.accountType.takeIf(String::isNotBlank)
|
|
||||||
?: it.displayName
|
|
||||||
}
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
FullScreenPicker(
|
FullScreenPicker(
|
||||||
title = stringResource(R.string.event_detail_calendar),
|
title = stringResource(R.string.event_detail_calendar),
|
||||||
onDismiss = onDismiss,
|
onDismiss = onDismiss,
|
||||||
) {
|
) {
|
||||||
groups.forEach { (account, cals) ->
|
CalendarPickerGroups(
|
||||||
Text(
|
calendars = calendars,
|
||||||
text = account,
|
selectedId = selectedId,
|
||||||
style = MaterialTheme.typography.labelLarge,
|
onSelect = onSelect,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
)
|
||||||
modifier = Modifier.padding(start = 24.dp, end = 24.dp, top = 16.dp, bottom = 4.dp),
|
|
||||||
)
|
|
||||||
cals.forEachIndexed { index, calendar ->
|
|
||||||
val isSelected = calendar.id == selectedId
|
|
||||||
GroupedRow(
|
|
||||||
title = calendar.displayName,
|
|
||||||
position = positionOf(index, cals.size),
|
|
||||||
selected = isSelected,
|
|
||||||
leading = { CalendarColorChip(calendar.color) },
|
|
||||||
trailing = if (isSelected) {
|
|
||||||
{
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Check,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
onClick = { onSelect(calendar.id) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Check
|
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
@@ -39,9 +38,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import de.jeanlucmakiola.calendula.R
|
import de.jeanlucmakiola.calendula.R
|
||||||
import de.jeanlucmakiola.calendula.domain.EventForm
|
import de.jeanlucmakiola.calendula.domain.EventForm
|
||||||
import de.jeanlucmakiola.calendula.domain.ics.IcsParseWarning
|
import de.jeanlucmakiola.calendula.domain.ics.IcsParseWarning
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip
|
import de.jeanlucmakiola.calendula.ui.common.CalendarPickerGroups
|
||||||
import de.jeanlucmakiola.calendula.ui.common.GroupedRow
|
|
||||||
import de.jeanlucmakiola.calendula.ui.common.positionOf
|
|
||||||
import de.jeanlucmakiola.calendula.ui.common.predictiveBack
|
import de.jeanlucmakiola.calendula.ui.common.predictiveBack
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,18 +106,6 @@ private fun ManyContent(state: ImportUiState.Many, onImport: (Long) -> Unit) {
|
|||||||
}
|
}
|
||||||
var selected by rememberSaveable { mutableStateOf(state.calendars.first().id) }
|
var selected by rememberSaveable { mutableStateOf(state.calendars.first().id) }
|
||||||
|
|
||||||
// Group by owning account, the same shape the event editor's calendar picker
|
|
||||||
// uses — coloured chip per calendar, a check on the selected one.
|
|
||||||
val groups = remember(state.calendars) {
|
|
||||||
state.calendars
|
|
||||||
.groupBy {
|
|
||||||
it.accountName.takeIf(String::isNotBlank)
|
|
||||||
?: it.accountType.takeIf(String::isNotBlank)
|
|
||||||
?: it.displayName
|
|
||||||
}
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
Modifier.fillMaxSize().verticalScroll(rememberScrollState())
|
Modifier.fillMaxSize().verticalScroll(rememberScrollState())
|
||||||
.padding(vertical = 8.dp),
|
.padding(vertical = 8.dp),
|
||||||
@@ -128,43 +113,13 @@ private fun ManyContent(state: ImportUiState.Many, onImport: (Long) -> Unit) {
|
|||||||
Text(
|
Text(
|
||||||
pluralStringResource(R.plurals.import_event_count, state.events.size, state.events.size),
|
pluralStringResource(R.plurals.import_event_count, state.events.size, state.events.size),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
modifier = Modifier.padding(start = 24.dp, end = 24.dp, top = 8.dp, bottom = 12.dp),
|
||||||
)
|
)
|
||||||
Text(
|
CalendarPickerGroups(
|
||||||
stringResource(R.string.import_target_header),
|
calendars = state.calendars,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
selectedId = selected,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
onSelect = { selected = it },
|
||||||
modifier = Modifier.padding(start = 24.dp, end = 24.dp, bottom = 4.dp),
|
|
||||||
)
|
)
|
||||||
groups.forEach { (account, cals) ->
|
|
||||||
Text(
|
|
||||||
text = account,
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.padding(start = 24.dp, end = 24.dp, top = 16.dp, bottom = 4.dp),
|
|
||||||
)
|
|
||||||
cals.forEachIndexed { index, calendar ->
|
|
||||||
val isSelected = calendar.id == selected
|
|
||||||
GroupedRow(
|
|
||||||
title = calendar.displayName,
|
|
||||||
position = positionOf(index, cals.size),
|
|
||||||
selected = isSelected,
|
|
||||||
leading = { CalendarColorChip(calendar.color) },
|
|
||||||
trailing = if (isSelected) {
|
|
||||||
{
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Check,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
onClick = { selected = calendar.id },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (state.warnings.isNotEmpty()) {
|
if (state.warnings.isNotEmpty()) {
|
||||||
Column(
|
Column(
|
||||||
Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||||
|
|||||||
1
floret-kit
Submodule
1
floret-kit
Submodule
Submodule floret-kit added at d9e4e877cc
Reference in New Issue
Block a user