From 815f2cf01a07408ece4823daf4b87d0be610f2a7 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Sun, 31 May 2026 21:05:31 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20iOS=20Kalenderfarben=20=C3=A4nderbar=20?= =?UTF-8?q?+=20Top-Bar=20entzerrt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Kalenderverwaltung: tappbarer ColorPicker pro Kalender (lokal/iCal direkt; CalDAV/Google/HA klappen ihre Unterkalender mit je eigenem Farbwähler auf). Neue API: updateLocalCalendarColor, updateICalColor, setCalendarColor (caldav/google/homeassistant) -> PUT …/{id} {color}. Geteilte Kalender read-only (nur Besitzer). - Top-Bar: Gruppen-Umschalter nur bei vorhandenen Gruppen, "Heute" nicht mehr quetschbar (fixedSize), kompaktere Icons -> "Heute" wird nicht mehr zu "H…". Co-Authored-By: Claude Opus 4.8 --- Calendarr iOS/Services/CalendarrAPI.swift | 22 ++++ Calendarr iOS/Views/AccountsView.swift | 101 +++++++++++++----- .../Views/Calendar/CalendarHostView.swift | 22 ++-- 3 files changed, 109 insertions(+), 36 deletions(-) diff --git a/Calendarr iOS/Services/CalendarrAPI.swift b/Calendarr iOS/Services/CalendarrAPI.swift index a50230c..f87631b 100644 --- a/Calendarr iOS/Services/CalendarrAPI.swift +++ b/Calendarr iOS/Services/CalendarrAPI.swift @@ -400,6 +400,28 @@ class CalendarrAPI { body: ["enabled": !hidden, "sidebar_hidden": hidden]) } + // MARK: – Calendar colour + + func updateLocalCalendarColor(id: Int, color: String) async throws { + _ = try await request("/api/local/calendars/\(id)", method: "PUT", body: ["color": color]) + } + + func updateICalColor(id: Int, color: String) async throws { + _ = try await request("/api/ical/subscriptions/\(id)", method: "PUT", body: ["color": color]) + } + + /// Set a per-calendar colour for server-managed sources (caldav/google/homeassistant). + func setCalendarColor(source: String, calendarId: Int, color: String) async throws { + let path: String + switch source { + case "caldav": path = "/api/caldav/calendars/\(calendarId)" + case "google": path = "/api/google/calendars/\(calendarId)" + case "homeassistant": path = "/api/homeassistant/calendars/\(calendarId)" + default: return + } + _ = try await request(path, method: "PUT", body: ["color": color]) + } + // MARK: – Profile (display name / login name / email) /// Update profile fields. A login-name change returns a fresh token (the old diff --git a/Calendarr iOS/Views/AccountsView.swift b/Calendarr iOS/Views/AccountsView.swift index 3679ba8..06beb11 100644 --- a/Calendarr iOS/Views/AccountsView.swift +++ b/Calendarr iOS/Views/AccountsView.swift @@ -131,16 +131,22 @@ struct AccountsView: View { .foregroundStyle(.secondary) } else { ForEach(caldavAccounts) { acc in - HStack { - Circle() - .fill(Color(hex: acc.color)) - .frame(width: 12, height: 12) - VStack(alignment: .leading, spacing: 2) { - Text(acc.name).font(.body) - Text(acc.url) - .font(.caption) - .foregroundStyle(.secondary) - .lineLimit(1) + VStack(alignment: .leading, spacing: 8) { + HStack { + Circle().fill(Color(hex: acc.color)).frame(width: 12, height: 12) + VStack(alignment: .leading, spacing: 2) { + Text(acc.name).font(.body) + Text(acc.url).font(.caption).foregroundStyle(.secondary).lineLimit(1) + } + } + ForEach(acc.calendars ?? []) { cal in + HStack { + CalendarColorDot(hex: cal.color ?? acc.color) { hex in + try? await api.setCalendarColor(source: "caldav", calendarId: cal.id, color: hex) + } + Text(cal.name).font(.callout) + } + .padding(.leading, 8) } } } @@ -163,9 +169,9 @@ struct AccountsView: View { } else { ForEach(localCalendars) { cal in HStack { - Circle() - .fill(Color(hex: cal.color)) - .frame(width: 12, height: 12) + CalendarColorDot(hex: cal.color, editable: cal.owned) { hex in + try? await api.updateLocalCalendarColor(id: cal.id, color: hex) + } Text(cal.name) if cal.group { Image(systemName: "person.2.fill").font(.caption2).foregroundStyle(.secondary) @@ -213,9 +219,9 @@ struct AccountsView: View { } else { ForEach(icalSubs) { sub in HStack { - Circle() - .fill(Color(hex: sub.color)) - .frame(width: 12, height: 12) + CalendarColorDot(hex: sub.color) { hex in + try? await api.updateICalColor(id: sub.id, color: hex) + } VStack(alignment: .leading, spacing: 2) { Text(sub.name).font(.body) Text(String(format: L10n.t("accounts.ical.every", appLang), sub.refreshMinutes)) @@ -242,10 +248,20 @@ struct AccountsView: View { .foregroundStyle(.secondary) } else { ForEach(googleAccounts) { acc in - HStack { - Image(systemName: "g.circle.fill") - .foregroundStyle(.red) - Text(acc.email) + VStack(alignment: .leading, spacing: 8) { + HStack { + Image(systemName: "g.circle.fill").foregroundStyle(.red) + Text(acc.email) + } + ForEach(acc.calendars ?? []) { cal in + HStack { + CalendarColorDot(hex: cal.color ?? "#4285f4") { hex in + try? await api.setCalendarColor(source: "google", calendarId: cal.id, color: hex) + } + Text(cal.name).font(.callout) + } + .padding(.leading, 8) + } } } .onDelete { offsets in @@ -347,12 +363,20 @@ struct AccountsView: View { .foregroundStyle(.secondary) } else { ForEach(haAccounts) { acc in - VStack(alignment: .leading, spacing: 2) { - Text(acc.name).font(.body) - Text(acc.url) - .font(.caption) - .foregroundStyle(.secondary) - .lineLimit(1) + VStack(alignment: .leading, spacing: 8) { + VStack(alignment: .leading, spacing: 2) { + Text(acc.name).font(.body) + Text(acc.url).font(.caption).foregroundStyle(.secondary).lineLimit(1) + } + ForEach(acc.calendars ?? []) { cal in + HStack { + CalendarColorDot(hex: cal.color ?? "#03a9f4") { hex in + try? await api.setCalendarColor(source: "homeassistant", calendarId: cal.id, color: hex) + } + Text(cal.name).font(.callout) + } + .padding(.leading, 8) + } } } .onDelete { offsets in @@ -682,6 +706,31 @@ struct AddHASheet: View { struct IdentifiableInt: Identifiable { let id: Int } struct ExportedICS: Identifiable { let id = UUID(); let url: URL } +/// A tappable colour swatch (ColorPicker) for a calendar. Persists via `onPick` +/// when the chosen colour changes. Read-only fallback when `editable` is false. +struct CalendarColorDot: View { + let hex: String + var editable: Bool = true + let onPick: (String) async -> Void + @State private var color: Color + + init(hex: String, editable: Bool = true, onPick: @escaping (String) async -> Void) { + self.hex = hex; self.editable = editable; self.onPick = onPick + _color = State(initialValue: Color(hex: hex)) + } + + var body: some View { + if editable { + ColorPicker("", selection: $color, supportsOpacity: false) + .labelsHidden() + .frame(width: 26, height: 26) + .onChange(of: color) { _, c in Task { await onPick(c.toHex()) } } + } else { + Circle().fill(Color(hex: hex)).frame(width: 14, height: 14) + } + } +} + /// Wraps UIActivityViewController so an exported .ics can be shared/saved. struct ActivityView: UIViewControllerRepresentable { let items: [Any] diff --git a/Calendarr iOS/Views/Calendar/CalendarHostView.swift b/Calendarr iOS/Views/Calendar/CalendarHostView.swift index 0ac3616..709ad5b 100644 --- a/Calendarr iOS/Views/Calendar/CalendarHostView.swift +++ b/Calendarr iOS/Views/Calendar/CalendarHostView.swift @@ -129,7 +129,7 @@ struct CalendarHostView: View { } ToolbarItem(placement: .navigationBarTrailing) { HStack(spacing: 8) { - groupMenu + if !groups.isEmpty { groupMenu } viewPickerMenu Button { showFilter = true } label: { Image(systemName: "line.3.horizontal.decrease.circle") @@ -178,23 +178,25 @@ struct CalendarHostView: View { } Button(L10n.t("nav.today", appLang)) { store.moveToToday() } .font(.callout).padding(.horizontal, 6) + .lineLimit(1).fixedSize() } - .padding(.leading, 8) - Spacer(minLength: 8) + .padding(.leading, 6) + Spacer(minLength: 6) Text(titleString) .font(.headline) .lineLimit(1) .minimumScaleFactor(0.7) - Spacer(minLength: 8) - groupMenu + .layoutPriority(1) + Spacer(minLength: 6) + if !groups.isEmpty { groupMenu } viewPickerMenu filterButton Button { showMenu = true } label: { Image(systemName: "line.3.horizontal") .font(.system(size: 18, weight: .medium)) - .frame(width: 40, height: 40) + .frame(width: 36, height: 36) } - .padding(.trailing, 4) + .padding(.trailing, 2) } .frame(height: 48) .background(.bar) @@ -205,7 +207,7 @@ struct CalendarHostView: View { Image(systemName: "line.3.horizontal.decrease.circle") .font(.system(size: 17, weight: .medium)) .foregroundStyle(store.hiddenCalendarKeys.isEmpty ? .primary : Color.accentColor) - .frame(width: 40, height: 40) + .frame(width: 36, height: 36) } .accessibilityLabel(L10n.t("filter.button", appLang)) } @@ -228,7 +230,7 @@ struct CalendarHostView: View { Image(systemName: store.activeGroup == nil ? "person.2" : "person.2.fill") .font(.system(size: 17, weight: .medium)) .foregroundStyle(store.activeGroup == nil ? .primary : Color.accentColor) - .frame(width: 40, height: 40) + .frame(width: 36, height: 36) } .accessibilityLabel(L10n.t("groups.title", appLang)) } @@ -263,7 +265,7 @@ struct CalendarHostView: View { Image(systemName: store.viewType.systemImage) .font(.system(size: 17, weight: .medium)) .foregroundStyle(.primary) - .frame(width: 40, height: 40) + .frame(width: 36, height: 36) } .accessibilityLabel(L10n.t("view.change", appLang)) }