oficina / calendar Public

Clone
git clone https://amadan.net/oficina/calendar

Plain git — no account needed to clone.

Download

Download this file

1import RastrilloApple
2import SwiftUI
3
4struct QuickDraft: Codable {
5 let calendar_id: Int64
6 let title, location, start_local, end_local, tz: String
7 let all_day: Bool
8 let `repeat`: String
9 let attendees: [String]
10}
11struct QuickPreview: Decodable {
12 let draft: QuickDraft
13 let when, repeat_label, calendar_name: String
14 let assumptions: [String]
15}
16struct QuickAddView: View {
17 let client: NativeHTTP
18 @ObservedObject var model: CalendarModel
19 @State private var preview: QuickPreview?
20 @State private var error = ""
21 @State private var parsing = false
22 @FocusState private var focused: Bool
23 private var requestKey: String { model.zone + "\n" + model.quickText }
24 var body: some View {
25 ScrollView {
26 VStack(alignment: .leading, spacing: 12) {
27 TextField("Lunch tomorrow at 1pm for an hour", text: $model.quickText, axis: .vertical)
28 .lineLimit(2...4)
29 #if os(macOS)
30 .textFieldStyle(.plain)
31 .padding(10)
32 .background(.background, in: RoundedRectangle(cornerRadius: 8))
33 .overlay {
34 RoundedRectangle(cornerRadius: 8)
35 .strokeBorder(focused ? Color.accentColor : Color.secondary.opacity(0.3), lineWidth: 1)
36 }
37 #else
38 .textFieldStyle(.roundedBorder)
39 #endif
40 .accessibilityLabel("Describe an event").accessibilityIdentifier("quickAddText")
41 .focused($focused).disabled(model.quickSaving)
42 Text(model.zone.replacingOccurrences(of: "_", with: " ")).font(.footnote).foregroundStyle(.secondary)
43 if parsing { ProgressView("Understanding your event…") }
44 if let preview {
45 VStack(alignment: .leading, spacing: 8) {
46 Text(preview.draft.title).font(.headline)
47 Label(preview.when, systemImage: "clock")
48 Label(preview.calendar_name, systemImage: "calendar")
49 if !preview.draft.location.isEmpty { Label(preview.draft.location, systemImage: "mappin") }
50 if !preview.repeat_label.isEmpty { Label(preview.repeat_label, systemImage: "repeat") }
51 if !preview.draft.attendees.isEmpty {
52 Label(preview.draft.attendees.joined(separator: ", "), systemImage: "person.2")
53 Text("Attendees are recorded, not invited. No invitations are sent.").font(.footnote)
54 }
55 ForEach(preview.assumptions, id: \.self) { Text($0).font(.footnote).foregroundStyle(.secondary) }
56 }.accessibilityIdentifier("quickAddPreview")
57 Button(model.quickSaving ? "Adding…" : "Add event", systemImage: "plus") { save(preview) }
58 .buttonStyle(.borderedProminent).disabled(model.quickSaving || parsing)
59 .accessibilityIdentifier("quickAddSave").keyboardShortcut(.return, modifiers: .command)
60 }
61 if !error.isEmpty { Label(error, systemImage: "exclamationmark.circle").foregroundStyle(.red) }
62 if !model.quickSavedNotice.isEmpty { Label(model.quickSavedNotice, systemImage: "checkmark.circle").accessibilityIdentifier("quickAddSaved") }
63 }.frame(maxWidth: .infinity, alignment: .leading)
64 }
65 .task(id: requestKey) {
66 preview = nil; error = ""
67 guard !model.quickText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { parsing = false; return }
68 parsing = true
69 let key = requestKey
70 do {
71 try await Task.sleep(for: .milliseconds(350))
72 let result: QuickPreview = try await client.request("/api/native/quick/parse", method: "POST",
73 body: JSONEncoder().encode(["text": model.quickText, "zone": model.zone]))
74 guard !Task.isCancelled, requestKey == key else { return }
75 preview = result; parsing = false
76 } catch {
77 guard !Task.isCancelled, requestKey == key else { return }
78 self.error = error.localizedDescription; parsing = false
79 }
80 }
81 .onChange(of: model.quickText) { _, text in preview = nil; if !text.isEmpty { model.quickSavedNotice = "" } }
82 .onChange(of: model.zone) { _, _ in preview = nil }
83 }
84 private func save(_ preview: QuickPreview) {
85 error = ""
86 Task {
87 do { try await model.saveQuick(preview, client: client); self.preview = nil }
88 catch is CancellationError { }
89 catch { self.error = error.localizedDescription }
90 }
91 }
92}
93