Files
calendula/docs/superpowers/plans/2026-06-11-03-write-support.md
Jean-Luc Makiola 9529f19c60 feat(write): event delete + WRITE_CALENDAR foundation (v1.1)
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>
2026-06-11 12:55:15 +02:00

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) → CalendarDataSourceContentResolver.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"):

  1. Permission-Strategie: WRITE_CALENDAR kommt 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).
  2. Read-only-Kalender respektieren: Calendars.CALENDAR_ACCESS_LEVEL wird mitgelesen (canModifyContents = Level ≥ CAL_ACCESS_CONTRIBUTOR). Edit/Delete-Actions erscheinen gar nicht erst für WebCal-Subscriptions, Geburtstags- und andere read-only-Kalender.
  3. Recurring Events: Löschen bietet "Nur dieser Termin" (Exception-Insert via Events.CONTENT_EXCEPTION_URI mit STATUS_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.
  4. 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.
  5. 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_CALENDAR ergänzen

Data layer:

  • Projections.kt: CALENDAR_ACCESS_LEVEL in CalendarProjection
  • Models.kt: CalendarSource.canModifyContents: Boolean (Default false). Kein neuer FailureReason — Delete-Fehler sind ein Snackbar-Fall, kein Full-Screen-Failure
  • CalendarMapper.kt: Access-Level → canModifyContents
  • CalendarDataSource: deleteEvent(eventId), deleteOccurrence(eventId, beginMillis) — Impl in AndroidCalendarDataSource (delete auf Events-URI bzw. Exception-Insert), WriteFailedException bei 0 rows / null-Uri
  • CalendarRepository(+Impl): beide Methoden durchreichen, auf io

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-Screen
  • EventDetailScreen: Edit/Delete nur wenn canModify; Delete → Confirm-Dialog (recurring: "Nur dieser Termin" / "Ganze Serie"), Erfolg → zurück, Fehler → Snackbar
  • Onboarding (PermissionScreen): RequestMultiplePermissions READ+WRITE, Gate bleibt READ; Copy-Anpassung (Footnote, Rationale-Body) DE+EN

Tests:

  • FakeCalendarDataSource: Write-Ops aufnehmen
  • CalendarRepositoryImplTest: 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): Long im DataSource (DTSTART/DTEND/EVENT_TIMEZONE, all-day in UTC)

v1.3 — Edit (Skizze)

  • Formular lädt EventDetail, Dirty-Check, update auf 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