diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 8efb238..da623ff 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -66,7 +66,21 @@ guide here, not a contract — scope per slice is decided as we go. | v1.2.1 | Form polish after on-device review — card design system, optional fields + settings defaults, OptionCard dialogs, expressive motion | complete (shipped 2026-06-11) | | v1.3 | Edit event — shared form, scoped recurring writes (this / following / all), recurrence picker | complete (shipped 2026-06-11) | | v1.4 | Reminder notifications — see below | complete (shipped 2026-06-11) | -| v2.0 | Quick-add, occurrence edit, conflict dialog, polish, release | planned | +| v2.0 | Conflict dialog, polish pass (stale read-only copy in F-Droid/README), release | planned | + +v2.0 scope was re-cut on 2026-06-11, after v1.4: +- **Occurrence edit** already shipped early, in v1.3. +- **Quick-add** is **cut from scope**: the full form already opens prefilled + (visible day, last-used calendar, optional fields hidden), so the sheet + would only save one screen transition while adding a second create-surface + to maintain. Revisit only if real-world feedback says creation feels heavy. +- **Calendar switching while editing** moves to the v3 backlog (sync-adapter + minefield: `CALENDAR_ID` is sync-adapter-owned, AOSP locks the field; an + honest implementation is copy+delete like Google Calendar, with sync-identity + and attendee side effects). +- **Conflict dialog** stays (plan 03, decision 5): on save, compare against + the row as it was when the form loaded; on external change, ask + overwrite / discard. Closes the silent-clobber gap on synced calendars. ## v1.4 — Reminder Notifications @@ -99,5 +113,7 @@ Deliberately deferred (add only if needed): - Full-text search - Tablet / foldable layouts - Optional: ICS file import (drag-and-drop) +- Optional: move event to another calendar (copy+delete model with a + consequences warning — deferred from v2.0, see above) Order is indicative — community feedback after V1 may re-prioritize. diff --git a/CHANGELOG.md b/CHANGELOG.md index bf757eb..dd377a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Conflict handling when saving an edit: if the event changed elsewhere + (sync, another device) while the form was open, saving now asks whether + to keep or discard your changes instead of silently overwriting the + edited fields — and tells you when the event was deleted in the meantime. + "Keep" still writes only the fields you touched; external changes to + untouched fields survive either way +- F-Droid store screenshots (German + English), captured with demo data + +### Changed +- F-Droid description and README no longer claim the app is read-only — + they now describe write support and reminder delivery + ## [1.4.0] — 2026-06-11 ### Added diff --git a/README.md b/README.md index 5890769..98ad074 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,14 @@ word "calendar". Calendula reads from Android's built-in `CalendarContract`, so any calendar source synced to your device (CalDAV via DAVx5, Google, local, WebCal subscriptions, ...) is shown. -## Features (V1) +## Features - Month, Week, and Day views -- Read-only event details (write support comes in V2) +- Full event details — attendees, reminders, recurrence, availability, and more +- Create, edit, and delete events — recurring events with scoped writes + (only this event / this and all following / whole series) and a simple + recurrence picker +- Reminder notifications, delivered by Calendula itself (tap opens the event) - Multi-calendar visibility toggle - Material You Dynamic Color (Android 12+) - Light/Dark theme follows system diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/domain/EventForm.kt b/app/src/main/java/de/jeanlucmakiola/calendula/domain/EventForm.kt index 19dba3f..e892c5f 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/domain/EventForm.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/domain/EventForm.kt @@ -94,6 +94,30 @@ fun EventDetail.toEditForm(beginMillis: Long, endMillis: Long, zone: TimeZone): ) } +/** + * What the edit form saw when it loaded — compared against a fresh read at + * save time to detect external changes (sync, another device) that landed + * while the form was open. The raw row times ride along because + * [toEditForm] derives the form's times from the *tapped occurrence*, so + * re-deriving with the same occurrence would mask an externally moved + * event. Not covered (the form can't write them, and the dirty-checked + * write can't clobber them): attendees, status, the user's own response, + * reminder methods, and a recurring event's duration. + */ +data class EditSnapshot( + val form: EventForm, + /** The raw Events-row times (for recurring events: the series anchor). */ + val rowStart: Instant, + val rowEnd: Instant, +) + +fun EventDetail.toEditSnapshot(beginMillis: Long, endMillis: Long, zone: TimeZone): EditSnapshot = + EditSnapshot( + form = toEditForm(beginMillis, endMillis, zone), + rowStart = instance.start, + rowEnd = instance.end, + ) + /** * The optional sections that hold a value in [form] — when editing, these * must be visible regardless of the user's default-fields setting, or the diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt index 741df1f..9a09f32 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt @@ -217,7 +217,8 @@ fun EventEditScreen( viewModel.consumeSaveResult() snackbarHostState.showSnackbar(writeDeniedMessage) } - SaveUiState.Idle, SaveUiState.AwaitingScope, SaveUiState.Saving, null -> Unit + // AwaitingScope/AwaitingConflict/Gone render as dialogs below. + else -> Unit } } @@ -269,6 +270,68 @@ fun EventEditScreen( onDismiss = viewModel::consumeSaveResult, ) } + + // The event changed externally (sync) while the form was open (v2.0). + if (state?.saveState is SaveUiState.AwaitingConflict) { + SaveConflictDialog( + onOverwrite = viewModel::saveOverwriting, + onDiscard = close, + onDismiss = viewModel::consumeSaveResult, + ) + } + + // ...or was deleted underneath us — nothing left to save onto. Closing + // through [onSaved] also pops the detail screen, whose occurrence is gone. + if (state?.saveState == SaveUiState.Gone) { + AlertDialog( + onDismissRequest = {}, + title = { Text(stringResource(R.string.event_edit_gone_title)) }, + text = { Text(stringResource(R.string.event_edit_gone_body)) }, + confirmButton = { + TextButton(onClick = { + viewModel.reset() + onSaved() + }) { Text(stringResource(R.string.dialog_ok)) } + }, + ) + } +} + +/** + * Overwrite-or-discard choice when the event changed underneath an open + * form (no locking; detected by re-reading at save time). "Overwrite" still + * only writes the fields the user edited — external changes to untouched + * fields survive either way. Cancelling returns to the form. + */ +@Composable +private fun SaveConflictDialog( + onOverwrite: () -> Unit, + onDiscard: () -> Unit, + onDismiss: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.event_edit_conflict_title)) }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text(stringResource(R.string.event_edit_conflict_body)) + Spacer(Modifier.height(4.dp)) + OptionCard( + label = stringResource(R.string.event_edit_conflict_overwrite), + supportingText = stringResource(R.string.event_edit_conflict_overwrite_hint), + onClick = onOverwrite, + ) + OptionCard( + label = stringResource(R.string.event_edit_conflict_discard), + supportingText = stringResource(R.string.event_edit_conflict_discard_hint), + onClick = onDiscard, + ) + } + }, + confirmButton = { + TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) } + }, + ) } /** diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditUiState.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditUiState.kt index 7111281..dd13a49 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditUiState.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditUiState.kt @@ -4,6 +4,7 @@ import de.jeanlucmakiola.calendula.domain.CalendarSource import de.jeanlucmakiola.calendula.domain.EventForm import de.jeanlucmakiola.calendula.domain.EventFormField import de.jeanlucmakiola.calendula.domain.EventFormProblem +import de.jeanlucmakiola.calendula.domain.RecurringWriteScope /** * UI state for the event form (v1.2: create; v1.3 reuses it for edit). Null @@ -39,6 +40,14 @@ sealed interface SaveUiState { data object Idle : SaveUiState /** A dirty recurring event waits for the user to pick the write scope. */ data object AwaitingScope : SaveUiState + /** + * The event changed externally (sync) while the form was open; the save + * is parked with its chosen [scope] until the user picks overwrite, + * discard, or cancel. + */ + data class AwaitingConflict(val scope: RecurringWriteScope) : SaveUiState + /** The event was deleted externally while the form was open. */ + data object Gone : SaveUiState data object Saving : SaveUiState data object Saved : SaveUiState /** WRITE_CALENDAR was revoked between the tap and the provider call. */ diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt index 875b658..28040ee 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt @@ -4,18 +4,20 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import de.jeanlucmakiola.calendula.data.calendar.CalendarRepository +import de.jeanlucmakiola.calendula.data.calendar.NoSuchEventException import de.jeanlucmakiola.calendula.data.di.IoDispatcher import de.jeanlucmakiola.calendula.data.prefs.CalendarPrefs import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs import de.jeanlucmakiola.calendula.domain.AccessLevel import de.jeanlucmakiola.calendula.domain.Availability import de.jeanlucmakiola.calendula.domain.CalendarSource +import de.jeanlucmakiola.calendula.domain.EditSnapshot import de.jeanlucmakiola.calendula.domain.EventForm import de.jeanlucmakiola.calendula.domain.EventFormField import de.jeanlucmakiola.calendula.domain.RecurringWriteScope import de.jeanlucmakiola.calendula.domain.populatedFields import de.jeanlucmakiola.calendula.domain.problems -import de.jeanlucmakiola.calendula.domain.toEditForm +import de.jeanlucmakiola.calendula.domain.toEditSnapshot import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -68,15 +70,21 @@ class EventEditViewModel @Inject constructor( val loadFailed: StateFlow = _loadFailed.asStateFlow() /** - * The event being edited plus the form exactly as it was prefilled. + * The event being edited plus everything the form saw at load time. * For recurring events the write scope is chosen at save time; the - * tapped occurrence's [beginMillis] anchors occurrence-level writes. + * tapped occurrence's [beginMillis]/[endMillis] anchor occurrence-level + * writes and the conflict re-read. [zone] is pinned at load so a device + * timezone change mid-edit can't fake a conflict. */ private data class EditTarget( val eventId: Long, - val original: EventForm, + val snapshot: EditSnapshot, val beginMillis: Long, - ) + val endMillis: Long, + val zone: TimeZone, + ) { + val original: EventForm get() = snapshot.form + } private data class LocalInputs( val form: EventForm?, @@ -167,11 +175,12 @@ class EventEditViewModel @Inject constructor( _loadFailed.value = true return@launch } - val original = detail.toEditForm(beginMillis, endMillis, TimeZone.currentSystemDefault()) - _editTarget.value = EditTarget(eventId, original, beginMillis) + val zone = TimeZone.currentSystemDefault() + val snapshot = detail.toEditSnapshot(beginMillis, endMillis, zone) + _editTarget.value = EditTarget(eventId, snapshot, beginMillis, endMillis, zone) // Sections holding data must show even when not in the defaults. - _revealed.value = original.populatedFields() - _form.value = original + _revealed.value = snapshot.form.populatedFields() + _form.value = snapshot.form } } @@ -249,10 +258,43 @@ class EventEditViewModel @Inject constructor( performSave(current.form, scope) } - private fun performSave(form: EventForm, scope: RecurringWriteScope) { + /** Finish a save parked in [SaveUiState.AwaitingConflict], overwriting. */ + fun saveOverwriting() { + val current = state.value ?: return + val parked = current.saveState as? SaveUiState.AwaitingConflict ?: return + performSave(current.form, parked.scope, ignoreConflict = true) + } + + private fun performSave( + form: EventForm, + scope: RecurringWriteScope, + ignoreConflict: Boolean = false, + ) { val target = _editTarget.value viewModelScope.launch { _saveState.value = SaveUiState.Saving + // No locking (plan 03, decision 5): right before writing, re-read + // the event and compare against what the form loaded. An external + // change parks the save in a conflict dialog instead of silently + // clobbering the edited fields. + if (target != null && !ignoreConflict) { + val fresh = try { + repository.eventDetail(target.eventId) + .toEditSnapshot(target.beginMillis, target.endMillis, target.zone) + } catch (e: CancellationException) { + throw e + } catch (e: NoSuchEventException) { + _saveState.value = SaveUiState.Gone + return@launch + } catch (e: Exception) { + // Can't verify — proceed; a real problem fails the write itself. + null + } + if (fresh != null && fresh != target.snapshot) { + _saveState.value = SaveUiState.AwaitingConflict(scope) + return@launch + } + } _saveState.value = try { if (target == null) { repository.createEvent(form) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index b3ad9ec..7760b06 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -82,6 +82,16 @@ Verfügbarkeit Sichtbarkeit + + Termin wurde extern geändert + Während du bearbeitet hast, wurde dieser Termin anderswo geändert — durch Synchronisierung oder eine andere App. Was soll mit deinen Änderungen passieren? + Meine Änderungen speichern + Nur von dir bearbeitete Felder überschreiben die externe Änderung + Meine Änderungen verwerfen + Der Termin bleibt, wie er jetzt ist + Termin wurde gelöscht + Dieser Termin wurde zwischenzeitlich gelöscht, etwa auf einem anderen Gerät. Deine Änderungen können nicht mehr gespeichert werden. + Wiederholt sich nicht Benutzerdefiniert diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 57c5c0a..17b589a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -83,6 +83,16 @@ Availability Visibility + + Event changed elsewhere + While you were editing, this event was changed — by sync or another app. What should happen to your changes? + Save my changes + Only fields you edited overwrite the outside change + Discard my changes + The event stays as it is now + Event deleted + This event was deleted in the meantime, for example on another device. Your changes can no longer be saved. + Does not repeat Custom diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/domain/EventFormTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/domain/EventFormTest.kt index f845f45..bc735b5 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/domain/EventFormTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/domain/EventFormTest.kt @@ -112,21 +112,24 @@ class EventFormTest { reminders: List = emptyList(), availability: Availability = Availability.Busy, accessLevel: AccessLevel = AccessLevel.Default, + rowStart: Long = 0L, + rowEnd: Long = 0L, + attendees: List = emptyList(), ): EventDetail = EventDetail( instance = EventInstance( instanceId = 1L, eventId = 1L, calendarId = 7L, title = title, - start = Instant.fromEpochMilliseconds(0L), - end = Instant.fromEpochMilliseconds(0L), + start = Instant.fromEpochMilliseconds(rowStart), + end = Instant.fromEpochMilliseconds(rowEnd), isAllDay = isAllDay, color = 0, location = location, ), description = description, organizer = null, - attendees = emptyList(), + attendees = attendees, rrule = rrule, reminders = reminders, availability = availability, @@ -177,6 +180,41 @@ class EventFormTest { assertThat(prefilled.rrule).isEqualTo("FREQ=WEEKLY") } + @Test + fun `snapshots of an unchanged event are equal`() { + val a = detail().toEditSnapshot(0L, 3_600_000L, berlin) + val b = detail().toEditSnapshot(0L, 3_600_000L, berlin) + assertThat(b).isEqualTo(a) + } + + @Test + fun `an external field change makes snapshots differ`() { + val loaded = detail().toEditSnapshot(0L, 3_600_000L, berlin) + val fresh = detail(title = "Stand-up (moved)").toEditSnapshot(0L, 3_600_000L, berlin) + assertThat(fresh).isNotEqualTo(loaded) + } + + @Test + fun `an external time move is caught by the row times the form cannot see`() { + // Both snapshots are taken for the same tapped occurrence, so the + // *forms* derive identical times — only rowStart/rowEnd betray the move. + val loaded = detail(rrule = "FREQ=WEEKLY", rowStart = 0L) + .toEditSnapshot(0L, 3_600_000L, berlin) + val fresh = detail(rrule = "FREQ=WEEKLY", rowStart = 86_400_000L) + .toEditSnapshot(0L, 3_600_000L, berlin) + assertThat(fresh.form).isEqualTo(loaded.form) + assertThat(fresh).isNotEqualTo(loaded) + } + + @Test + fun `changes the form cannot write do not fake a conflict`() { + val loaded = detail().toEditSnapshot(0L, 3_600_000L, berlin) + val fresh = detail( + attendees = listOf(Attendee("Ada", "ada@example.org", AttendeeStatus.Accepted)), + ).toEditSnapshot(0L, 3_600_000L, berlin) + assertThat(fresh).isEqualTo(loaded) + } + @Test fun `populatedFields reports exactly the sections holding values`() { val empty = form().copy(location = "", description = "") diff --git a/docs/superpowers/plans/2026-06-11-03-write-support.md b/docs/superpowers/plans/2026-06-11-03-write-support.md index 0d2940e..0097c36 100644 --- a/docs/superpowers/plans/2026-06-11-03-write-support.md +++ b/docs/superpowers/plans/2026-06-11-03-write-support.md @@ -47,8 +47,8 @@ Domain bleibt pure Kotlin. |---|---|---| | v1.1 | Write-Fundament: `WRITE_CALENDAR`, `canModifyContents`, Delete (Serie + einzelnes Vorkommen) | ausgeliefert (v1.1.0, 2026-06-11) | | v1.2 | Create: Event-Formular (Titel, Kalender, ganztägig, Start/Ende, Ort, Beschreibung), FAB, Default-Kalender-Pref | ausgeliefert (v1.2.0, 2026-06-11) | -| v1.3 | Edit: Formular wiederverwendet, Serien-Edit, Reminder-Edit, einfacher Recurrence-Picker | implementiert (Release wartet auf On-Device-Review) | -| v2.0 | Quick-Add, Occurrence-Edit, Konflikt-Dialog, Polish-Pass, Release | offen | +| v1.3 | Edit: Formular wiederverwendet, Serien-Edit, Reminder-Edit, einfacher Recurrence-Picker | ausgeliefert (v1.3.0, 2026-06-11) | +| v2.0 | Konflikt-Dialog, Polish-Pass (read-only-Copy in F-Droid/README), Release | offen (Scope-Recut, s.u.) | ## v1.1 — Write-Fundament + Delete @@ -180,9 +180,24 @@ Domain bleibt pure Kotlin. Bewusst nicht in v1.3 (→ v2.0): Konflikt-Dialog, Kalender-Wechsel beim Bearbeiten (Sync-Adapter-Minenfeld, sperren auch alle Stock-Apps). -## v2.0 — Abschluss (Skizze) +## v2.0 — Abschluss (Scope-Recut 2026-06-11, nach v1.4) -- Quick-Add-Sheet (Titel + Zeit, Rest Defaults) -- Occurrence-Edit (Exception mit geänderten Werten) -- Konflikt-Dialog beim Speichern -- Changelog, F-Droid-Metadaten, Release-Tag +- ~~Quick-Add-Sheet (Titel + Zeit, Rest Defaults)~~ — **gestrichen**: das + Formular öffnet bereits vorbefüllt (sichtbarer Tag, zuletzt benutzter + Kalender, optionale Felder versteckt); der Sheet spart nur einen + Screen-Übergang und kostet eine zweite Create-Surface. Nur bei + Praxis-Feedback wieder aufnehmen +- ~~Occurrence-Edit (Exception mit geänderten Werten)~~ — schon in v1.3 + ausgeliefert (vorgezogen) +- [x] Konflikt-Dialog beim Speichern (Leitentscheidung 5): `EditSnapshot` + (Formular + rohe Row-Zeiten) wird beim Laden gemerkt und vor dem + Schreiben gegen einen frischen Read verglichen; Abweichung parkt den + Save in `AwaitingConflict` (Überschreiben/Verwerfen/Abbrechen, + OptionCard-Stil), gelöschtes Event → `Gone`-Dialog. "Überschreiben" + schreibt weiterhin nur dirty Felder +- Kalender-Wechsel beim Bearbeiten → v3-Backlog (copy+delete-Modell) +- [x] Polish: F-Droid-Description + README auf Write-Support + Reminder + aktualisiert (DE+EN) +- [x] F-Droid-Screenshots (de-DE + en-US, je 6: Woche/Monat/Tag/Detail/ + Formular/Onboarding) — mit Demo-Kalendern auf dem Gerät aufgenommen +- Changelog, Release-Tag v2.0.0 (nach On-Device-Review) diff --git a/fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/description.txt b/fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/description.txt index 100c709..5055210 100644 --- a/fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/description.txt +++ b/fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/description.txt @@ -1,12 +1,17 @@ -Calendula ist eine moderne, quelloffene Kalender-App für Android. Sie liest -direkt aus dem System-Kalender-Provider — jede Quelle, die mit deinem Gerät -synchronisiert ist (Nextcloud über DAVx5, Google, lokal, WebCal-Subscriptions) -erscheint automatisch. +Calendula ist eine moderne, quelloffene Kalender-App für Android. Sie +arbeitet direkt auf dem System-Kalender-Provider — jede Quelle, die mit +deinem Gerät synchronisiert ist (Nextcloud über DAVx5, Google, lokal, +WebCal-Subscriptions), erscheint automatisch, und deine Änderungen +synchronisieren auf demselben Weg zurück. + +Termine erstellen, bearbeiten und löschen — auch wiederkehrende, mit +wählbarer Reichweite (nur dieser Termin / dieser und alle folgenden / ganze +Serie) und einem einfachen Wiederholungs-Picker. Erinnerungen stellt +Calendula selbst als Benachrichtigung zu — ein Tipp darauf öffnet den +Termin. Der Unterschied liegt im Design: echtes Material 3 Expressive durchgehend, mit Dynamic Color, expressiven Animationen und neuen Shape-Sprachen. -V1 ist read-only. Erstellen, Bearbeiten und Löschen von Events kommt mit V2. - -Datenschutz: keinerlei Telemetrie, kein Tracking, kein Netzwerkzugriff — deine -Daten bleiben auf dem Gerät. +Datenschutz: keinerlei Telemetrie, kein Tracking, kein Netzwerkzugriff — +deine Daten bleiben auf dem Gerät. diff --git a/fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/phoneScreenshots/01-week.png b/fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/phoneScreenshots/01-week.png new file mode 100644 index 0000000..a3c9d31 Binary files /dev/null and b/fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/phoneScreenshots/01-week.png differ diff --git a/fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/phoneScreenshots/02-month.png b/fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/phoneScreenshots/02-month.png new file mode 100644 index 0000000..58c9846 Binary files /dev/null and b/fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/phoneScreenshots/02-month.png differ diff --git a/fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/phoneScreenshots/03-day.png b/fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/phoneScreenshots/03-day.png new file mode 100644 index 0000000..458712c Binary files /dev/null and b/fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/phoneScreenshots/03-day.png differ diff --git a/fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/phoneScreenshots/04-detail.png b/fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/phoneScreenshots/04-detail.png new file mode 100644 index 0000000..47b67ab Binary files /dev/null and b/fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/phoneScreenshots/04-detail.png differ diff --git a/fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/phoneScreenshots/05-edit.png b/fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/phoneScreenshots/05-edit.png new file mode 100644 index 0000000..9bc16cf Binary files /dev/null and b/fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/phoneScreenshots/05-edit.png differ diff --git a/fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/phoneScreenshots/06-onboarding.png b/fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/phoneScreenshots/06-onboarding.png new file mode 100644 index 0000000..166c30c Binary files /dev/null and b/fdroid-metadata/de.jeanlucmakiola.calendula/de-DE/phoneScreenshots/06-onboarding.png differ diff --git a/fdroid-metadata/de.jeanlucmakiola.calendula/en-US/description.txt b/fdroid-metadata/de.jeanlucmakiola.calendula/en-US/description.txt index 578ac5d..f59f242 100644 --- a/fdroid-metadata/de.jeanlucmakiola.calendula/en-US/description.txt +++ b/fdroid-metadata/de.jeanlucmakiola.calendula/en-US/description.txt @@ -1,11 +1,15 @@ -Calendula is a modern, open-source calendar app for Android. It reads from -the system calendar provider, so any source synced to your device — Nextcloud -via DAVx5, Google, local, WebCal subscriptions — shows up automatically. +Calendula is a modern, open-source calendar app for Android. It works +directly on the system calendar provider, so any source synced to your +device — Nextcloud via DAVx5, Google, local, WebCal subscriptions — shows up +automatically, and changes you make sync back the same way. -The differentiator is the design: real Material 3 Expressive throughout, with -dynamic color, expressive motion, and expressive shapes. +Create, edit and delete events, including recurring events with scoped +changes (only this event / this and all following / the whole series) and a +simple repeat picker. Calendula also delivers your event reminders as +notifications — tap one and you're on the event. -V1 is read-only. Event creation, editing, and deletion are planned for V2. +The differentiator is the design: real Material 3 Expressive throughout, +with dynamic color, expressive motion, and expressive shapes. Privacy: zero telemetry, no analytics, no network access — your data never leaves the device. diff --git a/fdroid-metadata/de.jeanlucmakiola.calendula/en-US/phoneScreenshots/01-week.png b/fdroid-metadata/de.jeanlucmakiola.calendula/en-US/phoneScreenshots/01-week.png new file mode 100644 index 0000000..f6c4d46 Binary files /dev/null and b/fdroid-metadata/de.jeanlucmakiola.calendula/en-US/phoneScreenshots/01-week.png differ diff --git a/fdroid-metadata/de.jeanlucmakiola.calendula/en-US/phoneScreenshots/02-month.png b/fdroid-metadata/de.jeanlucmakiola.calendula/en-US/phoneScreenshots/02-month.png new file mode 100644 index 0000000..71ad113 Binary files /dev/null and b/fdroid-metadata/de.jeanlucmakiola.calendula/en-US/phoneScreenshots/02-month.png differ diff --git a/fdroid-metadata/de.jeanlucmakiola.calendula/en-US/phoneScreenshots/03-day.png b/fdroid-metadata/de.jeanlucmakiola.calendula/en-US/phoneScreenshots/03-day.png new file mode 100644 index 0000000..9bc2582 Binary files /dev/null and b/fdroid-metadata/de.jeanlucmakiola.calendula/en-US/phoneScreenshots/03-day.png differ diff --git a/fdroid-metadata/de.jeanlucmakiola.calendula/en-US/phoneScreenshots/04-detail.png b/fdroid-metadata/de.jeanlucmakiola.calendula/en-US/phoneScreenshots/04-detail.png new file mode 100644 index 0000000..a31a29c Binary files /dev/null and b/fdroid-metadata/de.jeanlucmakiola.calendula/en-US/phoneScreenshots/04-detail.png differ diff --git a/fdroid-metadata/de.jeanlucmakiola.calendula/en-US/phoneScreenshots/05-edit.png b/fdroid-metadata/de.jeanlucmakiola.calendula/en-US/phoneScreenshots/05-edit.png new file mode 100644 index 0000000..fe491d5 Binary files /dev/null and b/fdroid-metadata/de.jeanlucmakiola.calendula/en-US/phoneScreenshots/05-edit.png differ diff --git a/fdroid-metadata/de.jeanlucmakiola.calendula/en-US/phoneScreenshots/06-onboarding.png b/fdroid-metadata/de.jeanlucmakiola.calendula/en-US/phoneScreenshots/06-onboarding.png new file mode 100644 index 0000000..d9a7356 Binary files /dev/null and b/fdroid-metadata/de.jeanlucmakiola.calendula/en-US/phoneScreenshots/06-onboarding.png differ