first commit

This commit is contained in:
fototeddy
2026-09-04 21:30:51 +02:00
commit da64b1b6bd
160 changed files with 20664 additions and 0 deletions
@@ -0,0 +1,115 @@
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
}
}
}