feat(ics): import UI — open/receive .ics, 1-vs-many routing

Completes v2.7 Branch 2. Wires the import core into the app:

- Manifest ACTION_VIEW/SEND for text/calendar; MainActivity parses the
  incoming Uri (content/file only, so calendula:// deep-links don't match)
  and routes it through RootScreen → CalendarHost like the other one-shot
  intents.
- ImportViewModel reads + parses the file and routes by count: one event →
  the prefilled create form for review (EventEditViewModel.openImported,
  which freezes the reminder default so the file's reminders win); many →
  ImportScreen with a writable-calendar picker, then a bulk import (UID
  dedup) and a result summary.
- ImportScreen also surfaces parser warnings (skipped recurrence overrides,
  ignored attendees, unknown-timezone fallback). Strings EN+DE.

Package is ui.imports (not ui.import — Java keyword). lint + test +
assembleDebug green. No v2.7 tag until on-device review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 15:20:29 +02:00
parent e1c2e9f2e5
commit 3dfc96718c
12 changed files with 496 additions and 26 deletions

View File

@@ -80,43 +80,43 @@ den Rest der Datei durch.
## Tasks
**Parser-Engine (`domain/ics/`, reine Kotlin, JVM-Tests):**
- [ ] `IcsText`: `unescapeText` + `unfold(lines)` ergänzen (+ Test)
- [ ] `IcsLineParser`: `NAME;PARAM=v;PARAM=v:VALUE` → (name, params-map, value);
- [x] `IcsText`: `unescapeText` + `unfold(lines)` ergänzen (+ Test)
- [x] `IcsLineParser`: `NAME;PARAM=v;PARAM=v:VALUE` → (name, params-map, value);
Param-Werte ggf. in Quotes; Test (Kantenfälle: Doppelpunkt im Wert,
gequotete Params)
- [ ] `ParsedIcsEvent` (wie `IcsEvent`, aber `uid: String?`) + Datum/Zeit-Parser
- [x] `ParsedIcsEvent` (wie `IcsEvent`, aber `uid: String?`) + Datum/Zeit-Parser
(`VALUE=DATE` / `…Z` / `TZID` → Instant + isAllDay + zoneId)
- [ ] `IcsParser.parse(text)``IcsParseResult(events, warnings)`: VCALENDAR/
- [x] `IcsParser.parse(text)``IcsParseResult(events, warnings)`: VCALENDAR/
VEVENT-Walk, skip-and-report für `RECURRENCE-ID`/unbekannte `VTIMEZONE`/
Attendees; ein defektes VEVENT killt nicht den Rest. **Round-trip-Test**
gegen `IcsWriter`-Ausgabe (all-day, getimt, wiederkehrend+TZID, Reminder)
+ Fremd-Quirks (gefaltete Zeilen, fehlende UID, `PT…`-Trigger)
**Datenschicht (`data/calendar/` + `data/ics/`):**
- [ ] `ParsedIcsEvent.toEventForm(zone)` (Einzel-Öffnen → `EventForm`); Test
- [ ] Datasource: `existingUids(calendarId)` (Query `Events.UID_2445`) +
- [x] `ParsedIcsEvent.toEventForm(zone)` (Einzel-Öffnen → `EventForm`); Test
- [x] Datasource: `existingUids(calendarId)` (Query `Events.UID_2445`) +
`insertImported(event, calendarId)` (Insert mit vorgegebener/erzeugter UID)
- [ ] Repository `importEvents(targetCalendarId, events)``ImportSummary`
- [x] Repository `importEvents(targetCalendarId, events)``ImportSummary`
(imported / skippedDuplicate / skippedUnsupported); UID-Dedup; Test mit
Fake-Datasource
- [ ] `IcsImporter` (`data/ics/`): `Uri` → Text lesen (UTF-8, `contentResolver`)
- [x] `IcsImporter` (`data/ics/`): `Uri` → Text lesen (UTF-8, `contentResolver`)
**Intent + Routing:**
- [ ] Manifest: `ACTION_VIEW`/`ACTION_SEND`, MIME `text/calendar`, `.ics`-
- [x] Manifest: `ACTION_VIEW`/`ACTION_SEND`, MIME `text/calendar`, `.ics`-
Pfadmuster (`file`/`content`); `MainActivity` parst eingehenden `Uri`
- [ ] Routing in `CalendarHost`: 1 Event → Erstellen-Formular vorausgefüllt;
- [x] Routing in `CalendarHost`: 1 Event → Erstellen-Formular vorausgefüllt;
>1 → Bulk-Import-Screen; 0 → Hinweis
**UI:**
- [ ] Bulk-Import-Screen: Ziel-Kalender-Picker (OptionCard, nur beschreibbare),
- [x] Bulk-Import-Screen: Ziel-Kalender-Picker (OptionCard, nur beschreibbare),
Anzahl/Vorschau, Import-Button → `ImportSummary` als Ergebnis
- [ ] Einzel-Öffnen: `EventEditScreen` mit vorausgefülltem Formular (neuer
- [x] Einzel-Öffnen: `EventEditScreen` mit vorausgefülltem Formular (neuer
Prefill-Pfad im `EventEditViewModel`, ohne `eventId`)
- [ ] Strings DE+EN: Import-Titel, Ziel-Auswahl, Ergebnis-Plurals
- [x] Strings DE+EN: Import-Titel, Ziel-Auswahl, Ergebnis-Plurals
(importiert / übersprungen-vorhanden / übersprungen-nicht-unterstützt),
leere-Datei-Hinweis
**Abschluss:**
- [ ] `./gradlew lint test assembleDebug` grün
- [ ] CHANGELOG (`[Unreleased]`), ROADMAP/STATE; v2.7 cut **erst** wenn beide
- [x] `./gradlew lint test assembleDebug` grün
- [ ] CHANGELOG done; ROADMAP/STATE pending; v2.7 cut **erst** wenn beide
Branches gemerged sind und On-Device-Review durch ist