krz/moonrise

Night-sky companion for iPhone, Apple Watch and Vision Pro: what's up tonight, the Moon, planets, launches and missions, and a Library of the solar system. SwiftUI app with a Cloudflare Workers backend. astronomy cloudflare-workers ios night-sky space swiftui visionos watchos

App/Sources/LogScreen.swift

59d401dca1a327705632d1e2e20f00f807dee828
moonrise/App/Sources/LogScreen.swift history · blame · raw

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}