From ab0c64a9e16da5ee0fe7a18e27a54a101cfdd1ba Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Mon, 19 Oct 2026 05:38:06 +0200 Subject: [PATCH] Bring CalendarrKit into the repo and fix the iOS build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The monorepo's Xcode project referenced ../CalendarrKit, but the package was never brought across: it is the one repo that never had a remote, so it existed only on one machine. Added as a subtree at the root, where that existing relative path resolves, so the project needs no change. History is preserved — the commits explaining why the snapshot colours sit flat on the wire and why the package must stay Foundation-only are the kind of thing that cannot be reconstructed later. The iOS target also did not compile on beta, independent of any of this. The attachments section combined a string title with a footer, and SwiftUI has no initializer for that pair: the title then binds as the first positional argument of Section(content:footer:), which is what the three misleading "generic parameter 'Content'" errors were. Rewritten as header:/footer:, the way the reminders section directly above already does it. Carries over the pending change from the archived iOS repo: each calendar source now reports its own failure instead of being swallowed by `try?`, so an absent Home Assistant calendar can finally be diagnosed from the app. Co-Authored-By: Claude Opus 5 --- ios/Calendarr iOS/Models/CalendarStore.swift | 44 +++++++++++++++---- .../Views/Calendar/EventEditorSheet.swift | 20 ++++++++- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/ios/Calendarr iOS/Models/CalendarStore.swift b/ios/Calendarr iOS/Models/CalendarStore.swift index 266c20c..a2fb2ab 100644 --- a/ios/Calendarr iOS/Models/CalendarStore.swift +++ b/ios/Calendarr iOS/Models/CalendarStore.swift @@ -68,6 +68,10 @@ class CalendarStore { var syncErrors: [SyncError] = [] var weekStartsOnMonday = true var writableCalendars: [WritableCalendar] = [] + /// Why a source contributed nothing, keyed by source name. Empty when every + /// source answered. Surfaced in the event editor, because an empty picker + /// with no explanation is indistinguishable from "you own no calendars". + var writableCalendarErrors: [String: String] = [:] // When set, the calendar shows the group's combined overlay instead of the // user's own events. nil = personal view. var activeGroup: CalGroup? = nil @@ -634,29 +638,53 @@ class CalendarStore { // MARK: – Writable calendars func loadWritableCalendars(api: CalendarrAPI) async { - async let localCals = (try? await api.getLocalCalendars()) ?? [] - async let caldavAccs = (try? await api.getCalDAVAccounts()) ?? [] - async let googleCals = (try? await api.getGoogleCalendars()) ?? [] - async let haCals = (try? await api.getHACalendars()) ?? [] + // Each source is fetched independently and reports its own failure. + // `try?` used to discard all four, so a source that errored looked + // exactly like a source with nothing in it — which is how a missing + // Home Assistant calendar can go unexplained for a long time. + var errors: [String: String] = [:] + + async let localResult = Self.attempt("local") { try await api.getLocalCalendars() } + async let caldavResult = Self.attempt("caldav") { try await api.getCalDAVAccounts() } + async let googleResult = Self.attempt("google") { try await api.getGoogleCalendars() } + async let haResult = Self.attempt("homeassistant"){ try await api.getHACalendars() } + + let (localCals, localErr) = await localResult + let (caldavAccs, caldavErr) = await caldavResult + let (googleCals, googleErr) = await googleResult + let (haCals, haErr) = await haResult + for (source, message) in [("local", localErr), ("caldav", caldavErr), + ("google", googleErr), ("homeassistant", haErr)] { + if let message { errors[source] = message } + } var result: [WritableCalendar] = [] // Skip read-only shared calendars — offering them in the event editor // only leads to a 403 on save. Own + read_write (incl. group) stay. - for cal in await localCals where cal.owned || cal.permission == "read_write" { + for cal in localCals where cal.owned || cal.permission == "read_write" { result.append(WritableCalendar(id: "local-\(cal.id)", name: cal.name, color: cal.color, source: "local", numericId: cal.id)) } - for acc in await caldavAccs where acc.enabled { + for acc in caldavAccs where acc.enabled { for cal in acc.calendars ?? [] where cal.enabled { result.append(WritableCalendar(id: "caldav-\(cal.id)", name: "\(acc.name) – \(cal.name)", color: cal.color ?? acc.color, source: "caldav", numericId: cal.id)) } } - for (email, id, name, color) in await googleCals { + for (email, id, name, color) in googleCals { result.append(WritableCalendar(id: "google-\(id)", name: "\(email) – \(name)", color: color, source: "google", numericId: id)) } - for (accName, id, name, color) in await haCals { + for (accName, id, name, color) in haCals { result.append(WritableCalendar(id: "ha-\(id)", name: "\(accName) – \(name)", color: color, source: "homeassistant", numericId: id)) } writableCalendars = result + writableCalendarErrors = errors + } + + /// Run one source fetch, returning its result or the reason it produced + /// nothing. Never throws: one broken source must not empty the picker. + private static func attempt(_ source: String, + _ work: () async throws -> [T]) async -> ([T], String?) { + do { return (try await work(), nil) } + catch { return ([], error.localizedDescription) } } // MARK: – Query helpers diff --git a/ios/Calendarr iOS/Views/Calendar/EventEditorSheet.swift b/ios/Calendarr iOS/Views/Calendar/EventEditorSheet.swift index 6b5ef33..533dc34 100644 --- a/ios/Calendarr iOS/Views/Calendar/EventEditorSheet.swift +++ b/ios/Calendarr iOS/Views/Calendar/EventEditorSheet.swift @@ -35,6 +35,18 @@ struct EventEditorSheet: View { private var isEditing: Bool { editingEvent != nil } private var isCopying: Bool { copyFrom != nil && editingEvent == nil } + /// Sources that failed to answer, named instead of silently leaving their + /// calendars out of the picker. + @ViewBuilder + private var sourceErrorRows: some View { + let failures = store.writableCalendarErrors.sorted { $0.key < $1.key } + ForEach(failures, id: \.key) { failure in + Label("\(failure.key): \(failure.value)", systemImage: "exclamationmark.triangle") + .foregroundStyle(.orange) + .font(.caption) + } + } + private var selectedCal: WritableCalendar? { store.writableCalendars.first { $0.id == selectedCalendarId } } @@ -80,7 +92,9 @@ struct EventEditorSheet: View { Text(L10n.t("event.no_writable", appLang)) .foregroundStyle(.secondary) .font(.callout) - } else { + } + sourceErrorRows + if !store.writableCalendars.isEmpty { Picker(L10n.t("event.calendar_picker", appLang), selection: $selectedCalendarId) { ForEach(store.writableCalendars) { cal in HStack { @@ -122,7 +136,7 @@ struct EventEditorSheet: View { } .disabled(remindersDisabled) - Section(L10n.t("event.attachments", appLang)) { + Section { ForEach(existingAttachments.filter { !removedAttachmentIds.contains($0.id) }) { att in HStack { Image(systemName: att.symbolName).foregroundStyle(.secondary) @@ -156,6 +170,8 @@ struct EventEditorSheet: View { Label(L10n.t("event.attachment_add", appLang), systemImage: "paperclip") } .disabled(attachmentSlotsUsed >= 10) + } header: { + Text(L10n.t("event.attachments", appLang)) } footer: { Text(L10n.t("event.attachment_hint", appLang)) }