forked from fritob/Camper-Monitor
CamperMonitor (Haupt-Repo) und VanAligneiOS (aus dem gemergten solar-integration-Branch) liefen unter zwei verschiedenen internen Namen, obwohl die App nach aussen längst einheitlich "VanControl Pro" heisst. Jetzt durchgängig VanControl: - Ordner: CamperMonitor/, CamperMonitorWatch/, CamperMonitorComplication/, VanAligneiOSWidget/ → VanControl/, VanControlWatch/, VanControlComplication/, VanControlWidget/ - Xcode-Projekt: CamperMonitor.xcodeproj → VanControl.xcodeproj, alle Targets/Schemes/Produktnamen entsprechend umbenannt - Bundle-Identifier auf Wunsch mitgeändert: de.s0.fototeddy.VanControl* (App noch nicht veröffentlicht); dabei auch die WKCompanionAppBundleIdentifier-Werte korrigiert, die noch das alte de.fritob-Präfix statt des tatsächlichen de.s0.fototeddy-Präfixes trugen - Swift-Dateien/Typen: CamperMonitorApp → VanControlApp, VanAligneiOSWidget* → VanControlWidget* - Config/*-Info.plist umbenannt, README.md/Tools/README.md/run-tests.sh auf die neuen Pfade angepasst Bewusst unverändert: firmware/vanalign und alle Bezüge auf "VanAlign" als Namen der Neigungsmesser-Hardware (eigenständiges Produkt, kein App-Name) sowie der komplette Android/-Ordner. Build (App, Watch, Debug) und Protokoll-Testlauf grün. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
116 lines
4.5 KiB
Swift
116 lines
4.5 KiB
Swift
import SwiftUI
|
||
import WatchKit
|
||
|
||
/// Der Ausrichtungs-Assistent am Handgelenk – der eigentliche Grund für die
|
||
/// Uhr: Beim Rangieren hat man das iPhone nicht in der Hand, aber den Arm am
|
||
/// Lenkrad. Ein Blick nach unten genügt, und die Rückmeldung „steht eben“ kommt
|
||
/// zusätzlich als Vibration, ganz ohne Blick.
|
||
///
|
||
/// Gerechnet wird mit demselben `AlignmentAssistant` wie am iPhone: kein
|
||
/// Ortsbezug, nur der zeitliche Verlauf der Neigung. Was besser und was
|
||
/// schlechter wird, steckt vollständig darin.
|
||
struct WatchAlignView: View {
|
||
@Environment(PhoneLink.self) private var link
|
||
@Environment(WatchLevelRadio.self) private var radio
|
||
@State private var assistant = AlignmentAssistant()
|
||
/// Damit die Vibration einmal kommt und nicht im Sekundentakt.
|
||
@State private var hasAnnouncedTarget = false
|
||
|
||
private var reading: WatchLevelReading { WatchLevelReading(radio: radio, phone: link) }
|
||
private var state: LevelState? { reading.hasReading ? reading.state : nil }
|
||
|
||
var body: some View {
|
||
// Ohne ScrollView: Beim Rangieren schaut man kurz hin, da darf nichts
|
||
// ausserhalb des Bildes liegen. Alles ist deshalb so bemessen, dass es
|
||
// auch auf der kleinsten Uhr auf einen Blick passt.
|
||
VStack(spacing: 3) {
|
||
if let current = assistant.current {
|
||
Text(String(format: "%.1f°", current.deviation))
|
||
.font(.system(size: 30, weight: .semibold, design: .rounded))
|
||
.monospacedDigit()
|
||
.foregroundStyle(assistant.hasReachedTarget ? .green : .primary)
|
||
.contentTransition(.numericText())
|
||
} else {
|
||
ProgressView()
|
||
.padding(.vertical, 6)
|
||
}
|
||
|
||
Label(assistant.trend.text, systemImage: assistant.trend.symbol)
|
||
.font(.caption2.weight(.medium))
|
||
.foregroundStyle(trendColor)
|
||
|
||
Text(assistant.advice)
|
||
.font(.system(size: 11))
|
||
.multilineTextAlignment(.center)
|
||
.foregroundStyle(.secondary)
|
||
.lineLimit(2)
|
||
.minimumScaleFactor(0.8)
|
||
|
||
if let state {
|
||
HStack(spacing: 8) {
|
||
WatchBubble(pitch: state.pitch, roll: state.roll)
|
||
.frame(width: 40, height: 40)
|
||
VStack(alignment: .leading, spacing: 1) {
|
||
Text(String(format: "Längs %.1f°", state.pitch ?? 0))
|
||
Text(String(format: "Quer %.1f°", state.roll ?? 0))
|
||
}
|
||
.font(.system(size: 11))
|
||
.monospacedDigit()
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
|
||
Button {
|
||
assistant.reset()
|
||
hasAnnouncedTarget = false
|
||
} label: {
|
||
Label("Von vorn", systemImage: "arrow.counterclockwise")
|
||
.font(.system(size: 10))
|
||
}
|
||
.buttonStyle(.borderless)
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||
.padding(.horizontal, 2)
|
||
.navigationTitle("Ausrichten")
|
||
.onChange(of: tilt, initial: true) { _, tilt in
|
||
guard let tilt else { return }
|
||
assistant.add(pitch: tilt.pitch, roll: tilt.roll)
|
||
announceIfLevel()
|
||
}
|
||
}
|
||
|
||
/// Ein Wertepaar als Auslöser: gerechnet wird nur, wenn sich die Neigung
|
||
/// wirklich ändert – nicht bei jeder Aktualisierung der Anzeige.
|
||
private var tilt: Tilt? {
|
||
guard let state, let pitch = state.pitch, let roll = state.roll else { return nil }
|
||
return Tilt(pitch: pitch, roll: roll)
|
||
}
|
||
|
||
private struct Tilt: Equatable {
|
||
let pitch: Double
|
||
let roll: Double
|
||
}
|
||
|
||
private var trendColor: Color {
|
||
switch assistant.trend {
|
||
case .improving: return .green
|
||
case .worsening: return .orange
|
||
default: return .secondary
|
||
}
|
||
}
|
||
|
||
/// Die Vibration ist der Punkt: Sie sagt „anhalten“, ohne dass jemand
|
||
/// hinschauen muss. Erneut gemeldet wird erst, wenn das Fahrzeug die
|
||
/// Toleranz zwischendurch wieder verlassen hat.
|
||
private func announceIfLevel() {
|
||
if assistant.hasReachedTarget {
|
||
guard !hasAnnouncedTarget else { return }
|
||
hasAnnouncedTarget = true
|
||
WKInterfaceDevice.current().play(.success)
|
||
} else {
|
||
hasAnnouncedTarget = false
|
||
}
|
||
}
|
||
}
|