From d39c61c4751a278911abc32ff2cedcd43d669a5a Mon Sep 17 00:00:00 2001 From: Amir Zarrinkafsh <3339418+nightah@users.noreply.github.com> Date: Sun, 22 Mar 2026 13:19:20 +1100 Subject: [PATCH] refactor(calendar): group calendars by accounts in settings Calendars in the "Select Calendars" settings section are now grouped by their source account with color indicators, making it possible to distinguish calendars that share the same name across different accounts. Each account group header includes a toggle to select or deselect all calendars in that group. --- .../Providers/CalendarServiceProviding.swift | 3 +- .../components/Settings/SettingsView.swift | 48 +++++++++++++++---- DynamicIsland/managers/CalendarManager.swift | 30 ++++++++++++ .../LockScreenWidgetPreviewManager.swift | 2 + DynamicIsland/models/CalendarModel.swift | 1 + 5 files changed, 74 insertions(+), 10 deletions(-) diff --git a/DynamicIsland/Providers/CalendarServiceProviding.swift b/DynamicIsland/Providers/CalendarServiceProviding.swift index b33ae40..055de25 100644 --- a/DynamicIsland/Providers/CalendarServiceProviding.swift +++ b/DynamicIsland/Providers/CalendarServiceProviding.swift @@ -146,6 +146,7 @@ class CalendarService: CalendarServiceProviding { extension CalendarModel { init(from calendar: EKCalendar) { self.init( + accountName: calendar.accountTitle, id: calendar.calendarIdentifier, title: calendar.title, color: calendar.color, @@ -267,7 +268,7 @@ extension Priority { // MARK: - Helper Extensions -private extension EKCalendar { +extension EKCalendar { var accountTitle: String { switch source.sourceType { case .local, .subscribed, .birthdays: diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index ae7a5ec..a31e0c5 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -2905,18 +2905,48 @@ struct CalendarSettings: View { } Section(header: Text("Select Calendars")) { - ForEach(calendarManager.allCalendars, id: \.id) { calendar in - Toggle(isOn: Binding( - get: { calendarManager.getCalendarSelected(calendar) }, - set: { isSelected in - Task { - await calendarManager.setCalendarSelected(calendar, isSelected: isSelected) + let grouped = Dictionary(grouping: calendarManager.allCalendars, by: \.accountName) + let sortedAccounts = grouped.keys.sorted() + + ForEach(sortedAccounts, id: \.self) { account in + let accountCalendars = grouped[account] ?? [] + let allAccountSelected = accountCalendars.allSatisfy { calendarManager.getCalendarSelected($0) } + + Section(header: HStack { + Text(account) + Spacer() + Toggle("", isOn: Binding( + get: { allAccountSelected }, + set: { isSelected in + Task { + await calendarManager.setCalendarsSelected(accountCalendars, isSelected: isSelected) + } } + )) + .labelsHidden() + .toggleStyle(.switch) + .controlSize(.mini) + .disabled(!showCalendar) + }) { + ForEach(accountCalendars, id: \.id) { calendar in + Toggle(isOn: Binding( + get: { calendarManager.getCalendarSelected(calendar) }, + set: { isSelected in + Task { + await calendarManager.setCalendarSelected(calendar, isSelected: isSelected) + } + } + )) { + HStack(spacing: 8) { + Circle() + .fill(Color(calendar.color)) + .frame(width: 8, height: 8) + Text(calendar.title) + } + } + .disabled(!showCalendar) } - )) { - Text(calendar.title) } - .disabled(!showCalendar) } } } diff --git a/DynamicIsland/managers/CalendarManager.swift b/DynamicIsland/managers/CalendarManager.swift index 1d84c0b..42c66e3 100644 --- a/DynamicIsland/managers/CalendarManager.swift +++ b/DynamicIsland/managers/CalendarManager.swift @@ -382,6 +382,36 @@ class CalendarManager: ObservableObject { lastEventsFetchDate = Date() } + func setCalendarsSelected(_ calendars: [CalendarModel], isSelected: Bool) async { + var selectionState = Defaults[.calendarSelectionState] + let ids = Set(calendars.map { $0.id }) + + switch selectionState { + case .all: + if !isSelected { + let identifiers = Set(allCalendars.map { $0.id }).subtracting(ids) + selectionState = .selected(identifiers) + } + case .selected(var identifiers): + if isSelected { + identifiers.formUnion(ids) + } else { + identifiers.subtract(ids) + } + + if identifiers.isEmpty || identifiers.count == allCalendars.count { + selectionState = .all + } else { + selectionState = .selected(identifiers) + } + } + + Defaults[.calendarSelectionState] = selectionState + updateSelectedCalendars() + await updateEvents(force: true) + await updateLockScreenEvents(force: true) + } + func setReminderCompleted(reminderID: String, completed: Bool) async { await calendarService.setReminderCompleted(reminderID: reminderID, completed: completed) await updateEvents(force: true) diff --git a/DynamicIsland/managers/LockScreenWidgetPreviewManager.swift b/DynamicIsland/managers/LockScreenWidgetPreviewManager.swift index 9ac2c2a..7c3b5c0 100644 --- a/DynamicIsland/managers/LockScreenWidgetPreviewManager.swift +++ b/DynamicIsland/managers/LockScreenWidgetPreviewManager.swift @@ -302,6 +302,7 @@ final class LockScreenWidgetPreviewManager: ObservableObject { private func applyCalendarPreviewEvents() { let now = Date() let reminderCalendar = CalendarModel( + accountName: "Preview", id: "preview.reminders", title: "Reminders", color: .systemBlue, @@ -327,6 +328,7 @@ final class LockScreenWidgetPreviewManager: ObservableObject { ) let eventCalendar = CalendarModel( + accountName: "Preview", id: "preview.calendar", title: "Calendar", color: .systemOrange, diff --git a/DynamicIsland/models/CalendarModel.swift b/DynamicIsland/models/CalendarModel.swift index dd6e8b0..132ec3b 100644 --- a/DynamicIsland/models/CalendarModel.swift +++ b/DynamicIsland/models/CalendarModel.swift @@ -23,6 +23,7 @@ import Cocoa struct CalendarModel: Identifiable, Hashable { + let accountName: String let id: String let title: String let color: NSColor