App/Sources/LogScreen.swift
121 lines · 5485 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 NavigationLink("Programs") { ProgramsScreen(newEntry: newEntry) }
17 if entries.isEmpty {
18 Text("No observations yet. Add one here, or use Log on an object's card in the Sky tab.")
19 .font(.footnote).foregroundStyle(.secondary)
20 }
21 ForEach(entries) { o in
22 Button { editing = o } label: {
23 VStack(alignment: .leading, spacing: 3) {
24 Text(o.object).font(.body)
25 Text("\(o.date.formatted(date: .abbreviated, time: .shortened)) · \(o.place)").font(.footnote).foregroundStyle(.secondary)
26 if !o.notes.isEmpty { Text(o.notes).font(.footnote).lineLimit(3) }
27 }
28 }
29 .buttonStyle(.plain)
30 }
31 .onDelete { idx in
32 for i in idx { try? observingLog.delete(id: entries[i].id) }
33 reload()
34 }
35 }
36 .navigationTitle("Observing log")
37 .navigationBarTitleDisplayMode(.inline)
38 .toolbar {
39 ToolbarItem(placement: .cancellationAction) { Button("Done") { dismiss() } }
40 ToolbarItem(placement: .primaryAction) {
41 NavigationLink { EquipmentScreen() } label: { Image(systemName: "binoculars") }
42 .accessibilityLabel("Equipment")
43 }
44 ToolbarItem(placement: .primaryAction) {
45 Button { editing = newEntry() } label: { Image(systemName: "plus") }
46 .accessibilityLabel("New observation")
47 }
48 }
49 .safeAreaInset(edge: .bottom) {
50 Text("Saved as Org files in Files › On My iPhone › Moonrise › Observations.")
51 .font(.caption2).foregroundStyle(.secondary).padding(8)
52 }
53 .sheet(item: $editing) { o in
54 ObservationEditor(observation: o) { reload() }
55 }
56 }
57 .onAppear(perform: reload)
58 }
59
60 private func reload() { entries = observingLog.all() }
61}
62
63struct ObservationEditor: View {
64 @State var observation: Observation
65 var onSave: () -> Void
66 @Environment(\.dismiss) private var dismiss
67 @State private var error: String?
68 @State private var names: [String: String] = [:]
69
70 /// Current inventory names for linked items, else the text stored with the entry.
71 private var equipmentSummary: String {
72 let current = observation.equipmentIDs.compactMap { names[$0] }
73 if !current.isEmpty, current.count == observation.equipmentIDs.count { return current.joined(separator: ", ") }
74 return observation.equipment.isEmpty ? "Not noted" : observation.equipment
75 }
76
77 var body: some View {
78 NavigationStack {
79 Form {
80 Section {
81 TextField("Object", text: $observation.object)
82 DatePicker("Time", selection: $observation.date)
83 TextField("Place", text: $observation.place)
84 }
85 Section("Conditions") {
86 NavigationLink { EquipmentPicker(observation: $observation) } label: {
87 LabeledContent("Equipment", value: equipmentSummary)
88 }
89 Picker("Seeing", selection: $observation.seeing) {
90 Text("Not noted").tag(Int?.none)
91 ForEach(1...5, id: \.self) { Text("\($0) of 5").tag(Int?.some($0)) }
92 }
93 }
94 Section("Notes") {
95 TextEditor(text: $observation.notes).frame(minHeight: 140)
96 }
97 if let error { Text(error).foregroundStyle(.red).font(.footnote) }
98 }
99 .navigationTitle(observation.object.isEmpty ? "New observation" : observation.object)
100 .navigationBarTitleDisplayMode(.inline)
101 .toolbar {
102 ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } }
103 ToolbarItem(placement: .confirmationAction) {
104 Button("Save") {
105 do {
106 try observingLog.save(observation, resolver: CatalogResolver(programs: observingLog.programs()))
107 onSave()
108 dismiss()
109 } catch {
110 self.error = "Couldn't save: \(error.localizedDescription)"
111 }
112 }
113 .disabled(observation.object.trimmingCharacters(in: .whitespaces).isEmpty)
114 }
115 }
116 .onAppear {
117 names = Dictionary(equipmentInventory.all().map { ($0.id, $0.name) }, uniquingKeysWith: { a, _ in a })
118 }
119 }
120 }
121}