| 1 | import RastrilloApple |
| 2 | import SwiftUI |
| 3 | |
| 4 | struct EventEditor: View { |
| 5 | @ObservedObject var model: CalendarModel |
| 6 | let client: NativeHTTP |
| 7 | @State var event: CalendarEvent |
| 8 | @State private var start: Date |
| 9 | @State private var end: Date |
| 10 | @State private var scheduleChanged = false |
| 11 | @State private var wallClockFields: Bool |
| 12 | @State private var saving = false |
| 13 | @State private var error = "" |
| 14 | @Environment(\.dismiss) private var dismiss |
| 15 | |
| 16 | init(model: CalendarModel, client: NativeHTTP, event: CalendarEvent) { |
| 17 | self.model = model; self.client = client |
| 18 | _event = State(initialValue: event) |
| 19 | let parsedStart = model.parse(event.start_local, allDay: event.all_day, zone: event.tz) |
| 20 | let parsedEnd = model.parse(event.end_local, allDay: event.all_day, zone: event.tz) |
| 21 | _wallClockFields = State(initialValue: parsedStart == nil || parsedEnd == nil) |
| 22 | _start = State(initialValue: parsedStart ?? model.date) |
| 23 | _end = State(initialValue: parsedEnd ?? model.date) |
| 24 | } |
| 25 | var body: some View { |
| 26 | NavigationStack { |
| 27 | Form { |
| 28 | Section { |
| 29 | TextField("Title", text: $event.title).accessibilityIdentifier("eventTitle") |
| 30 | TextField("Location", text: $event.location) |
| 31 | if event.id == 0 { |
| 32 | Picker("Calendar", selection: $event.calendar_id) { |
| 33 | ForEach(model.calendars.filter(\.writable)) { Text($0.name).tag($0.id) } |
| 34 | } |
| 35 | } |
| 36 | } |
| 37 | Section("When") { |
| 38 | Toggle("All day", isOn: $event.all_day).disabled(wallClockFields) |
| 39 | if wallClockFields { |
| 40 | TextField("Starts", text: $event.start_local) |
| 41 | TextField("Ends", text: $event.end_local) |
| 42 | Text("This event uses a local time during a clock change. Its original local dates are preserved; edit them as YYYY-MM-DDTHH:mm.").font(.footnote).foregroundStyle(.secondary) |
| 43 | } else { |
| 44 | DatePicker("Starts", selection: $start, displayedComponents: event.all_day ? [.date] : [.date, .hourAndMinute]) |
| 45 | DatePicker(event.all_day ? "Ends before" : "Ends", selection: $end, displayedComponents: event.all_day ? [.date] : [.date, .hourAndMinute]) |
| 46 | } |
| 47 | LabeledContent("Time zone", value: event.tz) |
| 48 | if event.all_day { Text("The end date is the first day after this event.").font(.footnote).foregroundStyle(.secondary) } |
| 49 | } |
| 50 | Section("Notes") { TextEditor(text: $event.notes).frame(minHeight: 100).accessibilityLabel("Notes") } |
| 51 | if !error.isEmpty { Section { Text(error).foregroundStyle(.red) } } |
| 52 | }.formStyle(.grouped).environment(\.timeZone, TimeZone(identifier: event.tz) ?? .gmt) |
| 53 | .navigationTitle(event.id == 0 ? "New event" : "Edit event") |
| 54 | .toolbar { |
| 55 | ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() }.disabled(saving) } |
| 56 | ToolbarItem(placement: .confirmationAction) { |
| 57 | Button(saving ? "Saving…" : "Save") { |
| 58 | if scheduleChanged && !wallClockFields { |
| 59 | event.start_local = model.format(start, allDay: event.all_day, zone: event.tz) |
| 60 | event.end_local = model.format(end, allDay: event.all_day, zone: event.tz) |
| 61 | } |
| 62 | saving = true |
| 63 | Task { |
| 64 | do { try await model.save(event, client: client); dismiss() } |
| 65 | catch { self.error = error.localizedDescription; saving = false } |
| 66 | } |
| 67 | }.disabled(saving || event.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || (!wallClockFields && end <= start)) |
| 68 | } |
| 69 | } |
| 70 | } |
| 71 | #if os(macOS) |
| 72 | .frame(width: 500, height: 560) |
| 73 | #endif |
| 74 | .onChange(of: start) { _, _ in scheduleChanged = true } |
| 75 | .onChange(of: end) { _, _ in scheduleChanged = true } |
| 76 | .onChange(of: event.all_day) { _, allDay in |
| 77 | scheduleChanged = true |
| 78 | if allDay && !wallClockFields { |
| 79 | var calendar = Calendar(identifier: .gregorian) |
| 80 | calendar.timeZone = TimeZone(identifier: event.tz) ?? .gmt |
| 81 | start = calendar.startOfDay(for: start) |
| 82 | end = max(calendar.startOfDay(for: end), calendar.date(byAdding: .day, value: 1, to: start)!) |
| 83 | } |
| 84 | } |
| 85 | .interactiveDismissDisabled(saving) |
| 86 | } |
| 87 | } |
| 88 | |