Compare commits
11 Commits
chore/flor
...
d398c72005
| Author | SHA1 | Date | |
|---|---|---|---|
| d398c72005 | |||
| 2218c11d3f | |||
| 1e7ee5b98a | |||
| a82df3f6d0 | |||
| e0e3eb73b9 | |||
| 8a7a0af207 | |||
| 71a652ce3b | |||
| 5fa6eac1ad | |||
| a8aeae5f32 | |||
|
|
5771b603f2 | ||
|
|
5725989aff |
@@ -492,7 +492,13 @@ class AndroidCalendarDataSource @Inject constructor(
|
|||||||
return resolver.query(
|
return resolver.query(
|
||||||
uri,
|
uri,
|
||||||
InstanceProjection.COLUMNS,
|
InstanceProjection.COLUMNS,
|
||||||
null, null,
|
// Hide cancelled occurrences: "delete only this event" writes a
|
||||||
|
// cancelled exception for the one instance (#47). A NULL status is a
|
||||||
|
// normal, un-cancelled event, so it must survive the filter — a bare
|
||||||
|
// `!= CANCELED` would drop it (NULL != 2 is NULL, not true).
|
||||||
|
"${CalendarContract.Instances.STATUS} IS NULL OR " +
|
||||||
|
"${CalendarContract.Instances.STATUS} != ${CalendarContract.Events.STATUS_CANCELED}",
|
||||||
|
null,
|
||||||
CalendarContract.Instances.BEGIN + " ASC",
|
CalendarContract.Instances.BEGIN + " ASC",
|
||||||
)?.use { c -> c.mapAllNotNull { CursorColumnReader(c).toEventInstance() } } ?: emptyList()
|
)?.use { c -> c.mapAllNotNull { CursorColumnReader(c).toEventInstance() } } ?: emptyList()
|
||||||
}
|
}
|
||||||
@@ -1183,15 +1189,25 @@ class AndroidCalendarDataSource @Inject constructor(
|
|||||||
|
|
||||||
override fun deleteOccurrence(eventId: Long, beginMillis: Long) {
|
override fun deleteOccurrence(eventId: Long, beginMillis: Long) {
|
||||||
// A cancelled exception row hides exactly this occurrence; the sync
|
// A cancelled exception row hides exactly this occurrence; the sync
|
||||||
// adapter turns it into an EXDATE/cancelled VEVENT upstream.
|
// adapter turns it into an EXDATE/cancelled VEVENT upstream. It has to
|
||||||
val values = ContentValues().apply {
|
// carry the full time set (DTSTART + DURATION + zone), not just STATUS:
|
||||||
put(CalendarContract.Events.ORIGINAL_INSTANCE_TIME, beginMillis)
|
// the provider only demotes the cloned exception to a single instance —
|
||||||
put(CalendarContract.Events.STATUS, CalendarContract.Events.STATUS_CANCELED)
|
// clearing the inherited RRULE — when it can derive that instance from
|
||||||
}
|
// those columns. A STATUS-only cancel left the RRULE standing and
|
||||||
|
// cancelled the whole series, wiping every other occurrence (#47), the
|
||||||
|
// same trap the edit path documents (Codeberg #16).
|
||||||
|
val row = querySeriesRow(eventId)
|
||||||
|
val values = buildOccurrenceCancelValues(
|
||||||
|
originalInstanceMillis = beginMillis,
|
||||||
|
dtStartMillis = beginMillis,
|
||||||
|
duration = row.duration,
|
||||||
|
timezone = row.timezone,
|
||||||
|
allDay = row.allDay,
|
||||||
|
)
|
||||||
val uri = ContentUris.withAppendedId(
|
val uri = ContentUris.withAppendedId(
|
||||||
CalendarContract.Events.CONTENT_EXCEPTION_URI, eventId,
|
CalendarContract.Events.CONTENT_EXCEPTION_URI, eventId,
|
||||||
)
|
)
|
||||||
resolver.insert(uri, values)
|
resolver.insert(uri, values.toContentValues())
|
||||||
?: throw WriteFailedException("cancel occurrence event id=$eventId begin=$beginMillis")
|
?: throw WriteFailedException("cancel occurrence event id=$eventId begin=$beginMillis")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -182,6 +182,33 @@ internal fun buildOccurrenceExceptionValues(
|
|||||||
putAll(eventColorColumns(form.colorKey, form.color))
|
putAll(eventColorColumns(form.colorKey, form.color))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Column values for a *cancelled*-occurrence exception row ("delete only this
|
||||||
|
* event"): inserting them at `Events.CONTENT_EXCEPTION_URI/<id>` makes the
|
||||||
|
* provider clone the series row and cancel exactly this one instance.
|
||||||
|
*
|
||||||
|
* As with [buildOccurrenceExceptionValues], the occurrence must be anchored with
|
||||||
|
* DTSTART + DURATION so the provider derives a single instance and clears the
|
||||||
|
* inherited RRULE. A STATUS-only cancel skips that: the clone keeps the RRULE, so
|
||||||
|
* the *whole series* is cancelled and every other occurrence disappears
|
||||||
|
* (Codeberg #47). The occurrence's length/zone come straight from the series row
|
||||||
|
* — cancelling never changes them.
|
||||||
|
*/
|
||||||
|
internal fun buildOccurrenceCancelValues(
|
||||||
|
originalInstanceMillis: Long,
|
||||||
|
dtStartMillis: Long,
|
||||||
|
duration: String?,
|
||||||
|
timezone: String?,
|
||||||
|
allDay: Int,
|
||||||
|
): Map<String, Any?> = buildMap {
|
||||||
|
put(CalendarContract.Events.ORIGINAL_INSTANCE_TIME, originalInstanceMillis)
|
||||||
|
put(CalendarContract.Events.DTSTART, dtStartMillis)
|
||||||
|
put(CalendarContract.Events.DURATION, duration)
|
||||||
|
put(CalendarContract.Events.EVENT_TIMEZONE, timezone)
|
||||||
|
put(CalendarContract.Events.ALL_DAY, allDay)
|
||||||
|
put(CalendarContract.Events.STATUS, CalendarContract.Events.STATUS_CANCELED)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `EVENT_COLOR` / `EVENT_COLOR_KEY` columns for a colour selection. A
|
* The `EVENT_COLOR` / `EVENT_COLOR_KEY` columns for a colour selection. A
|
||||||
* [colorKey] writes the key alone (the provider derives `EVENT_COLOR` from the
|
* [colorKey] writes the key alone (the provider derives `EVENT_COLOR` from the
|
||||||
|
|||||||
@@ -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),
|
)
|
||||||
|
GroupedRow(
|
||||||
|
title = stringResource(R.string.event_edit_conflict_overwrite),
|
||||||
|
summary = stringResource(R.string.event_edit_conflict_overwrite_hint),
|
||||||
|
position = positionOf(0, 2),
|
||||||
onClick = onOverwrite,
|
onClick = onOverwrite,
|
||||||
)
|
)
|
||||||
OptionCard(
|
GroupedRow(
|
||||||
label = stringResource(R.string.event_edit_conflict_discard),
|
title = stringResource(R.string.event_edit_conflict_discard),
|
||||||
supportingText = stringResource(R.string.event_edit_conflict_discard_hint),
|
summary = stringResource(R.string.event_edit_conflict_discard_hint),
|
||||||
|
position = positionOf(1, 2),
|
||||||
onClick = onDiscard,
|
onClick = onDiscard,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) }
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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),
|
||||||
|
leading = { Icon(imageVector = eventFormFieldIcon(field), contentDescription = null) },
|
||||||
onClick = { onSelect(field) },
|
onClick = { onSelect(field) },
|
||||||
icon = eventFormFieldIcon(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,26 +1102,39 @@ 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 = {
|
||||||
|
// The quick-pick list adds on tap; only the custom step needs Add.
|
||||||
|
if (customMode) {
|
||||||
|
TextButton(
|
||||||
|
enabled = customMinutes != null && customMinutes !in alreadyChosen,
|
||||||
|
onClick = { customMinutes?.let(onSelect) },
|
||||||
|
) { Text(stringResource(R.string.event_edit_add)) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
if (!customMode) {
|
if (!customMode) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
val presets = REMINDER_QUICK_PICKS.filterNot { it in alreadyChosen }
|
||||||
REMINDER_QUICK_PICKS.filterNot { it in alreadyChosen }.forEach { minutes ->
|
val rowCount = presets.size + 1
|
||||||
OptionCard(
|
presets.forEachIndexed { index, minutes ->
|
||||||
label = reminderLabel(minutes),
|
GroupedRow(
|
||||||
|
title = reminderLabel(minutes),
|
||||||
|
position = positionOf(index, rowCount),
|
||||||
onClick = { onSelect(minutes) },
|
onClick = { onSelect(minutes) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
OptionCard(
|
GroupedRow(
|
||||||
label = stringResource(R.string.event_edit_reminder_custom),
|
title = stringResource(R.string.event_edit_reminder_custom),
|
||||||
|
position = positionOf(rowCount - 1, rowCount),
|
||||||
onClick = { customMode = true },
|
onClick = { customMode = true },
|
||||||
labelColor = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||||
|
) {
|
||||||
DialogAmountField(
|
DialogAmountField(
|
||||||
value = amountText,
|
value = amountText,
|
||||||
onValueChange = { amountText = it },
|
onValueChange = { amountText = it },
|
||||||
@@ -1139,20 +1148,7 @@ private fun ReminderPickerDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
// The quick-pick list adds on tap; only the custom step needs Add.
|
|
||||||
if (customMode) {
|
|
||||||
TextButton(
|
|
||||||
enabled = customMinutes != null && customMinutes !in alreadyChosen,
|
|
||||||
onClick = { customMinutes?.let(onSelect) },
|
|
||||||
) { Text(stringResource(R.string.event_edit_add)) }
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) }
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** How a custom recurrence ends; mirrors [RecurrenceEnd] in saveable form. */
|
/** How a custom recurrence ends; mirrors [RecurrenceEnd] in saveable form. */
|
||||||
@@ -1222,33 +1218,46 @@ 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 = {
|
||||||
|
// The preset list applies on tap; only the custom step needs OK.
|
||||||
|
if (customMode) {
|
||||||
|
TextButton(
|
||||||
|
enabled = customResult != null,
|
||||||
|
onClick = { customResult?.let(onSelect) },
|
||||||
|
) { Text(stringResource(R.string.dialog_ok)) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
if (!customMode) {
|
if (!customMode) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
val rowCount = RecurrenceFreq.entries.size + 2
|
||||||
OptionCard(
|
GroupedRow(
|
||||||
label = stringResource(R.string.event_edit_recurrence_none),
|
title = stringResource(R.string.event_edit_recurrence_none),
|
||||||
onClick = { onSelect(null) },
|
position = positionOf(0, rowCount),
|
||||||
selected = current == null,
|
selected = current == null,
|
||||||
|
onClick = { onSelect(null) },
|
||||||
)
|
)
|
||||||
RecurrenceFreq.entries.forEach { entry ->
|
RecurrenceFreq.entries.forEachIndexed { index, entry ->
|
||||||
OptionCard(
|
GroupedRow(
|
||||||
label = stringResource(recurrencePresetLabel(entry)),
|
title = stringResource(recurrencePresetLabel(entry)),
|
||||||
onClick = { onSelect(SimpleRecurrence(entry).toRRule()) },
|
position = positionOf(index + 1, rowCount),
|
||||||
selected = isPlainPreset && parsed?.freq == entry,
|
selected = isPlainPreset && parsed?.freq == entry,
|
||||||
|
onClick = { onSelect(SimpleRecurrence(entry).toRRule()) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
OptionCard(
|
GroupedRow(
|
||||||
label = stringResource(R.string.event_edit_recurrence_custom),
|
title = stringResource(R.string.event_edit_recurrence_custom),
|
||||||
onClick = { customMode = true },
|
position = positionOf(rowCount - 1, rowCount),
|
||||||
selected = current != null && !isPlainPreset,
|
selected = current != null && !isPlainPreset,
|
||||||
labelColor = MaterialTheme.colorScheme.primary,
|
onClick = { customMode = true },
|
||||||
)
|
)
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.event_edit_recurrence_every),
|
text = stringResource(R.string.event_edit_recurrence_every),
|
||||||
@@ -1270,45 +1279,50 @@ private fun RecurrencePickerDialog(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (freq == RecurrenceFreq.Weekly) {
|
if (freq == RecurrenceFreq.Weekly) {
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
WeekdayToggleRow(
|
WeekdayToggleRow(
|
||||||
selected = daysMask.toDaySet(),
|
selected = daysMask.toDaySet(),
|
||||||
onToggle = { day -> daysMask = daysMask xor day.toMaskBit() },
|
onToggle = { day -> daysMask = daysMask xor day.toMaskBit() },
|
||||||
locale = locale,
|
locale = locale,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.event_edit_recurrence_ends),
|
text = stringResource(R.string.event_edit_recurrence_ends),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
OptionCard(
|
}
|
||||||
label = stringResource(R.string.event_edit_recurrence_end_never),
|
GroupedRow(
|
||||||
onClick = { endMode = RecurrenceEndMode.Never },
|
title = stringResource(R.string.event_edit_recurrence_end_never),
|
||||||
|
position = positionOf(0, 3),
|
||||||
selected = endMode == RecurrenceEndMode.Never,
|
selected = endMode == RecurrenceEndMode.Never,
|
||||||
|
onClick = { endMode = RecurrenceEndMode.Never },
|
||||||
)
|
)
|
||||||
OptionCard(
|
GroupedRow(
|
||||||
label = stringResource(R.string.event_edit_recurrence_end_until),
|
title = stringResource(R.string.event_edit_recurrence_end_until),
|
||||||
onClick = {
|
summary = untilDate?.let {
|
||||||
endMode = RecurrenceEndMode.Until
|
|
||||||
showUntilPicker = true
|
|
||||||
},
|
|
||||||
supportingText = untilDate?.let {
|
|
||||||
remember(it, locale) {
|
remember(it, locale) {
|
||||||
DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
|
DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
|
||||||
.withLocale(locale).format(it.toJavaLocalDate())
|
.withLocale(locale).format(it.toJavaLocalDate())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
position = positionOf(1, 3),
|
||||||
selected = endMode == RecurrenceEndMode.Until,
|
selected = endMode == RecurrenceEndMode.Until,
|
||||||
|
onClick = {
|
||||||
|
endMode = RecurrenceEndMode.Until
|
||||||
|
showUntilPicker = true
|
||||||
|
},
|
||||||
)
|
)
|
||||||
OptionCard(
|
GroupedRow(
|
||||||
label = stringResource(R.string.event_edit_recurrence_end_count),
|
title = stringResource(R.string.event_edit_recurrence_end_count),
|
||||||
onClick = { endMode = RecurrenceEndMode.Count },
|
position = positionOf(2, 3),
|
||||||
selected = endMode == RecurrenceEndMode.Count,
|
selected = endMode == RecurrenceEndMode.Count,
|
||||||
|
onClick = { endMode = RecurrenceEndMode.Count },
|
||||||
)
|
)
|
||||||
if (endMode == RecurrenceEndMode.Count) {
|
if (endMode == RecurrenceEndMode.Count) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 4.dp),
|
||||||
|
) {
|
||||||
DialogAmountField(
|
DialogAmountField(
|
||||||
value = countText,
|
value = countText,
|
||||||
onValueChange = { countText = it },
|
onValueChange = { countText = it },
|
||||||
@@ -1323,20 +1337,6 @@ private fun RecurrencePickerDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
// The preset list applies on tap; only the custom step needs OK.
|
|
||||||
if (customMode) {
|
|
||||||
TextButton(
|
|
||||||
enabled = customResult != null,
|
|
||||||
onClick = { customResult?.let(onSelect) },
|
|
||||||
) { Text(stringResource(R.string.dialog_ok)) }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) }
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
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,11 +1731,21 @@ 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) {
|
||||||
|
TextButton(onClick = onClear) {
|
||||||
|
Text(stringResource(R.string.event_edit_color_reset))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||||
|
) {
|
||||||
if (palette.isNotEmpty()) {
|
if (palette.isNotEmpty()) {
|
||||||
ColorSwatchRow(
|
ColorSwatchRow(
|
||||||
colors = palette.map { it.argb },
|
colors = palette.map { it.argb },
|
||||||
@@ -1772,21 +1772,8 @@ private fun ColorPickerDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) }
|
|
||||||
},
|
|
||||||
dismissButton = if (hasExplicitColor) {
|
|
||||||
{
|
|
||||||
TextButton(onClick = onClear) {
|
|
||||||
Text(stringResource(R.string.event_edit_color_reset))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun accessLevelIcon(level: AccessLevel): ImageVector = when (level) {
|
private fun accessLevelIcon(level: AccessLevel): ImageVector = when (level) {
|
||||||
AccessLevel.Default -> Icons.Default.Tune
|
AccessLevel.Default -> Icons.Default.Tune
|
||||||
|
|||||||
@@ -160,7 +160,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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -220,6 +220,46 @@ class EventWriteMapperTest {
|
|||||||
assertThat(values).containsEntry(CalendarContract.Events.DESCRIPTION, null)
|
assertThat(values).containsEntry(CalendarContract.Events.DESCRIPTION, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- buildOccurrenceCancelValues ("delete only this event") ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `occurrence cancel anchors a single instance and cancels only it`() {
|
||||||
|
val values = buildOccurrenceCancelValues(
|
||||||
|
originalInstanceMillis = 1_700_000_000_000L,
|
||||||
|
dtStartMillis = 1_700_000_000_000L,
|
||||||
|
duration = "P3600S",
|
||||||
|
timezone = "Europe/Berlin",
|
||||||
|
allDay = 0,
|
||||||
|
)
|
||||||
|
assertThat(values[CalendarContract.Events.ORIGINAL_INSTANCE_TIME])
|
||||||
|
.isEqualTo(1_700_000_000_000L)
|
||||||
|
// DTSTART + DURATION make the provider derive a single instance and drop
|
||||||
|
// the inherited RRULE, so only this occurrence is cancelled — not the
|
||||||
|
// whole series (#47). DTEND is never sent (the provider rejects it).
|
||||||
|
assertThat(values[CalendarContract.Events.DTSTART]).isEqualTo(1_700_000_000_000L)
|
||||||
|
assertThat(values[CalendarContract.Events.DURATION]).isEqualTo("P3600S")
|
||||||
|
assertThat(values[CalendarContract.Events.EVENT_TIMEZONE]).isEqualTo("Europe/Berlin")
|
||||||
|
assertThat(values[CalendarContract.Events.STATUS])
|
||||||
|
.isEqualTo(CalendarContract.Events.STATUS_CANCELED)
|
||||||
|
assertThat(values).doesNotContainKey(CalendarContract.Events.DTEND)
|
||||||
|
assertThat(values).doesNotContainKey(CalendarContract.Events.RRULE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `all-day occurrence cancel keeps the all-day flag and utc zone`() {
|
||||||
|
val values = buildOccurrenceCancelValues(
|
||||||
|
originalInstanceMillis = 1_700_000_000_000L,
|
||||||
|
dtStartMillis = 1_700_000_000_000L,
|
||||||
|
duration = "P1D",
|
||||||
|
timezone = "UTC",
|
||||||
|
allDay = 1,
|
||||||
|
)
|
||||||
|
assertThat(values[CalendarContract.Events.ALL_DAY]).isEqualTo(1)
|
||||||
|
assertThat(values[CalendarContract.Events.EVENT_TIMEZONE]).isEqualTo("UTC")
|
||||||
|
assertThat(values[CalendarContract.Events.STATUS])
|
||||||
|
.isEqualTo(CalendarContract.Events.STATUS_CANCELED)
|
||||||
|
}
|
||||||
|
|
||||||
// --- per-event colour ---
|
// --- per-event colour ---
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
|
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
|
||||||