Compare commits
11 Commits
chore/flor
...
60aa889c57
| Author | SHA1 | Date | |
|---|---|---|---|
| 60aa889c57 | |||
| 3c9767387b | |||
| 1e7ee5b98a | |||
| a82df3f6d0 | |||
| e0e3eb73b9 | |||
| 8a7a0af207 | |||
| 71a652ce3b | |||
| 5fa6eac1ad | |||
| a8aeae5f32 | |||
|
|
5771b603f2 | ||
|
|
5725989aff |
@@ -1,26 +1,14 @@
|
|||||||
package de.jeanlucmakiola.calendula.ui.common
|
package de.jeanlucmakiola.calendula.ui.common
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
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.Check
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.FilledTonalButton
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SegmentedButton
|
|
||||||
import androidx.compose.material3.SegmentedButtonDefaults
|
|
||||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -29,13 +17,12 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.jeanlucmakiola.calendula.R
|
import de.jeanlucmakiola.calendula.R
|
||||||
import de.jeanlucmakiola.floret.components.DialogAmountField
|
import de.jeanlucmakiola.floret.components.CustomAmountEditor
|
||||||
import de.jeanlucmakiola.floret.components.FullScreenPicker
|
import de.jeanlucmakiola.floret.components.FullScreenPicker
|
||||||
import de.jeanlucmakiola.floret.components.GroupedRow
|
import de.jeanlucmakiola.floret.components.GroupedRow
|
||||||
import de.jeanlucmakiola.floret.components.Position
|
import de.jeanlucmakiola.floret.components.Position
|
||||||
@@ -195,56 +182,18 @@ private fun CustomReminderEditor(
|
|||||||
onConfirm: (Int) -> Unit,
|
onConfirm: (Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
val amount = amountText.toIntOrNull()?.takeIf { it in 1..999 }
|
val amount = amountText.toIntOrNull()?.takeIf { it in 1..999 }
|
||||||
Surface(
|
CustomAmountEditor(
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
amountText = amountText,
|
||||||
// A Position.Bottom shape: tight top corners meeting the row, full bottom.
|
onAmountChange = onAmountChange,
|
||||||
shape = RoundedCornerShape(topStart = 6.dp, topEnd = 6.dp, bottomStart = 22.dp, bottomEnd = 22.dp),
|
unitLabels = ReminderUnit.entries.map { stringResource(reminderUnitLabel(it)) },
|
||||||
modifier = Modifier
|
selectedUnit = unit.ordinal,
|
||||||
.fillMaxWidth()
|
onUnitChange = { onUnitChange(ReminderUnit.entries[it]) },
|
||||||
.padding(horizontal = 16.dp),
|
preview = amount?.let { reminderLeadTimeLabel(it * unit.minutesFactor) }
|
||||||
) {
|
?: stringResource(R.string.reminder_custom_amount),
|
||||||
Column(
|
setLabel = stringResource(R.string.reminder_custom_set),
|
||||||
modifier = Modifier.padding(16.dp),
|
confirmEnabled = amount != null,
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
onConfirm = { amount?.let { onConfirm(it * unit.minutesFactor) } },
|
||||||
) {
|
)
|
||||||
// Unit toggle first so it stays visible above the keyboard once the
|
|
||||||
// amount field (the bottom row) is focused and scrolled into view.
|
|
||||||
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
ReminderUnit.entries.forEachIndexed { index, entry ->
|
|
||||||
SegmentedButton(
|
|
||||||
selected = unit == entry,
|
|
||||||
onClick = { onUnitChange(entry) },
|
|
||||||
shape = SegmentedButtonDefaults.itemShape(index, ReminderUnit.entries.size),
|
|
||||||
label = { Text(stringResource(reminderUnitLabel(entry))) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Amount, a live preview of the lead time it resolves to, and Set —
|
|
||||||
// all on one row, sitting just above the keyboard.
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
DialogAmountField(
|
|
||||||
value = amountText,
|
|
||||||
onValueChange = onAmountChange,
|
|
||||||
placeholder = "10",
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(16.dp))
|
|
||||||
Text(
|
|
||||||
text = amount?.let { reminderLeadTimeLabel(it * unit.minutesFactor) }
|
|
||||||
?: stringResource(R.string.reminder_custom_amount),
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(16.dp))
|
|
||||||
FilledTonalButton(
|
|
||||||
onClick = { amount?.let { onConfirm(it * unit.minutesFactor) } },
|
|
||||||
enabled = amount != null,
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.reminder_custom_set))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A short explanatory paragraph shown under a picker's title, above the rows. */
|
/** A short explanatory paragraph shown under a picker's title, above the rows. */
|
||||||
@@ -364,41 +313,136 @@ private fun CustomDaysEditor(
|
|||||||
) {
|
) {
|
||||||
val days = amountText.toIntOrNull()
|
val days = amountText.toIntOrNull()
|
||||||
?.takeIf { it in AgendaRange.MIN_CUSTOM_DAYS..AgendaRange.MAX_CUSTOM_DAYS }
|
?.takeIf { it in AgendaRange.MIN_CUSTOM_DAYS..AgendaRange.MAX_CUSTOM_DAYS }
|
||||||
Surface(
|
CustomAmountEditor(
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
amountText = amountText,
|
||||||
shape = RoundedCornerShape(topStart = 6.dp, topEnd = 6.dp, bottomStart = 22.dp, bottomEnd = 22.dp),
|
onAmountChange = onAmountChange,
|
||||||
modifier = Modifier
|
placeholder = "30",
|
||||||
.fillMaxWidth()
|
preview = days?.let { pluralStringResource(R.plurals.agenda_range_days, it, it) }
|
||||||
.padding(horizontal = 16.dp),
|
?: stringResource(R.string.agenda_range_custom_hint),
|
||||||
) {
|
setLabel = stringResource(R.string.reminder_custom_set),
|
||||||
Row(
|
confirmEnabled = days != null,
|
||||||
modifier = Modifier.padding(16.dp),
|
onConfirm = { days?.let(onConfirm) },
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Snooze-duration picker, full-screen and **single-select**: the [presets]
|
||||||
|
* (whole-minute delays) each sit as a checkmark row, with a "Custom" row that
|
||||||
|
* expands an inline amount field plus a Minutes/Hours unit toggle to enter an
|
||||||
|
* arbitrary delay. Mirrors [AgendaRangePicker]'s custom-expand pattern; picking
|
||||||
|
* a preset or confirming a custom value applies via [onSelect] and closes.
|
||||||
|
* [label] renders a delay in minutes as a duration ("10 minutes", "1 hour") and
|
||||||
|
* is reused for both the rows and the custom preview.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun SnoozeDurationPicker(
|
||||||
|
title: String,
|
||||||
|
presets: List<Int>,
|
||||||
|
selected: Int,
|
||||||
|
label: @Composable (Int) -> String,
|
||||||
|
onSelect: (Int) -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
val customSelected = selected !in presets
|
||||||
|
val rowCount = presets.size + 1 // + the custom row
|
||||||
|
|
||||||
|
var customExpanded by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var amountText by rememberSaveable {
|
||||||
|
mutableStateOf(if (customSelected) snoozeCustomAmount(selected).toString() else "")
|
||||||
|
}
|
||||||
|
var unit by rememberSaveable {
|
||||||
|
mutableStateOf(if (customSelected) snoozeCustomUnit(selected) else ReminderUnit.Minutes)
|
||||||
|
}
|
||||||
|
|
||||||
|
FullScreenPicker(title = title, onDismiss = onDismiss, predictiveBack = true) {
|
||||||
|
presets.forEachIndexed { index, minute ->
|
||||||
|
val isSelected = minute == selected
|
||||||
|
GroupedRow(
|
||||||
|
title = label(minute),
|
||||||
|
position = positionOf(index, rowCount),
|
||||||
|
selected = isSelected,
|
||||||
|
trailing = if (isSelected) {
|
||||||
|
{ SelectedCheck() }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
onSelect(minute)
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// The Custom row connects downward into the editor card when expanded, so
|
||||||
|
// the two read as one grouped container (the shared custom-expand pattern).
|
||||||
|
GroupedRow(
|
||||||
|
title = if (customSelected) label(selected) else stringResource(R.string.event_edit_reminder_custom),
|
||||||
|
position = if (customExpanded) Position.Top else positionOf(presets.size, rowCount),
|
||||||
|
selected = customSelected,
|
||||||
|
trailing = if (customSelected) {
|
||||||
|
{ SelectedCheck() }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
onClick = { customExpanded = !customExpanded },
|
||||||
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = customExpanded,
|
||||||
|
enter = expandEnter(),
|
||||||
|
exit = collapseExit(),
|
||||||
) {
|
) {
|
||||||
DialogAmountField(
|
CustomSnoozeEditor(
|
||||||
value = amountText,
|
amountText = amountText,
|
||||||
onValueChange = onAmountChange,
|
onAmountChange = { amountText = it },
|
||||||
placeholder = "30",
|
unit = unit,
|
||||||
|
onUnitChange = { unit = it },
|
||||||
|
label = label,
|
||||||
|
onConfirm = { minutes ->
|
||||||
|
onSelect(minutes)
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(16.dp))
|
|
||||||
Text(
|
|
||||||
text = days?.let { pluralStringResource(R.plurals.agenda_range_days, it, it) }
|
|
||||||
?: stringResource(R.string.agenda_range_custom_hint),
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(16.dp))
|
|
||||||
FilledTonalButton(
|
|
||||||
onClick = { days?.let(onConfirm) },
|
|
||||||
enabled = days != null,
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.reminder_custom_set))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whole hours if the delay divides evenly, else minutes. */
|
||||||
|
private fun snoozeCustomUnit(minutes: Int): ReminderUnit =
|
||||||
|
if (minutes % ReminderUnit.Hours.minutesFactor == 0) ReminderUnit.Hours else ReminderUnit.Minutes
|
||||||
|
|
||||||
|
private fun snoozeCustomAmount(minutes: Int): Int =
|
||||||
|
if (minutes % ReminderUnit.Hours.minutesFactor == 0) minutes / ReminderUnit.Hours.minutesFactor else minutes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The expanded "Custom" snooze editor: a tonal card connected to the Custom row
|
||||||
|
* above it. A Minutes/Hours unit toggle, an amount field with a live preview of
|
||||||
|
* the delay it resolves to, and a tonal confirm enabled only for a valid 1–999
|
||||||
|
* amount. [onConfirm] receives the final delay in minutes.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun CustomSnoozeEditor(
|
||||||
|
amountText: String,
|
||||||
|
onAmountChange: (String) -> Unit,
|
||||||
|
unit: ReminderUnit,
|
||||||
|
onUnitChange: (ReminderUnit) -> Unit,
|
||||||
|
label: @Composable (Int) -> String,
|
||||||
|
onConfirm: (Int) -> Unit,
|
||||||
|
) {
|
||||||
|
val units = remember { listOf(ReminderUnit.Minutes, ReminderUnit.Hours) }
|
||||||
|
val amount = amountText.toIntOrNull()?.takeIf { it in 1..999 }
|
||||||
|
CustomAmountEditor(
|
||||||
|
amountText = amountText,
|
||||||
|
onAmountChange = onAmountChange,
|
||||||
|
unitLabels = units.map { stringResource(reminderUnitLabel(it)) },
|
||||||
|
selectedUnit = units.indexOf(unit).coerceAtLeast(0),
|
||||||
|
onUnitChange = { onUnitChange(units[it]) },
|
||||||
|
preview = amount?.let { label(it * unit.minutesFactor) }
|
||||||
|
?: stringResource(R.string.reminder_custom_amount),
|
||||||
|
setLabel = stringResource(R.string.reminder_custom_set),
|
||||||
|
confirmEnabled = amount != null,
|
||||||
|
onConfirm = { amount?.let { onConfirm(it * unit.minutesFactor) } },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** Human label for an [AgendaRange] (used by the picker rows and settings summary). */
|
/** Human label for an [AgendaRange] (used by the picker rows and settings summary). */
|
||||||
@Composable
|
@Composable
|
||||||
fun agendaRangeLabel(range: AgendaRange): String = when (range) {
|
fun agendaRangeLabel(range: AgendaRange): String = when (range) {
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ import de.jeanlucmakiola.floret.components.GroupedRow
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.MILLIS_PER_DAY
|
import de.jeanlucmakiola.calendula.ui.common.MILLIS_PER_DAY
|
||||||
import de.jeanlucmakiola.floret.components.InlineTextField
|
import de.jeanlucmakiola.floret.components.InlineTextField
|
||||||
import de.jeanlucmakiola.floret.components.OptionCard
|
import de.jeanlucmakiola.floret.components.OptionCard
|
||||||
|
import de.jeanlucmakiola.floret.components.OptionPicker
|
||||||
import de.jeanlucmakiola.floret.components.Position
|
import de.jeanlucmakiola.floret.components.Position
|
||||||
import de.jeanlucmakiola.floret.components.positionOf
|
import de.jeanlucmakiola.floret.components.positionOf
|
||||||
import de.jeanlucmakiola.calendula.ui.common.REMINDER_PRESETS
|
import de.jeanlucmakiola.calendula.ui.common.REMINDER_PRESETS
|
||||||
@@ -345,29 +346,29 @@ private fun SaveConflictDialog(
|
|||||||
onDiscard: () -> Unit,
|
onDiscard: () -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
) {
|
) {
|
||||||
AlertDialog(
|
FullScreenPicker(
|
||||||
onDismissRequest = onDismiss,
|
title = stringResource(R.string.event_edit_conflict_title),
|
||||||
title = { Text(stringResource(R.string.event_edit_conflict_title)) },
|
onDismiss = onDismiss,
|
||||||
text = {
|
) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Text(
|
||||||
Text(stringResource(R.string.event_edit_conflict_body))
|
text = stringResource(R.string.event_edit_conflict_body),
|
||||||
Spacer(Modifier.height(4.dp))
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
OptionCard(
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
label = stringResource(R.string.event_edit_conflict_overwrite),
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||||
supportingText = stringResource(R.string.event_edit_conflict_overwrite_hint),
|
)
|
||||||
onClick = onOverwrite,
|
GroupedRow(
|
||||||
)
|
title = stringResource(R.string.event_edit_conflict_overwrite),
|
||||||
OptionCard(
|
summary = stringResource(R.string.event_edit_conflict_overwrite_hint),
|
||||||
label = stringResource(R.string.event_edit_conflict_discard),
|
position = positionOf(0, 2),
|
||||||
supportingText = stringResource(R.string.event_edit_conflict_discard_hint),
|
onClick = onOverwrite,
|
||||||
onClick = onDiscard,
|
)
|
||||||
)
|
GroupedRow(
|
||||||
}
|
title = stringResource(R.string.event_edit_conflict_discard),
|
||||||
},
|
summary = stringResource(R.string.event_edit_conflict_discard_hint),
|
||||||
confirmButton = {
|
position = positionOf(1, 2),
|
||||||
TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) }
|
onClick = onDiscard,
|
||||||
},
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1064,24 +1065,19 @@ private fun FieldPickerDialog(
|
|||||||
onSelect: (EventFormField) -> Unit,
|
onSelect: (EventFormField) -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
) {
|
) {
|
||||||
AlertDialog(
|
FullScreenPicker(
|
||||||
onDismissRequest = onDismiss,
|
title = stringResource(R.string.event_edit_more_fields),
|
||||||
title = { Text(stringResource(R.string.event_edit_more_fields)) },
|
onDismiss = onDismiss,
|
||||||
text = {
|
) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
hiddenFields.forEachIndexed { index, field ->
|
||||||
hiddenFields.forEach { field ->
|
GroupedRow(
|
||||||
OptionCard(
|
title = stringResource(eventFormFieldLabel(field)),
|
||||||
label = stringResource(eventFormFieldLabel(field)),
|
position = positionOf(index, hiddenFields.size),
|
||||||
onClick = { onSelect(field) },
|
leading = { Icon(imageVector = eventFormFieldIcon(field), contentDescription = null) },
|
||||||
icon = eventFormFieldIcon(field),
|
onClick = { onSelect(field) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) }
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Quick-pick lead times offered as chips in the reminder dialog. */
|
/** Quick-pick lead times offered as chips in the reminder dialog. */
|
||||||
@@ -1106,41 +1102,10 @@ private fun ReminderPickerDialog(
|
|||||||
?.takeIf { it in 1..999 }
|
?.takeIf { it in 1..999 }
|
||||||
?.let { it * unit.minutesFactor }
|
?.let { it * unit.minutesFactor }
|
||||||
|
|
||||||
AlertDialog(
|
FullScreenPicker(
|
||||||
onDismissRequest = onDismiss,
|
title = stringResource(R.string.event_edit_add_reminder),
|
||||||
title = { Text(stringResource(R.string.event_edit_add_reminder)) },
|
onDismiss = onDismiss,
|
||||||
text = {
|
actions = {
|
||||||
if (!customMode) {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
REMINDER_QUICK_PICKS.filterNot { it in alreadyChosen }.forEach { minutes ->
|
|
||||||
OptionCard(
|
|
||||||
label = reminderLabel(minutes),
|
|
||||||
onClick = { onSelect(minutes) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
OptionCard(
|
|
||||||
label = stringResource(R.string.event_edit_reminder_custom),
|
|
||||||
onClick = { customMode = true },
|
|
||||||
labelColor = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
DialogAmountField(
|
|
||||||
value = amountText,
|
|
||||||
onValueChange = { amountText = it },
|
|
||||||
placeholder = "10",
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(12.dp))
|
|
||||||
DialogUnitDropdown(
|
|
||||||
label = stringResource(reminderUnitLabel(unit)),
|
|
||||||
entries = ReminderUnit.entries.map { stringResource(reminderUnitLabel(it)) },
|
|
||||||
onPick = { unit = ReminderUnit.entries[it] },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
// The quick-pick list adds on tap; only the custom step needs Add.
|
// The quick-pick list adds on tap; only the custom step needs Add.
|
||||||
if (customMode) {
|
if (customMode) {
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -1149,10 +1114,41 @@ private fun ReminderPickerDialog(
|
|||||||
) { Text(stringResource(R.string.event_edit_add)) }
|
) { Text(stringResource(R.string.event_edit_add)) }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
) {
|
||||||
TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) }
|
if (!customMode) {
|
||||||
},
|
val presets = REMINDER_QUICK_PICKS.filterNot { it in alreadyChosen }
|
||||||
)
|
val rowCount = presets.size + 1
|
||||||
|
presets.forEachIndexed { index, minutes ->
|
||||||
|
GroupedRow(
|
||||||
|
title = reminderLabel(minutes),
|
||||||
|
position = positionOf(index, rowCount),
|
||||||
|
onClick = { onSelect(minutes) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
GroupedRow(
|
||||||
|
title = stringResource(R.string.event_edit_reminder_custom),
|
||||||
|
position = positionOf(rowCount - 1, rowCount),
|
||||||
|
onClick = { customMode = true },
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||||
|
) {
|
||||||
|
DialogAmountField(
|
||||||
|
value = amountText,
|
||||||
|
onValueChange = { amountText = it },
|
||||||
|
placeholder = "10",
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(12.dp))
|
||||||
|
DialogUnitDropdown(
|
||||||
|
label = stringResource(reminderUnitLabel(unit)),
|
||||||
|
entries = ReminderUnit.entries.map { stringResource(reminderUnitLabel(it)) },
|
||||||
|
onPick = { unit = ReminderUnit.entries[it] },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** How a custom recurrence ends; mirrors [RecurrenceEnd] in saveable form. */
|
/** How a custom recurrence ends; mirrors [RecurrenceEnd] in saveable form. */
|
||||||
@@ -1222,109 +1218,10 @@ private fun RecurrencePickerDialog(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
AlertDialog(
|
FullScreenPicker(
|
||||||
onDismissRequest = onDismiss,
|
title = stringResource(R.string.event_detail_recurrence),
|
||||||
title = { Text(stringResource(R.string.event_detail_recurrence)) },
|
onDismiss = onDismiss,
|
||||||
text = {
|
actions = {
|
||||||
if (!customMode) {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
OptionCard(
|
|
||||||
label = stringResource(R.string.event_edit_recurrence_none),
|
|
||||||
onClick = { onSelect(null) },
|
|
||||||
selected = current == null,
|
|
||||||
)
|
|
||||||
RecurrenceFreq.entries.forEach { entry ->
|
|
||||||
OptionCard(
|
|
||||||
label = stringResource(recurrencePresetLabel(entry)),
|
|
||||||
onClick = { onSelect(SimpleRecurrence(entry).toRRule()) },
|
|
||||||
selected = isPlainPreset && parsed?.freq == entry,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
OptionCard(
|
|
||||||
label = stringResource(R.string.event_edit_recurrence_custom),
|
|
||||||
onClick = { customMode = true },
|
|
||||||
selected = current != null && !isPlainPreset,
|
|
||||||
labelColor = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.event_edit_recurrence_every),
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(12.dp))
|
|
||||||
DialogAmountField(
|
|
||||||
value = intervalText,
|
|
||||||
onValueChange = { intervalText = it },
|
|
||||||
placeholder = "1",
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(12.dp))
|
|
||||||
DialogUnitDropdown(
|
|
||||||
label = stringResource(recurrenceUnitLabel(freq)),
|
|
||||||
entries = RecurrenceFreq.entries.map {
|
|
||||||
stringResource(recurrenceUnitLabel(it))
|
|
||||||
},
|
|
||||||
onPick = { freq = RecurrenceFreq.entries[it] },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (freq == RecurrenceFreq.Weekly) {
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
WeekdayToggleRow(
|
|
||||||
selected = daysMask.toDaySet(),
|
|
||||||
onToggle = { day -> daysMask = daysMask xor day.toMaskBit() },
|
|
||||||
locale = locale,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.event_edit_recurrence_ends),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
OptionCard(
|
|
||||||
label = stringResource(R.string.event_edit_recurrence_end_never),
|
|
||||||
onClick = { endMode = RecurrenceEndMode.Never },
|
|
||||||
selected = endMode == RecurrenceEndMode.Never,
|
|
||||||
)
|
|
||||||
OptionCard(
|
|
||||||
label = stringResource(R.string.event_edit_recurrence_end_until),
|
|
||||||
onClick = {
|
|
||||||
endMode = RecurrenceEndMode.Until
|
|
||||||
showUntilPicker = true
|
|
||||||
},
|
|
||||||
supportingText = untilDate?.let {
|
|
||||||
remember(it, locale) {
|
|
||||||
DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
|
|
||||||
.withLocale(locale).format(it.toJavaLocalDate())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selected = endMode == RecurrenceEndMode.Until,
|
|
||||||
)
|
|
||||||
OptionCard(
|
|
||||||
label = stringResource(R.string.event_edit_recurrence_end_count),
|
|
||||||
onClick = { endMode = RecurrenceEndMode.Count },
|
|
||||||
selected = endMode == RecurrenceEndMode.Count,
|
|
||||||
)
|
|
||||||
if (endMode == RecurrenceEndMode.Count) {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
DialogAmountField(
|
|
||||||
value = countText,
|
|
||||||
onValueChange = { countText = it },
|
|
||||||
placeholder = "10",
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(12.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.event_edit_recurrence_times),
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
// The preset list applies on tap; only the custom step needs OK.
|
// The preset list applies on tap; only the custom step needs OK.
|
||||||
if (customMode) {
|
if (customMode) {
|
||||||
TextButton(
|
TextButton(
|
||||||
@@ -1333,10 +1230,113 @@ private fun RecurrencePickerDialog(
|
|||||||
) { Text(stringResource(R.string.dialog_ok)) }
|
) { Text(stringResource(R.string.dialog_ok)) }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
) {
|
||||||
TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) }
|
if (!customMode) {
|
||||||
},
|
val rowCount = RecurrenceFreq.entries.size + 2
|
||||||
)
|
GroupedRow(
|
||||||
|
title = stringResource(R.string.event_edit_recurrence_none),
|
||||||
|
position = positionOf(0, rowCount),
|
||||||
|
selected = current == null,
|
||||||
|
onClick = { onSelect(null) },
|
||||||
|
)
|
||||||
|
RecurrenceFreq.entries.forEachIndexed { index, entry ->
|
||||||
|
GroupedRow(
|
||||||
|
title = stringResource(recurrencePresetLabel(entry)),
|
||||||
|
position = positionOf(index + 1, rowCount),
|
||||||
|
selected = isPlainPreset && parsed?.freq == entry,
|
||||||
|
onClick = { onSelect(SimpleRecurrence(entry).toRRule()) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
GroupedRow(
|
||||||
|
title = stringResource(R.string.event_edit_recurrence_custom),
|
||||||
|
position = positionOf(rowCount - 1, rowCount),
|
||||||
|
selected = current != null && !isPlainPreset,
|
||||||
|
onClick = { customMode = true },
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.event_edit_recurrence_every),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(12.dp))
|
||||||
|
DialogAmountField(
|
||||||
|
value = intervalText,
|
||||||
|
onValueChange = { intervalText = it },
|
||||||
|
placeholder = "1",
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(12.dp))
|
||||||
|
DialogUnitDropdown(
|
||||||
|
label = stringResource(recurrenceUnitLabel(freq)),
|
||||||
|
entries = RecurrenceFreq.entries.map {
|
||||||
|
stringResource(recurrenceUnitLabel(it))
|
||||||
|
},
|
||||||
|
onPick = { freq = RecurrenceFreq.entries[it] },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (freq == RecurrenceFreq.Weekly) {
|
||||||
|
WeekdayToggleRow(
|
||||||
|
selected = daysMask.toDaySet(),
|
||||||
|
onToggle = { day -> daysMask = daysMask xor day.toMaskBit() },
|
||||||
|
locale = locale,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.event_edit_recurrence_ends),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
GroupedRow(
|
||||||
|
title = stringResource(R.string.event_edit_recurrence_end_never),
|
||||||
|
position = positionOf(0, 3),
|
||||||
|
selected = endMode == RecurrenceEndMode.Never,
|
||||||
|
onClick = { endMode = RecurrenceEndMode.Never },
|
||||||
|
)
|
||||||
|
GroupedRow(
|
||||||
|
title = stringResource(R.string.event_edit_recurrence_end_until),
|
||||||
|
summary = untilDate?.let {
|
||||||
|
remember(it, locale) {
|
||||||
|
DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
|
||||||
|
.withLocale(locale).format(it.toJavaLocalDate())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
position = positionOf(1, 3),
|
||||||
|
selected = endMode == RecurrenceEndMode.Until,
|
||||||
|
onClick = {
|
||||||
|
endMode = RecurrenceEndMode.Until
|
||||||
|
showUntilPicker = true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
GroupedRow(
|
||||||
|
title = stringResource(R.string.event_edit_recurrence_end_count),
|
||||||
|
position = positionOf(2, 3),
|
||||||
|
selected = endMode == RecurrenceEndMode.Count,
|
||||||
|
onClick = { endMode = RecurrenceEndMode.Count },
|
||||||
|
)
|
||||||
|
if (endMode == RecurrenceEndMode.Count) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 4.dp),
|
||||||
|
) {
|
||||||
|
DialogAmountField(
|
||||||
|
value = countText,
|
||||||
|
onValueChange = { countText = it },
|
||||||
|
placeholder = "10",
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(12.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.event_edit_recurrence_times),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (showUntilPicker) {
|
if (showUntilPicker) {
|
||||||
CalendarDatePickerDialog(
|
CalendarDatePickerDialog(
|
||||||
@@ -1701,24 +1701,14 @@ private fun VisibilityPickerDialog(
|
|||||||
onSelect: (AccessLevel) -> Unit,
|
onSelect: (AccessLevel) -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
) {
|
) {
|
||||||
AlertDialog(
|
OptionPicker(
|
||||||
onDismissRequest = onDismiss,
|
title = stringResource(R.string.event_edit_visibility),
|
||||||
title = { Text(stringResource(R.string.event_edit_visibility)) },
|
options = AccessLevel.entries.toList(),
|
||||||
text = {
|
selected = selected,
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
label = { stringResource(accessLevelLabel(it)) },
|
||||||
AccessLevel.entries.forEach { level ->
|
onSelect = onSelect,
|
||||||
OptionCard(
|
onDismiss = onDismiss,
|
||||||
label = stringResource(accessLevelLabel(level)),
|
leading = { Icon(imageVector = accessLevelIcon(it), contentDescription = null) },
|
||||||
onClick = { onSelect(level) },
|
|
||||||
icon = accessLevelIcon(level),
|
|
||||||
selected = level == selected,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) }
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1741,51 +1731,48 @@ private fun ColorPickerDialog(
|
|||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val dark = isSystemInDarkTheme()
|
val dark = isSystemInDarkTheme()
|
||||||
AlertDialog(
|
FullScreenPicker(
|
||||||
onDismissRequest = onDismiss,
|
title = stringResource(R.string.event_edit_color),
|
||||||
title = { Text(stringResource(R.string.event_edit_color)) },
|
onDismiss = onDismiss,
|
||||||
text = {
|
actions = {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
if (hasExplicitColor) {
|
||||||
if (palette.isNotEmpty()) {
|
|
||||||
ColorSwatchRow(
|
|
||||||
colors = palette.map { it.argb },
|
|
||||||
selected = selected,
|
|
||||||
onSelect = { argb ->
|
|
||||||
palette.firstOrNull { it.argb == argb }
|
|
||||||
?.let { onPickKey(it.key, it.argb) }
|
|
||||||
},
|
|
||||||
dark = dark,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ColorSwatchRow(
|
|
||||||
colors = CalendarColorPalette.all,
|
|
||||||
selected = selected,
|
|
||||||
onSelect = onPickRaw,
|
|
||||||
dark = dark,
|
|
||||||
)
|
|
||||||
if (syncWarning) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.event_edit_color_sync_warning),
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) }
|
|
||||||
},
|
|
||||||
dismissButton = if (hasExplicitColor) {
|
|
||||||
{
|
|
||||||
TextButton(onClick = onClear) {
|
TextButton(onClick = onClear) {
|
||||||
Text(stringResource(R.string.event_edit_color_reset))
|
Text(stringResource(R.string.event_edit_color_reset))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
},
|
||||||
)
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||||
|
) {
|
||||||
|
if (palette.isNotEmpty()) {
|
||||||
|
ColorSwatchRow(
|
||||||
|
colors = palette.map { it.argb },
|
||||||
|
selected = selected,
|
||||||
|
onSelect = { argb ->
|
||||||
|
palette.firstOrNull { it.argb == argb }
|
||||||
|
?.let { onPickKey(it.key, it.argb) }
|
||||||
|
},
|
||||||
|
dark = dark,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ColorSwatchRow(
|
||||||
|
colors = CalendarColorPalette.all,
|
||||||
|
selected = selected,
|
||||||
|
onSelect = onPickRaw,
|
||||||
|
dark = dark,
|
||||||
|
)
|
||||||
|
if (syncWarning) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.event_edit_color_sync_warning),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun accessLevelIcon(level: AccessLevel): ImageVector = when (level) {
|
private fun accessLevelIcon(level: AccessLevel): ImageVector = when (level) {
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ import de.jeanlucmakiola.calendula.ui.common.ReminderDefaultPicker
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.TimePickerAlert
|
import de.jeanlucmakiola.calendula.ui.common.TimePickerAlert
|
||||||
import de.jeanlucmakiola.floret.components.positionOf
|
import de.jeanlucmakiola.floret.components.positionOf
|
||||||
import de.jeanlucmakiola.calendula.ui.common.reminderLeadTimeLabel
|
import de.jeanlucmakiola.calendula.ui.common.reminderLeadTimeLabel
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.SnoozeDurationPicker
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
||||||
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
||||||
import de.jeanlucmakiola.calendula.ui.common.eventFormFieldIcon
|
import de.jeanlucmakiola.calendula.ui.common.eventFormFieldIcon
|
||||||
@@ -160,7 +161,7 @@ private enum class ChipAccent { Neutral, Primary, Tertiary }
|
|||||||
* Settings (M4), restructured in v2.3 into a category hub with sub-screens.
|
* Settings (M4), restructured in v2.3 into a category hub with sub-screens.
|
||||||
* Both the hub and the sub-screens use a collapsing [LargeTopAppBar] and the
|
* Both the hub and the sub-screens use a collapsing [LargeTopAppBar] and the
|
||||||
* grouped-row card system. Calendars opens the separate manager hoisted in
|
* grouped-row card system. Calendars opens the separate manager hoisted in
|
||||||
* [CalendarHost]; Language opens an inline OptionCard dialog; About is a card
|
* [CalendarHost]; Language opens a full-screen picker; About is a card
|
||||||
* at the top. A full-screen destination; [onBack] pops it.
|
* at the top. A full-screen destination; [onBack] pops it.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
@@ -1174,10 +1175,9 @@ private fun NotificationsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showSnooze) {
|
if (showSnooze) {
|
||||||
OptionPicker(
|
SnoozeDurationPicker(
|
||||||
title = stringResource(R.string.settings_snooze_duration),
|
title = stringResource(R.string.settings_snooze_duration),
|
||||||
predictiveBack = true,
|
presets = SNOOZE_PRESETS,
|
||||||
options = SNOOZE_PRESETS,
|
|
||||||
selected = state.snoozeMinutes,
|
selected = state.snoozeMinutes,
|
||||||
label = { snoozeDurationLabel(it) },
|
label = { snoozeDurationLabel(it) },
|
||||||
onSelect = { viewModel.setSnoozeMinutes(it) },
|
onSelect = { viewModel.setSnoozeMinutes(it) },
|
||||||
|
|||||||
@@ -220,7 +220,7 @@
|
|||||||
<string name="search_action">Suchen</string>
|
<string name="search_action">Suchen</string>
|
||||||
<string name="search_hint">Termine suchen</string>
|
<string name="search_hint">Termine suchen</string>
|
||||||
<string name="search_back">Zurück</string>
|
<string name="search_back">Zurück</string>
|
||||||
<string name="search_clear">Löschen</string>
|
<string name="search_clear">Leeren</string>
|
||||||
<string name="search_idle_hint">Durchsuche deine Termine nach Titel, Ort oder Notizen.</string>
|
<string name="search_idle_hint">Durchsuche deine Termine nach Titel, Ort oder Notizen.</string>
|
||||||
<string name="search_empty">Keine Termine passen zu „%1$s“.</string>
|
<string name="search_empty">Keine Termine passen zu „%1$s“.</string>
|
||||||
<!-- Startbildschirm-Widgets -->
|
<!-- Startbildschirm-Widgets -->
|
||||||
@@ -330,7 +330,7 @@
|
|||||||
<string name="calendars_add">Kalender hinzufügen</string>
|
<string name="calendars_add">Kalender hinzufügen</string>
|
||||||
<string name="calendars_synced_header">Synchronisierte Kalender</string>
|
<string name="calendars_synced_header">Synchronisierte Kalender</string>
|
||||||
<string name="calendars_synced_hint">Diese stammen von Konten auf deinem Gerät. Erstelle und bearbeite sie in der jeweiligen App.</string>
|
<string name="calendars_synced_hint">Diese stammen von Konten auf deinem Gerät. Erstelle und bearbeite sie in der jeweiligen App.</string>
|
||||||
<string name="calendars_manage_in_app">Verwalten</string>
|
<string name="calendars_manage_in_app">In der App verwalten</string>
|
||||||
<string name="calendars_add_account">Konto hinzufügen</string>
|
<string name="calendars_add_account">Konto hinzufügen</string>
|
||||||
<string name="calendars_new_title">Neuer Kalender</string>
|
<string name="calendars_new_title">Neuer Kalender</string>
|
||||||
<string name="calendars_edit_title">Kalender bearbeiten</string>
|
<string name="calendars_edit_title">Kalender bearbeiten</string>
|
||||||
@@ -394,10 +394,10 @@
|
|||||||
<item quantity="other">%d Tage</item>
|
<item quantity="other">%d Tage</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="duration_weeks">
|
<plurals name="duration_weeks">
|
||||||
<item quantity="one">Woche</item>
|
<item quantity="one">%d Woche</item>
|
||||||
<item quantity="other">Wochen</item>
|
<item quantity="other">%d Wochen</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="settings_default_view">Standartansicht</string>
|
<string name="settings_default_view">Standardansicht</string>
|
||||||
<string name="settings_font_headings">Überschriften Schriftart</string>
|
<string name="settings_font_headings">Überschriften Schriftart</string>
|
||||||
<string name="settings_font_system">Systemstandard</string>
|
<string name="settings_font_system">Systemstandard</string>
|
||||||
<string name="font_jetbrains_mono">JetBrains Mono</string>
|
<string name="font_jetbrains_mono">JetBrains Mono</string>
|
||||||
@@ -411,4 +411,67 @@
|
|||||||
<string name="settings_past_events_hide">Ausblenden</string>
|
<string name="settings_past_events_hide">Ausblenden</string>
|
||||||
<string name="settings_agenda_header">Agenda</string>
|
<string name="settings_agenda_header">Agenda</string>
|
||||||
<string name="settings_special_dates_reminders">Erinnerungen</string>
|
<string name="settings_special_dates_reminders">Erinnerungen</string>
|
||||||
|
<string name="settings_font_body">Inhaltsschriftart</string>
|
||||||
|
<string name="font_atkinson_hyperlegible">Atkinson Hyperlegible</string>
|
||||||
|
<string name="font_lora">Lora</string>
|
||||||
|
<string name="settings_font_custom_selected">Benutzerdefinierte Schriftart</string>
|
||||||
|
<string name="settings_section_views">Ansichten</string>
|
||||||
|
<string name="settings_quick_switch_header">Schnellwechselknopf</string>
|
||||||
|
<string name="settings_quick_switch_hint">Wähle aus, durch welche Ansichten du mit dem Knopf oben rechts wechseln möchtest, und ziehe, um sie neu anzuordnen. Deaktivierte Ansichten bleiben über das Navigationsmenü erreichbar.</string>
|
||||||
|
<string name="settings_drawer_order_header">Navigationsmenü</string>
|
||||||
|
<string name="settings_drawer_order_hint">Ziehen, um die im Navigationsmenü aufgelisteten Ansichten neu anzuordnen.</string>
|
||||||
|
<string name="reorder_drag_handle">Zum Neuanordnen ziehen</string>
|
||||||
|
<string name="settings_calendar_reminders_title">Erinnerungen pro Kalender</string>
|
||||||
|
<string name="settings_translate">Hilf beim Übersetzen</string>
|
||||||
|
<string name="settings_translate_hint">Füge eine Sprache auf Weblate hinzu oder verbessere sie</string>
|
||||||
|
<string name="settings_views_subtitle">Schnellwechsellnopf und Menüreihenfolge</string>
|
||||||
|
<string name="settings_special_dates_subtitle">Kontakte-Geburtstage und Jubiläen</string>
|
||||||
|
<string name="settings_section_special_dates">Besondere Termine von Kontakten</string>
|
||||||
|
<string name="settings_special_dates_enable">Kontakttermine anzeigen</string>
|
||||||
|
<string name="settings_special_dates_enable_hint">Spiegle die Geburtstage und andere Termine deiner Kontakte in lokale Kalender. Liest nur Kontakte auf diesem Gerät – es wird nichts hochgeladen und Ihre Kontakte werden nie geändert.</string>
|
||||||
|
<string name="settings_special_dates_type_birthday">Geburtstage</string>
|
||||||
|
<string name="settings_special_dates_type_anniversary">Jahrestage</string>
|
||||||
|
<string name="settings_special_dates_type_custom">Andere Ereignisse</string>
|
||||||
|
<string name="settings_special_dates_template">Titelformat</string>
|
||||||
|
<string name="settings_special_dates_template_hint">Verwende {Name} für den Kontakt und {Jahr} für das Jahr (das Geburtsjahr oder das Anfangsjahr eines Jahrestages; versteckt, wenn unbekannt).</string>
|
||||||
|
<string name="settings_special_dates_show_year">Jahr anzeigen</string>
|
||||||
|
<string name="settings_special_dates_show_year_hint">{year} in Titel einfügen, wenn bekannt</string>
|
||||||
|
<string name="settings_special_dates_sync_now">Jetzt synchronisieren</string>
|
||||||
|
<string name="settings_special_dates_never_synced">Noch nicht synchronisiert</string>
|
||||||
|
<string name="settings_special_dates_last_synced">Zuletzt synchronisiert %1$s</string>
|
||||||
|
<string name="settings_special_dates_calendar_hint">Lege die Farbe und Sichtbarkeit jedes Kalenders in den Kalendereinstellungen fest.</string>
|
||||||
|
<string name="settings_special_dates_paused_title">Pausiert</string>
|
||||||
|
<string name="settings_calendar_reminders_managed_hint">In den besonderen Terminen der Kontakte setzen</string>
|
||||||
|
<string name="settings_special_dates_paused_hint">Calendula kann deine Kontakte nicht mehr lesen, daher werden diese Kalender nicht aktualisiert.</string>
|
||||||
|
<string name="settings_special_dates_disable_title">Kontakttermine deaktivieren?</string>
|
||||||
|
<string name="settings_special_dates_disable_all_message">Dadurch werden die Kontaktterminkalender und ihre Ereignisse gelöscht. Alle von dir hinzugefügten Erinnerungen oder Notizen gehen verloren.</string>
|
||||||
|
<string name="settings_special_dates_grant">Zugriff gewähren</string>
|
||||||
|
<string name="settings_special_dates_disable_type_message">Dadurch werden der Kalender „%1$s“ und seine Ereignisse gelöscht. Alle von dir hinzugefügten Erinnerungen oder Notizen gehen verloren.</string>
|
||||||
|
<string name="settings_special_dates_disable_confirm">Deaktivieren</string>
|
||||||
|
<string name="dialog_save">Speichern</string>
|
||||||
|
<string name="calendars_disable_hint">Deaktiviere einen Kalender, um ihn aus der App zu entfernen – seine Ereignisse, Filter und Auswahlmöglichkeiten. Es wird nichts gelöscht und du kannst ihn hier jederzeit wieder aktivieren.</string>
|
||||||
|
<string name="calendars_show_in_app_a11y">„%1$s“ in der App anzeigen</string>
|
||||||
|
<string name="calendars_account_menu_a11y">Weitere Optionen für %1$s</string>
|
||||||
|
<string name="calendars_enable_all">Alle aktivieren</string>
|
||||||
|
<string name="calendars_disable_all">Alle deaktivieren</string>
|
||||||
|
<string name="calendars_auto_backup">Automatische Sicherung</string>
|
||||||
|
<string name="calendars_auto_backup_hint">Exportiere deine lokalen Kalender regelmäßig als .ics-Datei in einen Ordner.</string>
|
||||||
|
<string name="calendars_auto_backup_folder">Sicherungsordner</string>
|
||||||
|
<string name="calendars_auto_backup_folder_unset">Tippe, um einen Ordner auszuwählen</string>
|
||||||
|
<string name="calendars_auto_backup_interval">Intervall</string>
|
||||||
|
<string name="calendars_auto_backup_every">Alle %1$s</string>
|
||||||
|
<string name="calendars_auto_backup_interval_min">Mindestens 30 Minuten.</string>
|
||||||
|
<string name="calendars_auto_backup_status_never">Noch keine automatische Sicherung</string>
|
||||||
|
<string name="calendars_auto_backup_status_ok">Letzte Sicherung: %1$s</string>
|
||||||
|
<string name="calendars_auto_backup_status_failed">Letzte Sicherung fehlgeschlagen: %1$s</string>
|
||||||
|
<string name="backup_channel_name">Sicherung</string>
|
||||||
|
<string name="backup_channel_description">Warnt, wenn automatische Sicherungen wiederholt fehlschlagen.</string>
|
||||||
|
<string name="backup_failed_title">Automatische Sicherung fehlgeschlagen</string>
|
||||||
|
<string name="backup_failed_text">Calendula konnte die Sicherungsdatei nicht schreiben. Überprüfe den Sicherungsordner in den Einstellungen.</string>
|
||||||
|
<string name="special_dates_calendar_birthday">Geburtstage</string>
|
||||||
|
<string name="special_dates_calendar_anniversary">Jahrestage</string>
|
||||||
|
<string name="special_dates_calendar_custom">Besondere Termine</string>
|
||||||
|
<string name="special_dates_default_title_birthday">Geburtstag von {name} ({year})</string>
|
||||||
|
<string name="special_dates_default_title_anniversary">Jahrestag von {name} ({year})</string>
|
||||||
|
<string name="special_dates_default_title_custom">{name}</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -456,4 +456,11 @@
|
|||||||
<string name="settings_special_dates_disable_type_message">Questo cancella il calendario “%1$s” ed i suoi eventi. Ogni promemoria o note che erano stati aggiunti saranno persi.</string>
|
<string name="settings_special_dates_disable_type_message">Questo cancella il calendario “%1$s” ed i suoi eventi. Ogni promemoria o note che erano stati aggiunti saranno persi.</string>
|
||||||
<string name="settings_special_dates_disable_confirm">Spegni</string>
|
<string name="settings_special_dates_disable_confirm">Spegni</string>
|
||||||
<string name="dialog_save">Salva</string>
|
<string name="dialog_save">Salva</string>
|
||||||
|
<string name="font_atkinson_hyperlegible">Atkinson Hyperlegible</string>
|
||||||
|
<string name="settings_calendar_reminders_managed_hint">Impostato nelle date speciali dei contatti</string>
|
||||||
|
<string name="special_dates_calendar_birthday">Compleanni</string>
|
||||||
|
<string name="special_dates_calendar_anniversary">Anniversari</string>
|
||||||
|
<string name="special_dates_calendar_custom">Date speciali</string>
|
||||||
|
<string name="special_dates_default_title_birthday">Compleanno di {name} ({year})</string>
|
||||||
|
<string name="special_dates_default_title_anniversary">Anniversario di {name} ({year})</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 192 KiB |
25
fastlane/metadata/android/es-ES/full_description.txt
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
Calendula es una aplicación de calendario moderna y de código abierto
|
||||||
|
para Android. Funciona directamente sobre el proveedor de calendario del
|
||||||
|
sistema: cualquier origen sincronizado en tu dispositivo — Nextcloud
|
||||||
|
mediante DAVx5, Google, calendarios locales, suscripciones WebCal —
|
||||||
|
aparece automáticamente, y los cambios que hagas se sincronizan de vuelta
|
||||||
|
del mismo modo.
|
||||||
|
|
||||||
|
Crea, edita y elimina eventos, incluidos los recurrentes con cambios
|
||||||
|
acotados (solo este evento / este y todos los siguientes / toda la serie)
|
||||||
|
y un sencillo selector de repetición. Calendula también entrega los
|
||||||
|
recordatorios de tus eventos como notificaciones: toca una y estarás en
|
||||||
|
el evento.
|
||||||
|
|
||||||
|
Si quieres, muestra en tu calendario los cumpleaños, aniversarios y otras
|
||||||
|
fechas especiales de tus contactos. Tú mismo lo activas; los contactos se
|
||||||
|
leen solo en tu dispositivo, nunca se suben y nunca se modifican —
|
||||||
|
Calendula solo refleja las fechas en calendarios locales que controlas
|
||||||
|
por completo.
|
||||||
|
|
||||||
|
El factor diferenciador es el diseño: auténtico Material 3 Expressive en
|
||||||
|
toda la aplicación, con Dynamic Color, movimiento expresivo y formas
|
||||||
|
expresivas.
|
||||||
|
|
||||||
|
Privacidad: cero telemetría, sin analíticas, sin acceso a la red — tus
|
||||||
|
datos nunca salen del dispositivo.
|
||||||
BIN
fastlane/metadata/android/es-ES/images/icon.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 207 KiB |
1
fastlane/metadata/android/es-ES/short_description.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Un calendario moderno con Material 3 Expressive para Android.
|
||||||
1
fastlane/metadata/android/es-ES/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Calendula
|
||||||
24
fastlane/metadata/android/it-IT/full_description.txt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
Calendula è un'app di calendario moderna e open source per Android.
|
||||||
|
Funziona direttamente sul provider di calendario di sistema: qualsiasi
|
||||||
|
origine sincronizzata sul tuo dispositivo — Nextcloud tramite DAVx5,
|
||||||
|
Google, calendari locali, iscrizioni WebCal — compare automaticamente, e
|
||||||
|
le modifiche che apporti vengono sincronizzate a ritroso allo stesso
|
||||||
|
modo.
|
||||||
|
|
||||||
|
Crea, modifica ed elimina eventi, compresi quelli ricorrenti con
|
||||||
|
modifiche mirate (solo questo evento / questo e tutti i successivi /
|
||||||
|
l'intera serie) e un semplice selettore di ripetizione. Calendula
|
||||||
|
recapita anche i promemoria dei tuoi eventi come notifiche: ne tocchi una
|
||||||
|
e sei subito sull'evento.
|
||||||
|
|
||||||
|
Se vuoi, mostra nel calendario i compleanni, gli anniversari e le altre
|
||||||
|
date speciali dei tuoi contatti. Sei tu ad attivarlo; i contatti vengono
|
||||||
|
letti solo sul tuo dispositivo, non vengono mai caricati e non vengono
|
||||||
|
mai modificati — Calendula si limita a rispecchiare le date in calendari
|
||||||
|
locali che controlli completamente.
|
||||||
|
|
||||||
|
La differenza sta nel design: vero Material 3 Expressive ovunque, con
|
||||||
|
Dynamic Color, animazioni espressive e forme espressive.
|
||||||
|
|
||||||
|
Privacy: zero telemetria, nessuna analisi, nessun accesso alla rete — i
|
||||||
|
tuoi dati non lasciano mai il dispositivo.
|
||||||
BIN
fastlane/metadata/android/it-IT/images/icon.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 202 KiB |
1
fastlane/metadata/android/it-IT/short_description.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Un moderno calendario Material 3 Expressive per Android.
|
||||||
1
fastlane/metadata/android/it-IT/title.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Calendula
|
||||||