feat(ios): Kalender des Geraets anzeigen und bespielen

Die Kalenderveroeffentlichung in Outlook ist oft von der IT gesperrt, und
iCloud laesst sich nicht ueber einen ICS-Link einbinden. Das iPhone kennt
beide Konten aber bereits, also liest die App sie per EventKit direkt.

Die Termine bleiben auf dem Geraet und gehen nie an den Server. Im Web und
auf anderen Geraeten erscheinen sie deshalb nicht — dafuer braucht es keine
fremden Zugangsdaten und keine Kopie fremder Termindaten in unserer
Datenbank.

Standardmaessig aus: der Schalter steht unter Konten, und erst beim
Einschalten fragt EventKit nach Zugriff. Wer die Funktion nicht will, sieht
keine Berechtigungsabfrage.

Kaum neuer Code noetig, weil CalEvent schon source/calendarId/readOnly hat
und die Sichtbarkeits-, Sortier- und Ausblendlogik an
calendarKey(source:calendarId:) haengt: die Quelle "device" rutscht ohne
Umbau hinein. WritableCalendar bekommt nur ein deviceId, weil EventKit
Zeichenketten statt Zeilen-IDs vergibt.

Lesen, Anlegen, Aendern und Loeschen gehen ueber dieselben Ansichten wie bei
allen anderen Quellen. Nur die Gruppenansicht bleibt aussen vor, die ist
serverseitig zusammengesetzt.

Statt den Store durch zwei Views zu reichen, damit der Kalender nach dem
Einschalten nachlaedt, zaehlt der Dienst eine Generation hoch; ein
abweichender Stand gilt im Cache als Treffer-Fehlschlag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-23 14:57:15 +02:00
co-authored by Claude Opus 5
parent 2db4b42fa1
commit c811486ea7
8 changed files with 254 additions and 3 deletions
@@ -30,6 +30,8 @@ struct AccountsView: View {
@State private var birthdayNotify = -1
@AppStorage("appLanguage") private var appLang = "system"
@AppStorage(DeviceCalendarService.enabledKey) private var deviceCalendarsEnabled = false
@State private var deviceCalendarAccessGranted = false
var body: some View {
NavigationStack {
@@ -42,6 +44,7 @@ struct AccountsView: View {
caldavSection
localSection
birthdayContactsSection
deviceCalendarSection
icalSection
googleSection
haSection
@@ -235,6 +238,30 @@ struct AccountsView: View {
localCalendars.first { $0.isBirthday && $0.owned }
}
/// Calendars the phone already has (iCloud, Outlook/Exchange, Google).
/// They stay on the device — the server never sees them — so this is the
/// way in when publishing an Outlook calendar is blocked by company policy.
@ViewBuilder var deviceCalendarSection: some View {
Section {
Toggle(L10n.t("device.calendars.enable", appLang), isOn: $deviceCalendarsEnabled)
if deviceCalendarsEnabled && !deviceCalendarAccessGranted {
Text(L10n.t("device.calendars.denied", appLang))
.font(.caption).foregroundStyle(.secondary)
}
} header: {
Text(L10n.t("device.calendars.header", appLang))
} footer: {
Text(L10n.t("device.calendars.desc", appLang))
}
.onChange(of: deviceCalendarsEnabled) { _, on in
guard on else { return }
// Granting access bumps the service generation, which makes the
// calendar reload instead of serving its pre-device cache.
Task { deviceCalendarAccessGranted = await DeviceCalendarService.shared.requestAccess() }
}
.task { deviceCalendarAccessGranted = DeviceCalendarService.shared.hasAccess }
}
@ViewBuilder var birthdayContactsSection: some View {
Section {
if let cal = birthdayCalendar {