Bring CalendarrKit into the repo and fix the iOS build
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
dccf8671b2
commit
ab0c64a9e1
@@ -68,6 +68,10 @@ class CalendarStore {
|
|||||||
var syncErrors: [SyncError] = []
|
var syncErrors: [SyncError] = []
|
||||||
var weekStartsOnMonday = true
|
var weekStartsOnMonday = true
|
||||||
var writableCalendars: [WritableCalendar] = []
|
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
|
// When set, the calendar shows the group's combined overlay instead of the
|
||||||
// user's own events. nil = personal view.
|
// user's own events. nil = personal view.
|
||||||
var activeGroup: CalGroup? = nil
|
var activeGroup: CalGroup? = nil
|
||||||
@@ -634,29 +638,53 @@ class CalendarStore {
|
|||||||
// MARK: – Writable calendars
|
// MARK: – Writable calendars
|
||||||
|
|
||||||
func loadWritableCalendars(api: CalendarrAPI) async {
|
func loadWritableCalendars(api: CalendarrAPI) async {
|
||||||
async let localCals = (try? await api.getLocalCalendars()) ?? []
|
// Each source is fetched independently and reports its own failure.
|
||||||
async let caldavAccs = (try? await api.getCalDAVAccounts()) ?? []
|
// `try?` used to discard all four, so a source that errored looked
|
||||||
async let googleCals = (try? await api.getGoogleCalendars()) ?? []
|
// exactly like a source with nothing in it — which is how a missing
|
||||||
async let haCals = (try? await api.getHACalendars()) ?? []
|
// 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] = []
|
var result: [WritableCalendar] = []
|
||||||
// Skip read-only shared calendars — offering them in the event editor
|
// Skip read-only shared calendars — offering them in the event editor
|
||||||
// only leads to a 403 on save. Own + read_write (incl. group) stay.
|
// 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))
|
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 {
|
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))
|
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))
|
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))
|
result.append(WritableCalendar(id: "ha-\(id)", name: "\(accName) – \(name)", color: color, source: "homeassistant", numericId: id))
|
||||||
}
|
}
|
||||||
writableCalendars = result
|
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<T>(_ source: String,
|
||||||
|
_ work: () async throws -> [T]) async -> ([T], String?) {
|
||||||
|
do { return (try await work(), nil) }
|
||||||
|
catch { return ([], error.localizedDescription) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: – Query helpers
|
// MARK: – Query helpers
|
||||||
|
|||||||
@@ -35,6 +35,18 @@ struct EventEditorSheet: View {
|
|||||||
private var isEditing: Bool { editingEvent != nil }
|
private var isEditing: Bool { editingEvent != nil }
|
||||||
private var isCopying: Bool { copyFrom != nil && 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? {
|
private var selectedCal: WritableCalendar? {
|
||||||
store.writableCalendars.first { $0.id == selectedCalendarId }
|
store.writableCalendars.first { $0.id == selectedCalendarId }
|
||||||
}
|
}
|
||||||
@@ -80,7 +92,9 @@ struct EventEditorSheet: View {
|
|||||||
Text(L10n.t("event.no_writable", appLang))
|
Text(L10n.t("event.no_writable", appLang))
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
} else {
|
}
|
||||||
|
sourceErrorRows
|
||||||
|
if !store.writableCalendars.isEmpty {
|
||||||
Picker(L10n.t("event.calendar_picker", appLang), selection: $selectedCalendarId) {
|
Picker(L10n.t("event.calendar_picker", appLang), selection: $selectedCalendarId) {
|
||||||
ForEach(store.writableCalendars) { cal in
|
ForEach(store.writableCalendars) { cal in
|
||||||
HStack {
|
HStack {
|
||||||
@@ -122,7 +136,7 @@ struct EventEditorSheet: View {
|
|||||||
}
|
}
|
||||||
.disabled(remindersDisabled)
|
.disabled(remindersDisabled)
|
||||||
|
|
||||||
Section(L10n.t("event.attachments", appLang)) {
|
Section {
|
||||||
ForEach(existingAttachments.filter { !removedAttachmentIds.contains($0.id) }) { att in
|
ForEach(existingAttachments.filter { !removedAttachmentIds.contains($0.id) }) { att in
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: att.symbolName).foregroundStyle(.secondary)
|
Image(systemName: att.symbolName).foregroundStyle(.secondary)
|
||||||
@@ -156,6 +170,8 @@ struct EventEditorSheet: View {
|
|||||||
Label(L10n.t("event.attachment_add", appLang), systemImage: "paperclip")
|
Label(L10n.t("event.attachment_add", appLang), systemImage: "paperclip")
|
||||||
}
|
}
|
||||||
.disabled(attachmentSlotsUsed >= 10)
|
.disabled(attachmentSlotsUsed >= 10)
|
||||||
|
} header: {
|
||||||
|
Text(L10n.t("event.attachments", appLang))
|
||||||
} footer: {
|
} footer: {
|
||||||
Text(L10n.t("event.attachment_hint", appLang))
|
Text(L10n.t("event.attachment_hint", appLang))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user