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>
107 lines
5.4 KiB
Markdown
107 lines
5.4 KiB
Markdown
# 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"):**
|
|
|
|
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:**
|
|
- [x] `AndroidManifest.xml`: `WRITE_CALENDAR` ergänzen
|
|
|
|
**Data layer:**
|
|
- [x] `Projections.kt`: `CALENDAR_ACCESS_LEVEL` in `CalendarProjection`
|
|
- [x] `Models.kt`: `CalendarSource.canModifyContents: Boolean` (Default `false`).
|
|
Kein neuer `FailureReason` — Delete-Fehler sind ein Snackbar-Fall, kein
|
|
Full-Screen-Failure
|
|
- [x] `CalendarMapper.kt`: Access-Level → `canModifyContents`
|
|
- [x] `CalendarDataSource`: `deleteEvent(eventId)`, `deleteOccurrence(eventId, beginMillis)`
|
|
— Impl in `AndroidCalendarDataSource` (`delete` auf Events-URI bzw.
|
|
Exception-Insert), `WriteFailedException` bei 0 rows / null-Uri
|
|
- [x] `CalendarRepository(+Impl)`: beide Methoden durchreichen, auf `io`
|
|
|
|
**UI:**
|
|
- [x] `EventDetailUiState.Success.canModify` (Kalender-Lookup im ViewModel)
|
|
- [x] `EventDetailViewModel`: `delete(mode)` mit eigenem One-Shot-State
|
|
(Idle/Deleting/Deleted/Failed); `SecurityException` → kontextueller
|
|
WRITE-Request statt Failure-Screen
|
|
- [x] `EventDetailScreen`: Edit/Delete nur wenn `canModify`; Delete →
|
|
Confirm-Dialog (recurring: "Nur dieser Termin" / "Ganze Serie"),
|
|
Erfolg → zurück, Fehler → Snackbar
|
|
- [x] Onboarding (`PermissionScreen`): `RequestMultiplePermissions` READ+WRITE,
|
|
Gate bleibt READ; Copy-Anpassung (Footnote, Rationale-Body) DE+EN
|
|
|
|
**Tests:**
|
|
- [x] `FakeCalendarDataSource`: Write-Ops aufnehmen
|
|
- [x] `CalendarRepositoryImplTest`: delete-Pfade (Erfolg, Fehler)
|
|
- [x] `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
|