import RastrilloApple import SwiftUI struct EventEditor: View { @ObservedObject var model: CalendarModel let client: NativeHTTP @State var event: CalendarEvent @State private var start: Date @State private var end: Date @State private var scheduleChanged = false @State private var wallClockFields: Bool @State private var saving = false @State private var error = "" @Environment(\.dismiss) private var dismiss init(model: CalendarModel, client: NativeHTTP, event: CalendarEvent) { self.model = model; self.client = client _event = State(initialValue: event) let parsedStart = model.parse(event.start_local, allDay: event.all_day, zone: event.tz) let parsedEnd = model.parse(event.end_local, allDay: event.all_day, zone: event.tz) _wallClockFields = State(initialValue: parsedStart == nil || parsedEnd == nil) _start = State(initialValue: parsedStart ?? model.date) _end = State(initialValue: parsedEnd ?? model.date) } var body: some View { NavigationStack { Form { Section { TextField("Title", text: $event.title).accessibilityIdentifier("eventTitle") TextField("Location", text: $event.location) if event.id == 0 { Picker("Calendar", selection: $event.calendar_id) { ForEach(model.calendars.filter(\.writable)) { Text($0.name).tag($0.id) } } } } Section("When") { Toggle("All day", isOn: $event.all_day).disabled(wallClockFields) if wallClockFields { TextField("Starts", text: $event.start_local) TextField("Ends", text: $event.end_local) 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) } else { DatePicker("Starts", selection: $start, displayedComponents: event.all_day ? [.date] : [.date, .hourAndMinute]) DatePicker(event.all_day ? "Ends before" : "Ends", selection: $end, displayedComponents: event.all_day ? [.date] : [.date, .hourAndMinute]) } LabeledContent("Time zone", value: event.tz) if event.all_day { Text("The end date is the first day after this event.").font(.footnote).foregroundStyle(.secondary) } } Section("Notes") { TextEditor(text: $event.notes).frame(minHeight: 100).accessibilityLabel("Notes") } if !error.isEmpty { Section { Text(error).foregroundStyle(.red) } } }.formStyle(.grouped).environment(\.timeZone, TimeZone(identifier: event.tz) ?? .gmt) .navigationTitle(event.id == 0 ? "New event" : "Edit event") .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() }.disabled(saving) } ToolbarItem(placement: .confirmationAction) { Button(saving ? "Saving…" : "Save") { if scheduleChanged && !wallClockFields { event.start_local = model.format(start, allDay: event.all_day, zone: event.tz) event.end_local = model.format(end, allDay: event.all_day, zone: event.tz) } saving = true Task { do { try await model.save(event, client: client); dismiss() } catch { self.error = error.localizedDescription; saving = false } } }.disabled(saving || event.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || (!wallClockFields && end <= start)) } } } #if os(macOS) .frame(width: 500, height: 560) #endif .onChange(of: start) { _, _ in scheduleChanged = true } .onChange(of: end) { _, _ in scheduleChanged = true } .onChange(of: event.all_day) { _, allDay in scheduleChanged = true if allDay && !wallClockFields { var calendar = Calendar(identifier: .gregorian) calendar.timeZone = TimeZone(identifier: event.tz) ?? .gmt start = calendar.startOfDay(for: start) end = max(calendar.startOfDay(for: end), calendar.date(byAdding: .day, value: 1, to: start)!) } } .interactiveDismissDisabled(saving) } }