Commit 4e399248c2
Verified · cmc
Layout: unified · split
Sources/OrgApp/AgendaModel.swift +31 −1
| @@ -2,6 +2,7 @@ import Foundation | |||
| 2 | import Observation | 2 | import Observation |
| 3 | import OrgCore | 3 | import OrgCore |
| 4 | import OrgIndex | 4 | import OrgIndex |
| 5 | import OrgPresentation | ||
| 5 | 6 | ||
| 6 | /// The day/week agenda over the workspace's agenda files. Sources are read from disk and | 7 | /// The day/week agenda over the workspace's agenda files. Sources are read from disk and |
| 7 | /// cached by modification date and size; the open file's come from its buffer, so the agenda | 8 | /// cached by modification date and size; the open file's come from its buffer, so the agenda |
| @@ -22,6 +23,14 @@ public final class AgendaModel { | |||
| 22 | public var includeSubfolders = false | 23 | public var includeSubfolders = false |
| 23 | public var options = AgendaOptions() | 24 | public var options = AgendaOptions() |
| 24 | public private(set) var isComputing = false | 25 | public private(set) var isComputing = false |
| 26 | /// Calendar events over the days shown, from `calendarEvents` (nil reads none) and the | ||
| 27 | /// calendars in `calendarNames` (all when empty). | ||
| 28 | public private(set) var events: [AgendaEvent] = [] | ||
| 29 | public var calendarEvents: CalendarEvents? | ||
| 30 | public var calendarNames: [String] = [] | ||
| 31 | /// Today and the calendar of the last refresh. | ||
| 32 | public private(set) var today = Days.absolute(Date(), calendar: .current) | ||
| 33 | @ObservationIgnored private var calendar = Calendar.current | ||
| 25 | 34 | ||
| 26 | private let workspace: WorkspaceModel | 35 | private let workspace: WorkspaceModel |
| 27 | @ObservationIgnored private var cache: [String: (mtime: Date, size: Int, source: AgendaSource)] = [:] | 36 | @ObservationIgnored private var cache: [String: (mtime: Date, size: Int, source: AgendaSource)] = [:] |
| @@ -57,23 +66,33 @@ public final class AgendaModel { | |||
| 57 | let first = start ?? today + startOffset | 66 | let first = start ?? today + startOffset |
| 58 | let options = self.options | 67 | let options = self.options |
| 59 | let mode = self.mode | 68 | let mode = self.mode |
| 69 | let provider = calendarEvents | ||
| 70 | let names = calendarNames | ||
| 60 | let all = await sources(open: open) | 71 | let all = await sources(open: open) |
| 61 | let result = await Task.detached { | 72 | let result = await Task.detached { |
| 62 | var days: [AgendaDay] = [] | 73 | var days: [AgendaDay] = [] |
| 63 | var list: [AgendaItem] = [] | 74 | var list: [AgendaItem] = [] |
| 75 | var events: [AgendaEvent] = [] | ||
| 64 | switch mode { | 76 | switch mode { |
| 65 | case .agenda: | 77 | case .agenda: |
| 66 | days = Agenda.list(all, start: first, days: span, today: today, now: (clock.hour ?? 0, clock.minute ?? 0), options: options) | 78 | days = Agenda.list(all, start: first, days: span, today: today, now: (clock.hour ?? 0, clock.minute ?? 0), options: options) |
| 79 | if let provider { | ||
| 80 | let from = Self.date(first, calendar: calendar) | ||
| 81 | events = provider.events(from: from, to: Self.date(first + span, calendar: calendar), chosen: names) | ||
| 82 | } | ||
| 67 | case .todo(let keywords): | 83 | case .todo(let keywords): |
| 68 | list = Agenda.todoList(all, keywords: keywords, options: options) | 84 | list = Agenda.todoList(all, keywords: keywords, options: options) |
| 69 | case .match(let match, let todoOnly): | 85 | case .match(let match, let todoOnly): |
| 70 | list = Agenda.tagsMatch(all, match: match, todoOnly: todoOnly, now: now, calendar: calendar, options: options) | 86 | list = Agenda.tagsMatch(all, match: match, todoOnly: todoOnly, now: now, calendar: calendar, options: options) |
| 71 | } | 87 | } |
| 72 | return (days, list) | 88 | return (days, list, events) |
| 73 | }.value | 89 | }.value |
| 74 | guard id == generation else { return } | 90 | guard id == generation else { return } |
| 75 | days = result.0 | 91 | days = result.0 |
| 76 | list = result.1 | 92 | list = result.1 |
| 93 | events = result.2 | ||
| 94 | self.today = today | ||
| 95 | self.calendar = calendar | ||
| 77 | isComputing = false | 96 | isComputing = false |
| 78 | } | 97 | } |
| 79 | 98 | ||
| @@ -126,6 +145,17 @@ public final class AgendaModel { | |||
| 126 | return loaded.map(\.source) | 145 | return loaded.map(\.source) |
| 127 | } | 146 | } |
| 128 | 147 | ||
| 148 | /// The days as rows with the events, empty days hidden unless `showAllDates`; see | ||
| 149 | /// `AgendaRows.groups`. | ||
| 150 | public func groups(showAllDates: Bool = false, keep: (AgendaItem) -> Bool = { _ in true }) -> [AgendaDayGroup] { | ||
| 151 | AgendaRows.groups(days, events: events, today: today, showAllDates: showAllDates, calendar: calendar, keep: keep) | ||
| 152 | } | ||
| 153 | |||
| 154 | nonisolated static func date(_ day: Int, calendar: Calendar) -> Date { | ||
| 155 | let d = Days.date(day) | ||
| 156 | return calendar.date(from: DateComponents(year: d.year, month: d.month, day: d.day)) ?? Date() | ||
| 157 | } | ||
| 158 | |||
| 129 | public func showToday() { start = nil } | 159 | public func showToday() { start = nil } |
| 130 | 160 | ||
| 131 | /// Moves the view by a whole span, forward or back. | 161 | /// Moves the view by a whole span, forward or back. |
Sources/OrgApp/CalendarEvents.swift added +86
| @@ -0,0 +1,86 @@ | |||
| 1 | import CoreGraphics | ||
| 2 | import EventKit | ||
| 3 | import Foundation | ||
| 4 | import OrgCore | ||
| 5 | import OrgPresentation | ||
| 6 | |||
| 7 | /// What the agenda reads from a calendar event: `EKEvent`, or a value made in tests. | ||
| 8 | public protocol CalendarEventRecord { | ||
| 9 | var recordID: String { get } | ||
| 10 | var recordTitle: String { get } | ||
| 11 | var recordStart: Date { get } | ||
| 12 | var recordEnd: Date { get } | ||
| 13 | var recordIsAllDay: Bool { get } | ||
| 14 | var recordLocation: String? { get } | ||
| 15 | var recordCalendarID: String { get } | ||
| 16 | var recordCalendarTitle: String { get } | ||
| 17 | var recordCalendarColor: CGColor? { get } | ||
| 18 | } | ||
| 19 | |||
| 20 | extension EKEvent: CalendarEventRecord { | ||
| 21 | /// A repeating event's occurrences share an identifier; the start tells them apart. | ||
| 22 | public var recordID: String { (eventIdentifier ?? calendarItemIdentifier) + "@" + String(Int(startDate.timeIntervalSince1970)) } | ||
| 23 | public var recordTitle: String { title ?? "" } | ||
| 24 | public var recordStart: Date { startDate } | ||
| 25 | public var recordEnd: Date { endDate } | ||
| 26 | public var recordIsAllDay: Bool { isAllDay } | ||
| 27 | public var recordLocation: String? { location.flatMap { $0.isEmpty ? nil : $0 } } | ||
| 28 | public var recordCalendarID: String { calendar?.calendarIdentifier ?? "" } | ||
| 29 | public var recordCalendarTitle: String { calendar?.title ?? "" } | ||
| 30 | public var recordCalendarColor: CGColor? { calendar?.cgColor } | ||
| 31 | } | ||
| 32 | |||
| 33 | /// Read-only events from the system Calendar for the agenda (`calendar-events` in | ||
| 34 | /// config.toml), from the calendars `calendar-event-calendars` names. | ||
| 35 | public final class CalendarEvents: @unchecked Sendable { | ||
| 36 | private let store = EKEventStore() | ||
| 37 | |||
| 38 | public init() {} | ||
| 39 | |||
| 40 | /// Reading events needs full access; EventKit has no read-only level. | ||
| 41 | public static var isAuthorized: Bool { EKEventStore.authorizationStatus(for: .event) == .fullAccess } | ||
| 42 | |||
| 43 | /// Asks for access once; later calls answer with what the person chose. | ||
| 44 | public func requestAccess() async -> Bool { | ||
| 45 | if Self.isAuthorized { return true } | ||
| 46 | return (try? await store.requestFullAccessToEvents()) ?? false | ||
| 47 | } | ||
| 48 | |||
| 49 | /// The event calendars, for choosing among them. | ||
| 50 | public func calendars() -> [(id: String, title: String, color: RGBA?)] { | ||
| 51 | guard Self.isAuthorized else { return [] } | ||
| 52 | return store.calendars(for: .event).map { ($0.calendarIdentifier, $0.title, $0.cgColor.flatMap(Self.rgba)) } | ||
| 53 | } | ||
| 54 | |||
| 55 | /// Events overlapping `start..<end` in the chosen calendars (all when `chosen` is empty), | ||
| 56 | /// by start. None without access. | ||
| 57 | public func events(from start: Date, to end: Date, chosen: [String]) -> [AgendaEvent] { | ||
| 58 | guard Self.isAuthorized else { return [] } | ||
| 59 | let calendars = store.calendars(for: .event).filter { Self.chooses(id: $0.calendarIdentifier, title: $0.title, chosen: chosen) } | ||
| 60 | guard !calendars.isEmpty else { return [] } | ||
| 61 | let predicate = store.predicateForEvents(withStart: start, end: end, calendars: calendars) | ||
| 62 | return store.events(matching: predicate).map(Self.event).sorted { $0.start < $1.start } | ||
| 63 | } | ||
| 64 | |||
| 65 | /// Whether a calendar is among those chosen by identifier or title; every calendar is when | ||
| 66 | /// none are. | ||
| 67 | public static func chooses(id: String, title: String, chosen: [String]) -> Bool { | ||
| 68 | chosen.isEmpty || chosen.contains(id) || chosen.contains(title) | ||
| 69 | } | ||
| 70 | |||
| 71 | public static func event(_ record: some CalendarEventRecord) -> AgendaEvent { | ||
| 72 | AgendaEvent( | ||
| 73 | id: record.recordID, title: record.recordTitle, calendarID: record.recordCalendarID, calendarTitle: record.recordCalendarTitle, | ||
| 74 | color: record.recordCalendarColor.flatMap(rgba), start: record.recordStart, end: record.recordEnd, | ||
| 75 | isAllDay: record.recordIsAllDay, location: record.recordLocation | ||
| 76 | ) | ||
| 77 | } | ||
| 78 | |||
| 79 | /// A color in sRGB. | ||
| 80 | static func rgba(_ color: CGColor) -> RGBA? { | ||
| 81 | guard let space = CGColorSpace(name: CGColorSpace.sRGB), | ||
| 82 | let converted = color.converted(to: space, intent: .defaultIntent, options: nil), | ||
| 83 | let c = converted.components, c.count >= 3 else { return nil } | ||
| 84 | return RGBA(red: Double(c[0]), green: Double(c[1]), blue: Double(c[2]), alpha: Double(converted.alpha)) | ||
| 85 | } | ||
| 86 | } | ||
Sources/OrgApp/OrgPreferences.swift +17
| @@ -50,6 +50,23 @@ public enum OrgPreferences { | |||
| 50 | public static func agendaSpan(_ defaults: UserDefaults = .standard) -> Int { integer("agendaSpan", defaults) } | 50 | public static func agendaSpan(_ defaults: UserDefaults = .standard) -> Int { integer("agendaSpan", defaults) } |
| 51 | public static func agendaStartOffset(_ defaults: UserDefaults = .standard) -> Int { integer("agendaStartOffset", defaults) } | 51 | public static func agendaStartOffset(_ defaults: UserDefaults = .standard) -> Int { integer("agendaStartOffset", defaults) } |
| 52 | public static func agendaSubfolders(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaSubfolders", defaults) } | 52 | public static func agendaSubfolders(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaSubfolders", defaults) } |
| 53 | /// `org-agenda-show-all-dates`: days without entries show. | ||
| 54 | public static func agendaShowAllDates(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaShowAllDates", defaults) } | ||
| 55 | public static func agendaSkipScheduledIfDone(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaSkipScheduledIfDone", defaults) } | ||
| 56 | public static func agendaSkipDeadlineIfDone(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaSkipDeadlineIfDone", defaults) } | ||
| 57 | /// `options` with the skip-if-done settings. | ||
| 58 | public static func agendaOptions(_ options: AgendaOptions, _ defaults: UserDefaults = .standard) -> AgendaOptions { | ||
| 59 | var options = options | ||
| 60 | options.skipScheduledIfDone = agendaSkipScheduledIfDone(defaults) | ||
| 61 | options.skipDeadlineIfDone = agendaSkipDeadlineIfDone(defaults) | ||
| 62 | return options | ||
| 63 | } | ||
| 64 | /// `calendar-events`: Calendar's events show in the agenda. | ||
| 65 | public static func calendarEvents(_ defaults: UserDefaults = .standard) -> Bool { bool("calendarEvents", defaults) } | ||
| 66 | /// `calendar-event-calendars`: the calendars chosen, by title or identifier; empty for all. | ||
| 67 | public static func calendarEventCalendars(_ defaults: UserDefaults = .standard) -> [String] { | ||
| 68 | string("calendarEventCalendars", defaults).split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty } | ||
| 69 | } | ||
| 53 | public static func reminders(_ defaults: UserDefaults = .standard) -> Bool { bool("reminders", defaults) } | 70 | public static func reminders(_ defaults: UserDefaults = .standard) -> Bool { bool("reminders", defaults) } |
| 54 | /// `appt-message-warning-time`, in minutes. | 71 | /// `appt-message-warning-time`, in minutes. |
| 55 | public static func reminderLead(_ defaults: UserDefaults = .standard) -> Int { integer("reminderLead", defaults) } | 72 | public static func reminderLead(_ defaults: UserDefaults = .standard) -> Int { integer("reminderLead", defaults) } |
Sources/OrgApp/UserConfig.swift +10
| @@ -141,6 +141,12 @@ public enum UserConfig { | |||
| 141 | comment: "days the agenda shows", legacy: "agenda.span"), | 141 | comment: "days the agenda shows", legacy: "agenda.span"), |
| 142 | Setting(section: "", key: "org-agenda-start-day", defaultsKey: "agendaStartOffset", kind: .dayOffset, fallback: .string("-3d"), | 142 | Setting(section: "", key: "org-agenda-start-day", defaultsKey: "agendaStartOffset", kind: .dayOffset, fallback: .string("-3d"), |
| 143 | comment: "the agenda's first day: \"-3d\", \"+0d\"", legacy: "agenda.start-offset"), | 143 | comment: "the agenda's first day: \"-3d\", \"+0d\"", legacy: "agenda.start-offset"), |
| 144 | Setting(section: "", key: "org-agenda-show-all-dates", defaultsKey: "agendaShowAllDates", kind: .bool, fallback: .bool(false), | ||
| 145 | comment: "show days with no entries (Emacs's default is t; Orgstar hides them); today always shows", legacy: ""), | ||
| 146 | Setting(section: "", key: "org-agenda-skip-scheduled-if-done", defaultsKey: "agendaSkipScheduledIfDone", kind: .bool, fallback: .bool(false), | ||
| 147 | comment: "leave done entries out of the agenda even on the day they are scheduled", legacy: ""), | ||
| 148 | Setting(section: "", key: "org-agenda-skip-deadline-if-done", defaultsKey: "agendaSkipDeadlineIfDone", kind: .bool, fallback: .bool(false), | ||
| 149 | comment: "leave done entries out of the agenda even on the day they are due", legacy: ""), | ||
| 144 | Setting(section: "", key: "appt-message-warning-time", defaultsKey: "reminderLead", kind: .integer, fallback: .integer(12), | 150 | Setting(section: "", key: "appt-message-warning-time", defaultsKey: "reminderLead", kind: .integer, fallback: .integer(12), |
| 145 | comment: "minutes of warning before timed entries; APPT_WARNTIME overrides", legacy: "agenda.reminder-lead"), | 151 | comment: "minutes of warning before timed entries; APPT_WARNTIME overrides", legacy: "agenda.reminder-lead"), |
| 146 | Setting(section: "", key: "org-clock-idle-time", defaultsKey: "clockIdleTime", kind: .integer, fallback: .integer(0), | 152 | Setting(section: "", key: "org-clock-idle-time", defaultsKey: "clockIdleTime", kind: .integer, fallback: .integer(0), |
| @@ -183,6 +189,10 @@ public enum UserConfig { | |||
| 183 | comment: "the agenda reads org files in subfolders too", legacy: "agenda.include-subfolders"), | 189 | comment: "the agenda reads org files in subfolders too", legacy: "agenda.include-subfolders"), |
| 184 | Setting(section: "orgstar", key: "reminders", defaultsKey: "reminders", kind: .bool, fallback: .bool(true), | 190 | Setting(section: "orgstar", key: "reminders", defaultsKey: "reminders", kind: .bool, fallback: .bool(true), |
| 185 | comment: "notify before timed entries", legacy: "agenda.reminders"), | 191 | comment: "notify before timed entries", legacy: "agenda.reminders"), |
| 192 | Setting(section: "orgstar", key: "calendar-events", defaultsKey: "calendarEvents", kind: .bool, fallback: .bool(false), | ||
| 193 | comment: "show events from Calendar in the agenda, read-only", legacy: ""), | ||
| 194 | Setting(section: "orgstar", key: "calendar-event-calendars", defaultsKey: "calendarEventCalendars", kind: .string, fallback: .string(""), | ||
| 195 | comment: "the calendars whose events show, by title or identifier, separated by commas; \"\" for all", legacy: ""), | ||
| 186 | Setting(section: "orgstar", key: "global-capture-hotkey", defaultsKey: "globalCapture", kind: .bool, fallback: .bool(true), | 196 | Setting(section: "orgstar", key: "global-capture-hotkey", defaultsKey: "globalCapture", kind: .bool, fallback: .bool(true), |
| 187 | comment: "⌃⌥Space opens Capture from any app; templates go in capture.toml", legacy: "capture.global-hotkey"), | 197 | comment: "⌃⌥Space opens Capture from any app; templates go in capture.toml", legacy: "capture.global-hotkey"), |
| 188 | ] | 198 | ] |
Sources/Orgstar/AgendaView.swift +1
| @@ -251,6 +251,7 @@ struct AgendaView: View { | |||
| 251 | agenda.includeSubfolders = includeSubfolders | 251 | agenda.includeSubfolders = includeSubfolders |
| 252 | agenda.options.prefixFormat = ["agenda": prefixAgenda, "todo": prefixTodo, "tags": prefixTags] | 252 | agenda.options.prefixFormat = ["agenda": prefixAgenda, "todo": prefixTodo, "tags": prefixTags] |
| 253 | agenda.options.logMode = logMode | 253 | agenda.options.logMode = logMode |
| 254 | agenda.options = OrgPreferences.agendaOptions(agenda.options) | ||
| 254 | let open = session.openTexts | 255 | let open = session.openTexts |
| 255 | await agenda.refresh(open: open) | 256 | await agenda.refresh(open: open) |
| 256 | } | 257 | } |
Sources/OrgstarMobile/MobileModel.swift +1
| @@ -114,6 +114,7 @@ final class MobileModel { | |||
| 114 | agenda.span = OrgPreferences.agendaSpan() | 114 | agenda.span = OrgPreferences.agendaSpan() |
| 115 | agenda.startOffset = OrgPreferences.agendaStartOffset() | 115 | agenda.startOffset = OrgPreferences.agendaStartOffset() |
| 116 | agenda.includeSubfolders = OrgPreferences.agendaSubfolders() | 116 | agenda.includeSubfolders = OrgPreferences.agendaSubfolders() |
| 117 | agenda.options = OrgPreferences.agendaOptions(agenda.options) | ||
| 117 | } | 118 | } |
| 118 | settingsVersion += 1 | 119 | settingsVersion += 1 |
| 119 | } | 120 | } |
Tests/OrgAppTests/CalendarEventsTests.swift added +72
| @@ -0,0 +1,72 @@ | |||
| 1 | import CoreGraphics | ||
| 2 | import Foundation | ||
| 3 | import OrgCore | ||
| 4 | import OrgPresentation | ||
| 5 | import Testing | ||
| 6 | @testable import OrgApp | ||
| 7 | |||
| 8 | struct AgendaSettingsTests { | ||
| 9 | @Test func agendaSettingsReadFromConfigToml() throws { | ||
| 10 | let defaults = try #require(UserDefaults(suiteName: "orgstar-agenda-settings-\(UUID().uuidString)")) | ||
| 11 | #expect(!OrgPreferences.agendaShowAllDates(defaults)) | ||
| 12 | #expect(!OrgPreferences.agendaSkipScheduledIfDone(defaults) && !OrgPreferences.agendaSkipDeadlineIfDone(defaults)) | ||
| 13 | #expect(!OrgPreferences.calendarEvents(defaults) && OrgPreferences.calendarEventCalendars(defaults).isEmpty) | ||
| 14 | let problems = UserConfig.apply(""" | ||
| 15 | org-agenda-show-all-dates = true | ||
| 16 | org-agenda-skip-scheduled-if-done = true | ||
| 17 | org-agenda-skip-deadline-if-done = true | ||
| 18 | [orgstar] | ||
| 19 | calendar-events = true | ||
| 20 | calendar-event-calendars = "Work, Family , ,ABC-123" | ||
| 21 | """, to: defaults) | ||
| 22 | #expect(problems.isEmpty, "\(problems)") | ||
| 23 | #expect(OrgPreferences.agendaShowAllDates(defaults)) | ||
| 24 | #expect(OrgPreferences.calendarEvents(defaults)) | ||
| 25 | #expect(OrgPreferences.calendarEventCalendars(defaults) == ["Work", "Family", "ABC-123"]) | ||
| 26 | let options = OrgPreferences.agendaOptions(AgendaOptions(), defaults) | ||
| 27 | #expect(options.skipScheduledIfDone && options.skipDeadlineIfDone) | ||
| 28 | #expect(UserConfig.template(from: defaults).contains("org-agenda-show-all-dates = true")) | ||
| 29 | } | ||
| 30 | } | ||
| 31 | |||
| 32 | struct CalendarEventsTests { | ||
| 33 | struct Record: CalendarEventRecord { | ||
| 34 | var recordID = "e1@0" | ||
| 35 | var recordTitle = "Standup" | ||
| 36 | var recordStart = Date(timeIntervalSince1970: 1_791_190_800) | ||
| 37 | var recordEnd = Date(timeIntervalSince1970: 1_791_192_600) | ||
| 38 | var recordIsAllDay = false | ||
| 39 | var recordLocation: String? = "Room 4" | ||
| 40 | var recordCalendarID = "cal-1" | ||
| 41 | var recordCalendarTitle = "Work" | ||
| 42 | var recordCalendarColor: CGColor? = CGColor(srgbRed: 1, green: 0.5, blue: 0, alpha: 1) | ||
| 43 | } | ||
| 44 | |||
| 45 | @Test func recordsBecomeAgendaEvents() { | ||
| 46 | let event = CalendarEvents.event(Record()) | ||
| 47 | #expect(event.id == "e1@0" && event.title == "Standup" && event.calendarTitle == "Work" && event.calendarID == "cal-1") | ||
| 48 | #expect(event.color?.hex == "#ff8000" && event.location == "Room 4" && !event.isAllDay) | ||
| 49 | var noColor = Record() | ||
| 50 | noColor.recordCalendarColor = nil | ||
| 51 | #expect(CalendarEvents.event(noColor).color == nil) | ||
| 52 | } | ||
| 53 | |||
| 54 | @Test func eventsBecomeRows() { | ||
| 55 | var utc = Calendar(identifier: .gregorian) | ||
| 56 | utc.timeZone = TimeZone(identifier: "UTC")! | ||
| 57 | let event = CalendarEvents.event(Record()) | ||
| 58 | let day = Days.absolute(event.start, calendar: utc) | ||
| 59 | let row = AgendaRow(event: event, day: day, calendar: utc) | ||
| 60 | #expect(row.gutter == "09:00–09:30" && row.category == "Work" && row.title == "Standup" && row.isEvent && row.icon == .event) | ||
| 61 | var allDay = Record() | ||
| 62 | allDay.recordIsAllDay = true | ||
| 63 | #expect(AgendaRow(event: CalendarEvents.event(allDay), day: day, calendar: utc).gutter == "All day") | ||
| 64 | } | ||
| 65 | |||
| 66 | @Test func calendarsChosenByTitleOrIdentifier() { | ||
| 67 | #expect(CalendarEvents.chooses(id: "x", title: "Work", chosen: [])) | ||
| 68 | #expect(CalendarEvents.chooses(id: "x", title: "Work", chosen: ["Work"])) | ||
| 69 | #expect(CalendarEvents.chooses(id: "x", title: "Work", chosen: ["x"])) | ||
| 70 | #expect(!CalendarEvents.chooses(id: "x", title: "Work", chosen: ["Home"])) | ||
| 71 | } | ||
| 72 | } | ||
ios/Orgstar/Info.plist +2
| @@ -27,6 +27,8 @@ | |||
| 27 | </array> | 27 | </array> |
| 28 | <key>CFBundleVersion</key> | 28 | <key>CFBundleVersion</key> |
| 29 | <string>$(CURRENT_PROJECT_VERSION)</string> | 29 | <string>$(CURRENT_PROJECT_VERSION)</string> |
| 30 | <key>NSCalendarsFullAccessUsageDescription</key> | ||
| 31 | <string>The agenda shows your calendar events alongside your Org entries. Orgstar only reads them.</string> | ||
| 30 | <key>UIApplicationSceneManifest</key> | 32 | <key>UIApplicationSceneManifest</key> |
| 31 | <dict> | 33 | <dict> |
| 32 | <key>UIApplicationSupportsMultipleScenes</key> | 34 | <key>UIApplicationSupportsMultipleScenes</key> |
scripts/build-app.sh +2
| @@ -94,6 +94,8 @@ cat > "$app/Contents/Info.plist" <<EOF | |||
| 94 | <string>26.0</string> | 94 | <string>26.0</string> |
| 95 | <key>LSApplicationCategoryType</key> | 95 | <key>LSApplicationCategoryType</key> |
| 96 | <string>public.app-category.productivity</string> | 96 | <string>public.app-category.productivity</string> |
| 97 | <key>NSCalendarsFullAccessUsageDescription</key> | ||
| 98 | <string>The agenda shows your calendar events alongside your Org entries. Orgstar only reads them.</string> | ||
| 97 | <key>NSHighResolutionCapable</key> | 99 | <key>NSHighResolutionCapable</key> |
| 98 | <true/> | 100 | <true/> |
| 99 | <key>NSPrincipalClass</key> | 101 | <key>NSPrincipalClass</key> |
scripts/build-ios-app.sh +2
| @@ -47,6 +47,8 @@ cat > "$app/Info.plist" <<PLIST | |||
| 47 | <array><string>iPhoneSimulator</string></array> | 47 | <array><string>iPhoneSimulator</string></array> |
| 48 | <key>MinimumOSVersion</key> | 48 | <key>MinimumOSVersion</key> |
| 49 | <string>26.0</string> | 49 | <string>26.0</string> |
| 50 | <key>NSCalendarsFullAccessUsageDescription</key> | ||
| 51 | <string>The agenda shows your calendar events alongside your Org entries. Orgstar only reads them.</string> | ||
| 50 | <key>UIDeviceFamily</key> | 52 | <key>UIDeviceFamily</key> |
| 51 | <array><integer>1</integer><integer>2</integer></array> | 53 | <array><integer>1</integer><integer>2</integer></array> |
| 52 | <key>UILaunchScreen</key> | 54 | <key>UILaunchScreen</key> |