forked from fritob/Camper-Monitor
Uhr: Nivellierung auf drei Seiten, Assistent passt ohne Scrollen
Alles auf einem Bildschirm hiess scrollen, und auf dieser Grösse ist das der falsche Handgriff: Einen Wisch trifft man beim Rangieren auch ohne hinzusehen, eine Scrollposition nicht. Was gerade zählt, hängt ausserdem vom Arbeitsschritt ab. Jetzt drei Seiten zum Wischen: * Neigung – Fahrzeugansicht oder Libelle, was zu tun ist, Herkunft der Werte * Keile – die Höhe je Rad. Fehlen Spurweite und Radstand, steht das dort, statt dass die Seite leer wirkt. * Ausrichten – der Assistent, vorher hinter einem Knopf Der Assistent ist dabei enger gesetzt: kleinere Zahl, zwei Zeilen Rat mit Skalierung, kleinere Libelle, und kein ScrollView mehr. Beim Rangieren schaut man kurz hin, da darf nichts ausserhalb des Bildes liegen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,52 +20,58 @@ struct WatchAlignView: View {
|
||||
private var state: LevelState? { reading.hasReading ? reading.state : nil }
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 8) {
|
||||
if let current = assistant.current {
|
||||
Text(String(format: "%.1f°", current.deviation))
|
||||
.font(.system(size: 40, weight: .semibold, design: .rounded))
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(assistant.hasReachedTarget ? .green : .primary)
|
||||
.contentTransition(.numericText())
|
||||
} else {
|
||||
ProgressView()
|
||||
.padding(.vertical, 12)
|
||||
}
|
||||
|
||||
Label(assistant.trend.text, systemImage: assistant.trend.symbol)
|
||||
.font(.footnote.weight(.medium))
|
||||
.foregroundStyle(trendColor)
|
||||
|
||||
Text(assistant.advice)
|
||||
.font(.caption)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
if let state {
|
||||
HStack(spacing: 12) {
|
||||
WatchBubble(pitch: state.pitch, roll: state.roll)
|
||||
.frame(width: 56, height: 56)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(String(format: "Längs %.1f°", state.pitch ?? 0))
|
||||
Text(String(format: "Quer %.1f°", state.roll ?? 0))
|
||||
}
|
||||
.font(.caption2)
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.top, 2)
|
||||
}
|
||||
|
||||
Button("Von vorn messen", systemImage: "arrow.counterclockwise") {
|
||||
assistant.reset()
|
||||
hasAnnouncedTarget = false
|
||||
}
|
||||
.font(.caption2)
|
||||
.padding(.top, 4)
|
||||
// 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)
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
|
||||
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 }
|
||||
|
||||
@@ -1,16 +1,45 @@
|
||||
import SwiftUI
|
||||
|
||||
/// Nivellierung auf der Uhr.
|
||||
/// Nivellierung auf der Uhr, aufgeteilt auf drei Seiten zum Wischen.
|
||||
///
|
||||
/// Auf einem Bildschirm dieser Grösse ist Wischen besser als Scrollen: Beim
|
||||
/// Rangieren trifft man einen Wisch auch ohne hinzusehen, eine Scrollposition
|
||||
/// dagegen nicht – und was gerade wichtig ist, hängt vom Arbeitsschritt ab.
|
||||
///
|
||||
/// 1. **Neigung** – wie das Fahrzeug steht, als Fahrzeugansicht oder Libelle.
|
||||
/// 2. **Keile** – wie hoch jedes Rad unterlegt werden muss.
|
||||
/// 3. **Ausrichten** – der Assistent fürs Rangieren, mit Vibration.
|
||||
///
|
||||
/// Die Werte kommen bevorzugt **direkt vom Sensor** (`WatchLevelRadio`); nur
|
||||
/// wenn der gerade nicht erreichbar ist, tritt der zuletzt über das iPhone
|
||||
/// gemeldete Stand an seine Stelle. Woher sie stammen, steht unten – ein alter
|
||||
/// Wert sieht sonst aus wie ein stillstehendes Fahrzeug.
|
||||
/// gemeldete Stand an seine Stelle. Woher sie stammen, steht auf der ersten
|
||||
/// Seite – ein alter Wert sieht sonst aus wie ein stillstehendes Fahrzeug.
|
||||
///
|
||||
/// Kalibriert wird hier nichts. Das gehört einmalig ans iPhone, mit ebenem
|
||||
/// Fahrzeug und Ruhe dafür; auf der Uhr wäre der Knopf nur eine Gelegenheit,
|
||||
/// die Nullage aus Versehen zu verstellen.
|
||||
struct WatchLevelView: View {
|
||||
@State private var page = Page.reading
|
||||
|
||||
enum Page: Hashable {
|
||||
case reading, wedges, align
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $page) {
|
||||
LevelReadingPage()
|
||||
.tag(Page.reading)
|
||||
WedgePage()
|
||||
.tag(Page.wedges)
|
||||
WatchAlignView()
|
||||
.tag(Page.align)
|
||||
}
|
||||
.tabViewStyle(.page)
|
||||
}
|
||||
}
|
||||
|
||||
/// Erste Seite: wie das Fahrzeug steht.
|
||||
struct LevelReadingPage: View {
|
||||
@Environment(PhoneLink.self) private var link
|
||||
@Environment(WatchLevelRadio.self) private var radio
|
||||
@AppStorage("levelDisplayStyle") private var showsVehicle = true
|
||||
@@ -36,16 +65,6 @@ struct WatchLevelView: View {
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(reading.state.isLevel ? .green : .primary)
|
||||
|
||||
if let lift, !lift.isNegligible {
|
||||
WheelLiftPlan(lift: lift, isCompact: true)
|
||||
.padding(.top, 2)
|
||||
}
|
||||
|
||||
NavigationLink(value: WatchRoute.align) {
|
||||
Label("Ausrichten", systemImage: "arrow.left.and.right")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
|
||||
Button {
|
||||
showsVehicle.toggle()
|
||||
} label: {
|
||||
@@ -58,7 +77,7 @@ struct WatchLevelView: View {
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
.navigationTitle("Nivellierung")
|
||||
.navigationTitle("Neigung")
|
||||
}
|
||||
|
||||
private func angle(_ title: String, _ value: Double?) -> some View {
|
||||
@@ -71,6 +90,49 @@ struct WatchLevelView: View {
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Zweite Seite: die Keilhöhen, als Draufsicht auf die vier Räder.
|
||||
struct WedgePage: View {
|
||||
@Environment(PhoneLink.self) private var link
|
||||
@Environment(WatchLevelRadio.self) private var radio
|
||||
|
||||
private var reading: WatchLevelReading { WatchLevelReading(radio: radio, phone: link) }
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 8) {
|
||||
if let lift, !lift.isNegligible {
|
||||
WheelLiftPlan(lift: lift, isCompact: true)
|
||||
Text("Zentimeter unter das jeweilige Rad. Das höchste bleibt liegen.")
|
||||
.font(.system(size: 10))
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
} else if hasMeasurements {
|
||||
Label("Keine Keile nötig", systemImage: "checkmark.circle.fill")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.green)
|
||||
.padding(.top, 20)
|
||||
} else {
|
||||
// Ohne Fahrzeugmasse lässt sich nichts rechnen. Das gehört
|
||||
// gesagt, sonst wirkt die Seite kaputt.
|
||||
Label("Fahrzeugmasse fehlen", systemImage: "ruler")
|
||||
.font(.footnote.weight(.medium))
|
||||
Text("Spurweite und Radstand am iPhone im Fahrzeugprofil "
|
||||
+ "hinterlegen, dann steht hier die Keilhöhe je Rad.")
|
||||
.font(.system(size: 10))
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
.navigationTitle("Keile")
|
||||
}
|
||||
|
||||
private var hasMeasurements: Bool {
|
||||
link.payload?.profile.trackWidth != nil && link.payload?.profile.wheelbase != nil
|
||||
}
|
||||
|
||||
/// Wie hoch jedes Rad unterlegt werden muss – reine Geometrie, und nur zu
|
||||
/// haben, wenn im Fahrzeugprofil Spurweite und Radstand stehen.
|
||||
|
||||
@@ -18,7 +18,6 @@ struct WatchRootView: View {
|
||||
.navigationDestination(for: WatchRoute.self) { route in
|
||||
switch route {
|
||||
case .level: WatchLevelView()
|
||||
case .align: WatchAlignView()
|
||||
case .device(let id): WatchDeviceDetailView(deviceID: id)
|
||||
}
|
||||
}
|
||||
@@ -81,7 +80,6 @@ struct WatchRootView: View {
|
||||
/// gesetzt wird – von der Komplikation.
|
||||
enum WatchRoute: Hashable {
|
||||
case level
|
||||
case align
|
||||
case device(UUID)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user