Compare commits

...

11 Commits

Author SHA1 Message Date
60aa889c57 Merge pull request 'feat(settings): custom snooze duration, extract editor card to floret-kit' (!67) from feat/custom-snooze-duration into release/v2.15.0
Reviewed-on: #67
2026-07-08 16:10:32 +00:00
3c9767387b feat(settings): custom snooze duration, extract editor card to floret-kit
All checks were successful
CI / ci (pull_request) Successful in 10m14s
The Snooze-duration setting gains a Custom… option: a new single-select
SnoozeDurationPicker keeps the minute presets and adds a Custom row that
expands an amount field with a Minutes/Hours toggle, so any delay is settable
(not just the fixed presets). Closes the settings half of the snooze request.

The three presets-plus-custom editors (reminder default, agenda range, snooze)
now delegate to floret-kit's new CustomAmountEditor instead of each duplicating
the tonal editor card — the app keeps its domain math, strings and labels.

Re-pin the floret-kit submodule to the branch commit carrying CustomAmountEditor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 18:02:13 +02:00
1e7ee5b98a Merge pull request 'release: v2.14.0 — Day view on date-header tap' (!61) from release/v2.14.0 into main
All checks were successful
Release — F-Droid repo + Gitea release / detect (push) Successful in 5s
Release — F-Droid repo + Gitea release / release (push) Successful in 8m51s
Reviewed-on: #61
2026-07-07 14:42:29 +00:00
a82df3f6d0 Merge branch 'main' into release/v2.14.0
All checks were successful
Translations / check (pull_request) Successful in 4s
CI / ci (pull_request) Successful in 5m17s
2026-07-07 14:37:07 +00:00
e0e3eb73b9 chore: pin floret-kit to v0.1.0
All checks were successful
Translations / check (pull_request) Successful in 6s
CI / ci (pull_request) Successful in 9m39s
Move the submodule pin from a loose main commit to the tagged v0.1.0
release (same tree content), so the from-source F-Droid build tracks a
stable, traceable kit version instead of a rolling commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 15:59:28 +02:00
8a7a0af207 chore(fastlane): refresh screenshots, add it/es store listings
All checks were successful
Translations / check (pull_request) Successful in 5s
CI / ci (pull_request) Successful in 9m36s
Recapture all phone screenshots on the Pixel 10 Pro (Android 16) at a
uniform 1280x2856 across every locale, showing the current UI with sample
events: week, month, day, event detail, agenda, and the calendar-access
onboarding. Replaces the old 05-edit shot with 05-agenda (the light theme
has no standalone edit screen) for en-US and de-DE.

Add fully localized store metadata (title, summary, full description) and
per-locale icon for Italian (it-IT) and Spanish (es-ES), matching the
existing en-US/de-DE listings; the app UI is already translated for both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 15:45:44 +02:00
71a652ce3b feat: full-screen selection pickers (retire OptionCard modals, bar actions)
All checks were successful
Translations / check (pull_request) Successful in 6s
CI / ci (pull_request) Successful in 10m8s
Unify the app's "choose one" surfaces on the full-screen picker style
(floret-kit FullScreenPicker/OptionPicker) instead of the OptionCard modal
dialogs, for consistency across the app:

- Event editor: visibility + add-field -> OptionPicker; reminder, recurrence
  rule, and colour -> FullScreenPicker, with the custom-value Add / OK and the
  colour Reset carried in the app-bar via the picker's new `actions` slot; the
  save-conflict chooser -> full-screen.
- The recurring scope choosers stay compact OptionCard popups — saving an edit
  to, or deleting, a recurring event — since a quick 2-3 option decision reads
  better as a popup than a near-empty full screen.

Bumps the floret-kit pin (55ad536 -> e1919ca) for the FullScreenPicker
`actions` passthrough.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 14:23:23 +02:00
5fa6eac1ad Merge chore/floret-implementation into release/v2.14.0
All checks were successful
Translations / check (pull_request) Successful in 4s
CI / ci (pull_request) Successful in 8m41s
Brings the floret-kit migration onto the 2.14.0 release line: Calendula now
draws its shared UI/crash/locale/reminder/time code from the floret-kit
submodule (Gradle composite build) instead of inline copies, plus the
week-number isoWeekNumber extraction and the wrapped-title height animation.

Pins floret-kit at 55ad536 (origin/main). Integrated for a full pre-release
verification sweep; on-device review still owed before cutting the release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 11:03:25 +02:00
a8aeae5f32 Merge pull request 'Translations update from Weblate' (!63) from weblate-bot/calendula:weblate-calendula-strings into main
All checks were successful
Release — F-Droid repo + Gitea release / detect (push) Successful in 6s
Release — F-Droid repo + Gitea release / release (push) Has been skipped
Reviewed-on: #63
2026-07-06 20:40:51 +00:00
Ulisse Perusin
5771b603f2 Translated using Weblate (Italian)
All checks were successful
Translations / check (pull_request) Successful in 5s
CI / ci (pull_request) Successful in 10m29s
Currently translated at 99.0% (406 of 410 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/it/
2026-07-06 20:20:20 +00:00
Kachelkaiser
5725989aff Translated using Weblate (German)
Currently translated at 100.0% (410 of 410 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/de/
2026-07-06 20:20:19 +00:00
40 changed files with 502 additions and 348 deletions

View File

@@ -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 1999
* 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) {

View File

@@ -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) {

View File

@@ -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) },

View File

@@ -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>

View File

@@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 192 KiB

View 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

View File

@@ -0,0 +1 @@
Un calendario moderno con Material 3 Expressive para Android.

View File

@@ -0,0 +1 @@
Calendula

View 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

View File

@@ -0,0 +1 @@
Un moderno calendario Material 3 Expressive per Android.

View File

@@ -0,0 +1 @@
Calendula