App/Sources/LogScreen.swift
120 lines · 5341 bytes
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 NavigationLink { EquipmentScreen() } label: { Image(systemName: "binoculars") }
41 .accessibilityLabel("Equipment")
42 }
43 ToolbarItem(placement: .primaryAction) {
44 Button { editing = newEntry() } label: { Image(systemName: "plus") }
45 .accessibilityLabel("New observation")
46 }
47 }
48 .safeAreaInset(edge: .bottom) {
49 Text("Saved as Org files in Files › On My iPhone › Moonrise › Observations.")
50 .font(.caption2).foregroundStyle(.secondary).padding(8)
51 }
52 .sheet(item: $editing) { o in
53 ObservationEditor(observation: o) { reload() }
54 }
55 }
56 .onAppear(perform: reload)
57 }
58
59 private func reload() { entries = observingLog.all() }
60}
61
62struct ObservationEditor: View {
63 @State var observation: Observation
64 var onSave: () -> Void
65 @Environment(\.dismiss) private var dismiss
66 @State private var error: String?
67 @State private var names: [String: String] = [:]
68
69 /// Current inventory names for linked items, else the text stored with the entry.
70 private var equipmentSummary: String {
71 let current = observation.equipmentIDs.compactMap { names[$0] }
72 if !current.isEmpty, current.count == observation.equipmentIDs.count { return current.joined(separator: ", ") }
73 return observation.equipment.isEmpty ? "Not noted" : observation.equipment
74 }
75
76 var body: some View {
77 NavigationStack {
78 Form {
79 Section {
80 TextField("Object", text: $observation.object)
81 DatePicker("Time", selection: $observation.date)
82 TextField("Place", text: $observation.place)
83 }
84 Section("Conditions") {
85 NavigationLink { EquipmentPicker(observation: $observation) } label: {
86 LabeledContent("Equipment", value: equipmentSummary)
87 }
88 Picker("Seeing", selection: $observation.seeing) {
89 Text("Not noted").tag(Int?.none)
90 ForEach(1...5, id: \.self) { Text("\($0) of 5").tag(Int?.some($0)) }
91 }
92 }
93 Section("Notes") {
94 TextEditor(text: $observation.notes).frame(minHeight: 140)
95 }
96 if let error { Text(error).foregroundStyle(.red).font(.footnote) }
97 }
98 .navigationTitle(observation.object.isEmpty ? "New observation" : observation.object)
99 .navigationBarTitleDisplayMode(.inline)
100 .toolbar {
101 ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } }
102 ToolbarItem(placement: .confirmationAction) {
103 Button("Save") {
104 do {
105 try observingLog.save(observation)
106 onSave()
107 dismiss()
108 } catch {
109 self.error = "Couldn't save: \(error.localizedDescription)"
110 }
111 }
112 .disabled(observation.object.trimmingCharacters(in: .whitespaces).isEmpty)
113 }
114 }
115 .onAppear {
116 names = Dictionary(equipmentInventory.all().map { ($0.id, $0.name) }, uniquingKeysWith: { a, _ in a })
117 }
118 }
119 }
120}