App/Sources/LogScreen.swift
111 lines · 4853 bytes
3 symbols in this file
1import ObservingLog
2import SwiftUI
3
4/// The observing log lives in Documents/Observations, one Org file per entry, visible in the Files app.
5let observingLog = ObservingLog(folder: URL.documentsDirectory.appending(path: "Observations"))
6
7struct LogScreen: View {
8 @State private var entries: [Observation] = []
9 @State private var editing: Observation?
10 var newEntry: () -> Observation
11 @Environment(\.dismiss) private var dismiss
12
13 var body: some View {
14 NavigationStack {
15 List {
16 if entries.isEmpty {
17 Text("No observations yet. Add one here, or use Log on an object's card in the Sky tab.")
18 .font(.footnote).foregroundStyle(.secondary)
19 }
20 ForEach(entries) { o in
21 Button { editing = o } label: {
22 VStack(alignment: .leading, spacing: 3) {
23 Text(o.object).font(.body)
24 Text("\(o.date.formatted(date: .abbreviated, time: .shortened)) · \(o.place)").font(.footnote).foregroundStyle(.secondary)
25 if !o.notes.isEmpty { Text(o.notes).font(.footnote).lineLimit(3) }
26 }
27 }
28 .buttonStyle(.plain)
29 }
30 .onDelete { idx in
31 for i in idx { try? observingLog.delete(id: entries[i].id) }
32 reload()
33 }
34 }
35 .navigationTitle("Observing log")
36 .navigationBarTitleDisplayMode(.inline)
37 .toolbar {
38 ToolbarItem(placement: .cancellationAction) { Button("Done") { dismiss() } }
39 ToolbarItem(placement: .primaryAction) {
40 Button { editing = newEntry() } label: { Image(systemName: "plus") }
41 .accessibilityLabel("New observation")
42 }
43 }
44 .safeAreaInset(edge: .bottom) {
45 Text("Saved as Org files in Files › On My iPhone › Moonrise › Observations.")
46 .font(.caption2).foregroundStyle(.secondary).padding(8)
47 }
48 .sheet(item: $editing) { o in
49 ObservationEditor(observation: o) { reload() }
50 }
51 }
52 .onAppear(perform: reload)
53 }
54
55 private func reload() { entries = observingLog.all() }
56}
57
58struct ObservationEditor: View {
59 @State var observation: Observation
60 var onSave: () -> Void
61 @Environment(\.dismiss) private var dismiss
62 @State private var error: String?
63
64 private static let equipmentChoices = ["Naked eye", "Binoculars", "Telescope", "Camera"]
65
66 var body: some View {
67 NavigationStack {
68 Form {
69 Section {
70 TextField("Object", text: $observation.object)
71 DatePicker("Time", selection: $observation.date)
72 TextField("Place", text: $observation.place)
73 }
74 Section("Conditions") {
75 Picker("Equipment", selection: $observation.equipment) {
76 Text("Not noted").tag("")
77 ForEach(Self.equipmentChoices, id: \.self) { Text($0).tag($0) }
78 if !observation.equipment.isEmpty, !Self.equipmentChoices.contains(observation.equipment) {
79 Text(observation.equipment).tag(observation.equipment)
80 }
81 }
82 Picker("Seeing", selection: $observation.seeing) {
83 Text("Not noted").tag(Int?.none)
84 ForEach(1...5, id: \.self) { Text("\($0) of 5").tag(Int?.some($0)) }
85 }
86 }
87 Section("Notes") {
88 TextEditor(text: $observation.notes).frame(minHeight: 140)
89 }
90 if let error { Text(error).foregroundStyle(.red).font(.footnote) }
91 }
92 .navigationTitle(observation.object.isEmpty ? "New observation" : observation.object)
93 .navigationBarTitleDisplayMode(.inline)
94 .toolbar {
95 ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } }
96 ToolbarItem(placement: .confirmationAction) {
97 Button("Save") {
98 do {
99 try observingLog.save(observation)
100 onSave()
101 dismiss()
102 } catch {
103 self.error = "Couldn't save: \(error.localizedDescription)"
104 }
105 }
106 .disabled(observation.object.trimmingCharacters(in: .whitespaces).isEmpty)
107 }
108 }
109 }
110 }
111}