Commit 640a8729ac
Verified · cmc
Layout: unified · split
Sources/Orgstar/OrgstarApp.swift +50 −1
| @@ -2,6 +2,7 @@ import AppKit | |||
| 2 | import OrgApp | 2 | import OrgApp |
| 3 | import OrgCore | 3 | import OrgCore |
| 4 | import OrgEditorAppKit | 4 | import OrgEditorAppKit |
| 5 | import OrgPresentation | ||
| 5 | import SwiftUI | 6 | import SwiftUI |
| 6 | import UserNotifications | 7 | import UserNotifications |
| 7 | 8 | ||
| @@ -519,6 +520,10 @@ struct SettingsView: View { | |||
| 519 | @AppStorage("agendaSpan") private var agendaSpan = 10 | 520 | @AppStorage("agendaSpan") private var agendaSpan = 10 |
| 520 | @AppStorage("agendaStartOffset") private var agendaStartOffset = -3 | 521 | @AppStorage("agendaStartOffset") private var agendaStartOffset = -3 |
| 521 | @AppStorage("agendaSubfolders") private var agendaSubfolders = false | 522 | @AppStorage("agendaSubfolders") private var agendaSubfolders = false |
| 523 | @AppStorage("agendaShowAllDates") private var agendaShowAllDates = false | ||
| 524 | @AppStorage("calendarEvents") private var calendarEvents = false | ||
| 525 | @AppStorage("calendarEventCalendars") private var calendarEventCalendars = "" | ||
| 526 | @State private var calendars: [(id: String, title: String, color: RGBA?)] = [] | ||
| 522 | @AppStorage("reminders") private var reminders = true | 527 | @AppStorage("reminders") private var reminders = true |
| 523 | @AppStorage("reminderLead") private var reminderLead = 12 | 528 | @AppStorage("reminderLead") private var reminderLead = 12 |
| 524 | @AppStorage("clockIdleTime") private var clockIdleTime = 0 | 529 | @AppStorage("clockIdleTime") private var clockIdleTime = 0 |
| @@ -675,6 +680,33 @@ struct SettingsView: View { | |||
| 675 | Stepper("Agenda shows \(agendaSpan) days", value: $agendaSpan, in: 1...366) | 680 | Stepper("Agenda shows \(agendaSpan) days", value: $agendaSpan, in: 1...366) |
| 676 | Stepper(agendaStartOffset == 0 ? "Agenda starts today" : "Agenda starts \(-agendaStartOffset) days before today", value: $agendaStartOffset, in: -14...0) | 681 | Stepper(agendaStartOffset == 0 ? "Agenda starts today" : "Agenda starts \(-agendaStartOffset) days before today", value: $agendaStartOffset, in: -14...0) |
| 677 | Toggle("Include files in subfolders", isOn: $agendaSubfolders) | 682 | Toggle("Include files in subfolders", isOn: $agendaSubfolders) |
| 683 | Toggle("Show days without entries", isOn: $agendaShowAllDates) | ||
| 684 | } | ||
| 685 | Section { | ||
| 686 | Toggle("Show events from Calendar", isOn: $calendarEvents) | ||
| 687 | if calendarEvents { | ||
| 688 | if calendars.isEmpty { | ||
| 689 | Text(CalendarEvents.isAuthorized ? "No calendars." : "Allow Orgstar to read your calendars in System Settings ▸ Privacy & Security ▸ Calendars.") | ||
| 690 | .foregroundStyle(.secondary) | ||
| 691 | } | ||
| 692 | ForEach(calendars, id: \.id) { calendar in | ||
| 693 | Toggle(isOn: calendarChosen(calendar)) { | ||
| 694 | HStack(spacing: 6) { | ||
| 695 | Circle() | ||
| 696 | .fill(calendar.color.map { Color(.sRGB, red: $0.red, green: $0.green, blue: $0.blue, opacity: $0.alpha) } ?? .secondary) | ||
| 697 | .frame(width: 8, height: 8) | ||
| 698 | Text(calendar.title) | ||
| 699 | } | ||
| 700 | } | ||
| 701 | } | ||
| 702 | } | ||
| 703 | } footer: { | ||
| 704 | Text("Events show in the agenda read-only; double-click one to open it in Calendar. New calendars show while every calendar is checked.") | ||
| 705 | .settingsFooter() | ||
| 706 | } | ||
| 707 | .task(id: calendarEvents) { | ||
| 708 | let allowed = calendarEvents ? await CalendarAccess.events.requestAccess() : false | ||
| 709 | calendars = allowed ? CalendarAccess.events.calendars() : [] | ||
| 678 | } | 710 | } |
| 679 | Section { | 711 | Section { |
| 680 | Toggle("Notify before timed agenda entries", isOn: $reminders) | 712 | Toggle("Notify before timed agenda entries", isOn: $reminders) |
| @@ -699,7 +731,24 @@ struct SettingsView: View { | |||
| 699 | } | 731 | } |
| 700 | } | 732 | } |
| 701 | .formStyle(.grouped) | 733 | .formStyle(.grouped) |
| 702 | .frame(height: 430) | 734 | .frame(height: 560) |
| 735 | } | ||
| 736 | |||
| 737 | /// `calendar-event-calendars`: empty for every calendar, else the titles checked. | ||
| 738 | private func calendarChosen(_ calendar: (id: String, title: String, color: RGBA?)) -> Binding<Bool> { | ||
| 739 | let chosen = calendarEventCalendars.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty } | ||
| 740 | return Binding { | ||
| 741 | CalendarEvents.chooses(id: calendar.id, title: calendar.title, chosen: chosen) | ||
| 742 | } set: { on in | ||
| 743 | var titles = calendars.map(\.title).filter { title in | ||
| 744 | chosen.isEmpty || chosen.contains(title) || chosen.contains(calendars.first { $0.title == title }?.id ?? "") | ||
| 745 | } | ||
| 746 | titles.removeAll { $0 == calendar.title } | ||
| 747 | if on { titles.append(calendar.title) } | ||
| 748 | // An empty list means every calendar; turning events off hides them all. | ||
| 749 | guard !titles.isEmpty else { return } | ||
| 750 | calendarEventCalendars = Set(titles) == Set(calendars.map(\.title)) ? "" : titles.joined(separator: ", ") | ||
| 751 | } | ||
| 703 | } | 752 | } |
| 704 | 753 | ||
| 705 | private var capture: some View { | 754 | private var capture: some View { |