feat: iOS Kalenderfarben änderbar + Top-Bar entzerrt
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user