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.
This commit is contained in:
Amir Zarrinkafsh 2026-03-22 13:19:20 +11:00
commit d39c61c475
No known key found for this signature in database
GPG key ID: ECDB8EF9E77E4EBF
5 changed files with 74 additions and 10 deletions

View file

@ -146,6 +146,7 @@ class CalendarService: CalendarServiceProviding {
extension CalendarModel { extension CalendarModel {
init(from calendar: EKCalendar) { init(from calendar: EKCalendar) {
self.init( self.init(
accountName: calendar.accountTitle,
id: calendar.calendarIdentifier, id: calendar.calendarIdentifier,
title: calendar.title, title: calendar.title,
color: calendar.color, color: calendar.color,
@ -267,7 +268,7 @@ extension Priority {
// MARK: - Helper Extensions // MARK: - Helper Extensions
private extension EKCalendar { extension EKCalendar {
var accountTitle: String { var accountTitle: String {
switch source.sourceType { switch source.sourceType {
case .local, .subscribed, .birthdays: case .local, .subscribed, .birthdays:

View file

@ -2905,18 +2905,48 @@ struct CalendarSettings: View {
} }
Section(header: Text("Select Calendars")) { Section(header: Text("Select Calendars")) {
ForEach(calendarManager.allCalendars, id: \.id) { calendar in let grouped = Dictionary(grouping: calendarManager.allCalendars, by: \.accountName)
Toggle(isOn: Binding( let sortedAccounts = grouped.keys.sorted()
get: { calendarManager.getCalendarSelected(calendar) },
set: { isSelected in ForEach(sortedAccounts, id: \.self) { account in
Task { let accountCalendars = grouped[account] ?? []
await calendarManager.setCalendarSelected(calendar, isSelected: isSelected) 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)
} }
} }
} }

View file

@ -382,6 +382,36 @@ class CalendarManager: ObservableObject {
lastEventsFetchDate = Date() 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 { func setReminderCompleted(reminderID: String, completed: Bool) async {
await calendarService.setReminderCompleted(reminderID: reminderID, completed: completed) await calendarService.setReminderCompleted(reminderID: reminderID, completed: completed)
await updateEvents(force: true) await updateEvents(force: true)

View file

@ -302,6 +302,7 @@ final class LockScreenWidgetPreviewManager: ObservableObject {
private func applyCalendarPreviewEvents() { private func applyCalendarPreviewEvents() {
let now = Date() let now = Date()
let reminderCalendar = CalendarModel( let reminderCalendar = CalendarModel(
accountName: "Preview",
id: "preview.reminders", id: "preview.reminders",
title: "Reminders", title: "Reminders",
color: .systemBlue, color: .systemBlue,
@ -327,6 +328,7 @@ final class LockScreenWidgetPreviewManager: ObservableObject {
) )
let eventCalendar = CalendarModel( let eventCalendar = CalendarModel(
accountName: "Preview",
id: "preview.calendar", id: "preview.calendar",
title: "Calendar", title: "Calendar",
color: .systemOrange, color: .systemOrange,

View file

@ -23,6 +23,7 @@
import Cocoa import Cocoa
struct CalendarModel: Identifiable, Hashable { struct CalendarModel: Identifiable, Hashable {
let accountName: String
let id: String let id: String
let title: String let title: String
let color: NSColor let color: NSColor