krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Sources/OrgstarWidgets/AgendaTimeline.swift

322a87d47526d505d903e05d14e65a5464e1468f
orgstar/Sources/OrgstarWidgets/AgendaTimeline.swift history · blame · raw

96 lines · 4573 bytes

 1import OrgApp
 2import SwiftUI
 3import WidgetKit
 4
 5struct AgendaEntry: TimelineEntry {
 6    let date: Date
 7    /// Nil until the app has written one.
 8    let snapshot: AgendaSnapshot?
 9}
10
11/// The app's agenda snapshot, with an entry for each time what's left of the day changes.
12struct AgendaProvider: TimelineProvider {
13    func placeholder(in context: Context) -> AgendaEntry {
14        AgendaEntry(date: .now, snapshot: .sample(.now))
15    }
16
17    func getSnapshot(in context: Context, completion: @escaping (AgendaEntry) -> Void) {
18        let snapshot = Self.load()
19        completion(AgendaEntry(date: .now, snapshot: snapshot ?? (context.isPreview ? .sample(.now) : nil)))
20    }
21
22    func getTimeline(in context: Context, completion: @escaping (Timeline<AgendaEntry>) -> Void) {
23        completion(Self.timeline(Self.load(), now: .now))
24    }
25
26    static func load() -> AgendaSnapshot? {
27        AgendaSnapshot.file().flatMap(AgendaSnapshot.read)
28    }
29
30    static func timeline(_ snapshot: AgendaSnapshot?, now: Date, calendar: Calendar = .current) -> Timeline<AgendaEntry> {
31        let changes = snapshot?.changes(after: now, calendar: calendar)
32            ?? [calendar.date(byAdding: .day, value: 1, to: calendar.startOfDay(for: now))!]
33        let entries = ([now] + changes).map { AgendaEntry(date: $0, snapshot: snapshot) }
34        return Timeline(entries: entries, policy: .atEnd)
35    }
36}
37
38extension AgendaSnapshot.Hex {
39    func color(_ scheme: ColorScheme) -> Color {
40        Self.parse(scheme == .dark ? dark : light) ?? .primary
41    }
42
43    /// `#rrggbb` or `#rrggbbaa`.
44    static func parse(_ hex: String) -> Color? {
45        let digits = hex.dropFirst()
46        guard hex.first == "#", digits.count == 6 || digits.count == 8, let value = UInt64(digits, radix: 16) else { return nil }
47        let full = digits.count == 6 ? value << 8 | 0xFF : value
48        func part(_ shift: UInt64) -> Double { Double(full >> shift & 0xFF) / 255 }
49        return Color(.sRGB, red: part(24), green: part(16), blue: part(8), opacity: part(0))
50    }
51}
52
53extension AgendaSnapshot {
54    func color(_ key: String, _ scheme: ColorScheme) -> Color? {
55        colors[key]?.color(scheme)
56    }
57
58    /// A day for the gallery and previews.
59    static func sample(_ now: Date, calendar: Calendar = .current) -> AgendaSnapshot {
60        let midnight = calendar.startOfDay(for: now)
61        func at(_ hour: Int, _ minute: Int = 0) -> Date { midnight.addingTimeInterval(Double(hour * 3600 + minute * 60)) }
62        let red = Hex(light: "#d70015", dark: "#ff6961")
63        let orange = Hex(light: "#c93400", dark: "#ff9f0a")
64        let gray = Hex(light: "#6c6c70", dark: "#98989d")
65        let blue = Hex(light: "#0a66d8", dark: "#409cff")
66        func row(
67            _ title: String, _ gutter: String, status: String = "normal", todo: String? = nil, todoColor: Hex? = nil,
68            priority: String? = nil, category: String = "work", start: Date? = nil, end: Date? = nil
69        ) -> Row {
70            Row(
71                id: title, gutter: gutter, status: status, title: title, category: category, todo: todo, priority: priority, isDone: false,
72                isEvent: false, start: start, end: end, path: nil, offset: nil,
73                statusColor: status == "overdue" ? red : status == "due-soon" ? orange : gray, todoColor: todoColor,
74                priorityColor: priority == nil ? nil : red, categoryColor: Hex(light: "#2e6b8f", dark: "#7fb8d9")
75            )
76        }
77        let today = Day(
78            day: 0, date: midnight, header: "Today",
79            overdue: [row("Send the forms", "3d late", status: "overdue", todo: "TODO", todoColor: red, category: "home")],
80            rows: [
81                row("Standup", "09:30", start: at(9, 30)),
82                row("Review the draft", "11:00–12:00", todo: "NEXT", todoColor: blue, priority: "A", start: at(11), end: at(12)),
83                row("Dentist", "14:00", category: "home", start: at(14)),
84                row("File expenses", "Due today", status: "due-soon", todo: "TODO", todoColor: red),
85            ]
86        )
87        let tomorrow = Day(
88            day: 1, date: calendar.date(byAdding: .day, value: 1, to: midnight)!, header: "Tomorrow", overdue: [],
89            rows: [row("Plan the week", "Scheduled", todo: "TODO", todoColor: red)]
90        )
91        return AgendaSnapshot(
92            generated: now, days: [today, tomorrow],
93            colors: ["agenda-today": Hex(light: "#007aff", dark: "#0a84ff"), "agenda-time": gray, "foreground": Hex(light: "#1f2328", dark: "#e6edf3")]
94        )
95    }
96}