Sources/OrgstarWidgets/AgendaTimeline.swift
96 lines · 4573 bytes
14 symbols in this file
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}