First slice of milestone 2 (write support), per the new plan in docs/superpowers/plans/2026-06-11-03-write-support.md: - Delete from the event detail screen with confirmation; recurring events choose "only this event" (cancelled exception via CONTENT_EXCEPTION_URI, series survives) or "all events in the series" (Events-row delete) - WRITE_CALENDAR in the manifest; onboarding requests read+write in one system dialog but only read gates the app — declining write keeps it usable read-only. v1.0 installs get a contextual write request on their first delete - CALENDAR_ACCESS_LEVEL is read into CalendarSource.canModifyContents; read-only calendars (WebCal, birthdays, …) show no write actions. The no-op placeholder Edit button is removed until edit ships (v1.3) - Onboarding copy drops the now-false "read-only" claim (DE+EN) - Tests: repository delete delegation/error propagation, access-level mapping; FakeCalendarDataSource grows write ops Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5.4 KiB
Calendula - Plan 03: Write Support (Milestone 2 / v2.0)
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Calendula kann Events anlegen, bearbeiten und löschen — direkt über
CalendarContract-Writes, ohne eigene DB. Der V1-Spec dient als Leitplanke,
nicht als Gesetz: Ausgeliefert wird in vier Slices (v1.1 → v2.0), jeder Slice
ist für sich releasebar und lässt ./gradlew lint test assembleDebug grün.
Architecture: Writes laufen durch dieselbe Schichtung wie Reads:
ui/ → CalendarRepository (Interface) → CalendarDataSource →
ContentResolver.insert/update/delete. Kein neuer Layer, keine Transaktions-
Abstraktion — der Provider notified nach jedem Write selbst, der bestehende
ContentObserver-Tick aktualisiert alle Views automatisch (F3 gilt unverändert).
Domain bleibt pure Kotlin.
Leitentscheidungen (Abweichungen / Präzisierungen ggü. Spec §2 "V2"):
- Permission-Strategie:
WRITE_CALENDARkommt ins Manifest. Das Onboarding fragt READ+WRITE zusammen an (eine System-Dialog-Gruppe), zwingend bleibt nur READ — wer Write ablehnt, nutzt die App weiter read-only. v1.0-Upgrader (haben nur READ) bekommen den WRITE-Request kontextuell beim ersten Schreib-Versuch. Onboarding-Footnote verliert die "Nur Lesezugriff"- Behauptung (wäre mit Manifest-Eintrag gelogen). - Read-only-Kalender respektieren:
Calendars.CALENDAR_ACCESS_LEVELwird mitgelesen (canModifyContents= Level ≥CAL_ACCESS_CONTRIBUTOR). Edit/Delete-Actions erscheinen gar nicht erst für WebCal-Subscriptions, Geburtstags- und andere read-only-Kalender. - Recurring Events: Löschen bietet "Nur dieser Termin" (Exception-Insert
via
Events.CONTENT_EXCEPTION_URImitSTATUS_CANCELED+ORIGINAL_INSTANCE_TIME) vs. "Ganze Serie" (Delete der Events-Row). Bearbeiten startet mit "ganze Serie"; Occurrence-Edit (Exception mit neuen Werten) folgt erst, wenn das Serien-Edit stabil ist. - Kein RRULE-Editor in v1.2: Create startet ohne Wiederholungs-UI (einmalige Events). Ein einfacher Recurrence-Picker (täglich/wöchentlich/ monatlich/jährlich + Ende) kommt mit v1.3/v2.0.
- Conflict UX (Spec V2 "event modified externally during edit"): kein Locking. Beim Speichern wird gegen die beim Laden gemerkte Row verglichen (Dirty-Check auf den editierten Feldern); bei externem Konflikt Dialog "Überschreiben / Verwerfen". Mehr ist YAGNI.
Slices
| Slice | Inhalt | Status |
|---|---|---|
| v1.1 | Write-Fundament: WRITE_CALENDAR, canModifyContents, Delete (Serie + einzelnes Vorkommen) |
in Arbeit |
| v1.2 | Create: Event-Formular (Titel, Kalender, ganztägig, Start/Ende, Ort, Beschreibung), FAB, Default-Kalender-Pref | offen |
| v1.3 | Edit: Formular wiederverwendet, Serien-Edit, Reminder-Edit, einfacher Recurrence-Picker | offen |
| v2.0 | Quick-Add, Occurrence-Edit, Konflikt-Dialog, Polish-Pass, Release | offen |
v1.1 — Write-Fundament + Delete
Build/Manifest:
AndroidManifest.xml:WRITE_CALENDARergänzen
Data layer:
Projections.kt:CALENDAR_ACCESS_LEVELinCalendarProjectionModels.kt:CalendarSource.canModifyContents: Boolean(Defaultfalse). Kein neuerFailureReason— Delete-Fehler sind ein Snackbar-Fall, kein Full-Screen-FailureCalendarMapper.kt: Access-Level →canModifyContentsCalendarDataSource:deleteEvent(eventId),deleteOccurrence(eventId, beginMillis)— Impl inAndroidCalendarDataSource(deleteauf Events-URI bzw. Exception-Insert),WriteFailedExceptionbei 0 rows / null-UriCalendarRepository(+Impl): beide Methoden durchreichen, aufio
UI:
EventDetailUiState.Success.canModify(Kalender-Lookup im ViewModel)EventDetailViewModel:delete(mode)mit eigenem One-Shot-State (Idle/Deleting/Deleted/Failed);SecurityException→ kontextueller WRITE-Request statt Failure-ScreenEventDetailScreen: Edit/Delete nur wenncanModify; Delete → Confirm-Dialog (recurring: "Nur dieser Termin" / "Ganze Serie"), Erfolg → zurück, Fehler → Snackbar- Onboarding (
PermissionScreen):RequestMultiplePermissionsREAD+WRITE, Gate bleibt READ; Copy-Anpassung (Footnote, Rationale-Body) DE+EN
Tests:
FakeCalendarDataSource: Write-Ops aufnehmenCalendarRepositoryImplTest: delete-Pfade (Erfolg, Fehler)CalendarMapperTest: Access-Level-Mapping
v1.2 — Create (Skizze)
EventForm-Domain-Modell + Validierung (Ende > Start, Titel-Fallback)EventEditScreen(ein Formular für Create+Edit), M3-Date/Time-Picker- FAB auf allen drei Hauptansichten, vorbelegt mit sichtbarem Tag/Slot
CalendarPrefs.defaultCalendarId+ Auswahl im Formular (nur beschreibbare Kalender anbieten)insertEvent(form): Longim DataSource (DTSTART/DTEND/EVENT_TIMEZONE, all-day in UTC)
v1.3 — Edit (Skizze)
- Formular lädt
EventDetail, Dirty-Check,updateauf Events-Row - Reminder hinzufügen/entfernen (
Reminders-Insert/Delete) - Einfacher Recurrence-Picker (FREQ + INTERVAL + UNTIL/COUNT)
v2.0 — Abschluss (Skizze)
- Quick-Add-Sheet (Titel + Zeit, Rest Defaults)
- Occurrence-Edit (Exception mit geänderten Werten)
- Konflikt-Dialog beim Speichern
- Changelog, F-Droid-Metadaten, Release-Tag