import SwiftUI /// Der Einstieg: das Wichtigste untereinander, in der Reihenfolge, in der man /// im Wohnmobil danach schaut. struct WatchRootView: View { @Environment(PhoneLink.self) private var link var body: some View { NavigationStack { Group { if let payload = link.payload { content(for: payload) } else { waiting } } .navigationTitle(link.payload?.profile.name ?? "Camper") } } private func content(for payload: WatchPayload) -> some View { List { if let level = payload.levelDevice { Section { NavigationLink { WatchLevelView(device: level) } label: { LevelSummaryRow(state: level.level, link: level.link) } } } Section { ForEach(payload.devices.filter { $0.role != .leveling }) { device in NavigationLink { WatchDeviceDetailView(deviceID: device.id) } label: { DeviceRow(device: device) } } } Section { FreshnessNote(payload: payload, receivedAt: link.receivedAt) } } } private var waiting: some View { VStack(spacing: 8) { Image(systemName: "iphone.gen3.radiowaves.left.and.right") .font(.title2) .foregroundStyle(.tint) Text("Warte auf das iPhone") .font(.headline) Text("Die Werte kommen vom iPhone. Dort muss Camper Monitor " + "geöffnet sein.") .font(.caption2) .foregroundStyle(.secondary) .multilineTextAlignment(.center) } .padding(.horizontal, 4) } } /// Eine Gerätezeile: Name klein, Hauptwert gross – auf dem kleinen Bildschirm /// zählt nur der eine Wert, wegen dem man den Arm hebt. struct DeviceRow: View { let device: WatchDevice var body: some View { HStack(spacing: 8) { Image(systemName: device.role.symbol) .foregroundStyle(.tint) .frame(width: 20) VStack(alignment: .leading, spacing: 1) { Text(device.name) .font(.caption2) .foregroundStyle(.secondary) .lineLimit(1) if let metric = device.snapshot?.primaryMetric, metric.value != nil { Text(metric.formattedWithUnit) .font(.title3.weight(.semibold)) .monospacedDigit() .foregroundStyle(device.isLive ? .primary : .secondary) } else { Text(device.link.label) .font(.footnote) .foregroundStyle(.secondary) } } Spacer(minLength: 0) Circle() .fill(device.statusColor) .frame(width: 7, height: 7) } } } /// Die Nivellierung bekommt eine eigene, grössere Zeile: sie ist der Grund, /// aus dem man die Uhr im Fahrzeug überhaupt anschaut. struct LevelSummaryRow: View { let state: LevelState? let link: DeviceLinkState var body: some View { HStack(spacing: 10) { WatchBubble(pitch: state?.pitch, roll: state?.roll) .frame(width: 44, height: 44) VStack(alignment: .leading, spacing: 2) { Text("Nivellierung") .font(.caption2) .foregroundStyle(.secondary) // Ohne Messwerte steht hier, woran es liegt, statt eines // nichtssagenden Strichs. Text(state?.instruction ?? link.label) .font(.footnote.weight(.medium)) .lineLimit(2) } Spacer(minLength: 0) } .padding(.vertical, 2) } } /// Wie alt der Stand ist – auf der Uhr die wichtigste Nebenangabe. Alles hier /// ist weitergereicht; eine abgerissene Strecke zum iPhone sieht sonst genauso /// aus wie ein Fahrzeug, an dem sich nichts tut. struct FreshnessNote: View { let payload: WatchPayload let receivedAt: Date? var body: some View { TimelineView(.periodic(from: .now, by: 1)) { context in let age = context.date.timeIntervalSince(receivedAt ?? payload.generatedAt) Label { Text(text(age: age)) .font(.caption2) } icon: { Image(systemName: age > 10 ? "exclamationmark.triangle.fill" : "iphone.gen3") } .foregroundStyle(age > 10 ? .orange : .secondary) } } private func text(age: TimeInterval) -> String { if !payload.isRadioReady { return payload.radioStatus } if age < 3 { return "Stand: eben" } if age < 90 { return String(format: "Stand: vor %.0f s", age) } return String(format: "Stand: vor %.0f min – iPhone erreichbar?", age / 60) } } extension WatchDevice { /// Grün heisst: der Wert ist frisch und kommt vom Gerät selbst. var statusColor: Color { switch link { case .live: return (snapshot?.isStale ?? true) ? .orange : .green case .needsKey: return .orange case .failed: return .red default: return .gray } } }