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

v1.0.0
moonrise/App/Sources/LogScreen.swift history · blame · raw

111 lines · 4853 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                    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}