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:
co-authored by
Claude Opus 5
parent
2db4b42fa1
commit
c811486ea7
@@ -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 {
|
||||
|
||||
@@ -264,6 +264,8 @@ struct EventDetailSheet: View {
|
||||
// calendarId looks like "homeassistant-42" → numeric DB id 42
|
||||
let calId = Int(event.calendarId.replacingOccurrences(of: "homeassistant-", with: "")) ?? 0
|
||||
try await api.deleteHAEvent(calendarId: calId, uid: event.id)
|
||||
case DeviceCalendarService.source:
|
||||
try DeviceCalendarService.shared.delete(eventId: event.id)
|
||||
default:
|
||||
let calId = Int(event.calendarId)
|
||||
try await api.deleteCalDAVEvent(uid: event.id, url: event.url, calendarId: calId)
|
||||
|
||||
@@ -56,7 +56,7 @@ struct EventEditorSheet: View {
|
||||
/// controls and explain that they won't fire.
|
||||
private var remindersDisabled: Bool {
|
||||
guard let cal = selectedCal else { return false }
|
||||
let key = CalendarStore.calendarKey(source: cal.source, calendarId: String(cal.numericId))
|
||||
let key = CalendarStore.calendarKey(source: cal.source, calendarId: cal.calendarIdString)
|
||||
return store.reminderDisabledKeys.contains(key)
|
||||
}
|
||||
|
||||
@@ -293,6 +293,8 @@ struct EventEditorSheet: View {
|
||||
if ev.source == "homeassistant" {
|
||||
let num = ev.calendarId.replacingOccurrences(of: "homeassistant-", with: "")
|
||||
selectedCalendarId = "ha-\(num)"
|
||||
} else if ev.source == DeviceCalendarService.source {
|
||||
selectedCalendarId = "device-\(ev.calendarId)"
|
||||
} else {
|
||||
selectedCalendarId = ev.calendarId
|
||||
}
|
||||
@@ -347,6 +349,10 @@ struct EventEditorSheet: View {
|
||||
try await api.createHAEvent(calendarId: haCalId, title: title,
|
||||
start: start, end: end, isAllDay: isAllDay,
|
||||
location: location, description: notes)
|
||||
case DeviceCalendarService.source:
|
||||
try DeviceCalendarService.shared.update(
|
||||
eventId: ev.id, title: title, start: start, end: end,
|
||||
isAllDay: isAllDay, location: location, notes: notes)
|
||||
default: // caldav
|
||||
let calId = Int(ev.calendarId)
|
||||
try await api.updateCalDAVEvent(uid: ev.id, url: ev.url, calendarId: calId,
|
||||
@@ -370,6 +376,10 @@ struct EventEditorSheet: View {
|
||||
try await api.createHAEvent(calendarId: cal.numericId, title: title,
|
||||
start: start, end: end, isAllDay: isAllDay,
|
||||
location: location, description: notes)
|
||||
case DeviceCalendarService.source:
|
||||
try DeviceCalendarService.shared.create(
|
||||
calendarId: cal.calendarIdString, title: title, start: start, end: end,
|
||||
isAllDay: isAllDay, location: location, notes: notes)
|
||||
default: // caldav
|
||||
try await api.createCalDAVEvent(calendarId: cal.numericId, title: title,
|
||||
start: start, end: end, isAllDay: isAllDay,
|
||||
|
||||
@@ -244,6 +244,14 @@ struct CalendarFilterContent: View {
|
||||
for sub in icalSubs { add("ical", sub.id, sub.name, sub.color) }
|
||||
for acc in googleAccounts { for cal in acc.calendars ?? [] { add("google", cal.id, cal.name, cal.color ?? "#4285f4") } }
|
||||
for acc in haAccounts { for cal in acc.calendars ?? [] { add("homeassistant", cal.id, cal.name, cal.color ?? "#46bdc6") } }
|
||||
// Device calendars key on an EventKit identifier, not a server row id.
|
||||
for cal in DeviceCalendarService.shared.calendars() {
|
||||
let key = CalendarStore.calendarKey(source: DeviceCalendarService.source,
|
||||
calendarId: cal.calendarIdentifier)
|
||||
if b.contains(key) { continue }
|
||||
combined.append(CalRow(id: key, name: cal.title, colorHex: "#8E8E93",
|
||||
readOnly: !cal.allowsContentModifications))
|
||||
}
|
||||
|
||||
allKeys = Set(combined.map(\.id))
|
||||
let orderedKeys = store.ordered(combined.map(\.id))
|
||||
|
||||
Reference in New Issue
Block a user