Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26e088e48b | ||
|
|
4a8a40eea2 |
@@ -138,15 +138,15 @@ struct AlignmentAssistantView: View {
|
|||||||
|
|
||||||
private var readings: some View {
|
private var readings: some View {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
reading("Längs", state.pitch)
|
reading("Längs", LevelDirectionFormatting.pitchTile(state.pitch))
|
||||||
reading("Quer", state.roll)
|
reading("Quer", LevelDirectionFormatting.rollTile(state.roll))
|
||||||
reading("Gesamt", assistant.current?.deviation)
|
reading("Gesamt", LevelDirectionFormatting.magnitude(assistant.current?.deviation))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func reading(_ title: String, _ value: Double?) -> some View {
|
private func reading(_ title: String, _ text: String) -> some View {
|
||||||
VStack(spacing: 4) {
|
VStack(spacing: 4) {
|
||||||
Text(value.map { String(format: "%.1f°", $0) } ?? "–")
|
Text(text)
|
||||||
.font(.title2.weight(.semibold).monospacedDigit())
|
.font(.title2.weight(.semibold).monospacedDigit())
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ struct DeviceCard: View {
|
|||||||
|
|
||||||
if isShowingLastSettings, let snapshot {
|
if isShowingLastSettings, let snapshot {
|
||||||
lastSettings(for: snapshot)
|
lastSettings(for: snapshot)
|
||||||
|
} else if let snapshot, snapshot.primaryMetric != nil, device.role == .leveling {
|
||||||
|
levelReadout(for: snapshot)
|
||||||
} else if let snapshot, let primary = snapshot.primaryMetric {
|
} else if let snapshot, let primary = snapshot.primaryMetric {
|
||||||
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
||||||
Text(primary.formatted)
|
Text(primary.formatted)
|
||||||
@@ -115,6 +117,21 @@ struct DeviceCard: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Für die Nivellierung: Front/Heck und links/rechts nebeneinander, groß,
|
||||||
|
/// ohne "Längsneigung"/"Querneigung"-Beschriftung – die Richtung steht ja
|
||||||
|
/// schon im vorangestellten Buchstaben.
|
||||||
|
private func levelReadout(for snapshot: DeviceSnapshot) -> some View {
|
||||||
|
let pitch = snapshot.metrics.first(where: { $0.key == "pitch" })?.value
|
||||||
|
let roll = snapshot.metrics.first(where: { $0.key == "roll" })?.value
|
||||||
|
return HStack(spacing: 24) {
|
||||||
|
Text(LevelDirectionFormatting.pitchTile(pitch))
|
||||||
|
Text(LevelDirectionFormatting.rollTile(roll))
|
||||||
|
}
|
||||||
|
.font(.system(size: 36, weight: .semibold, design: .rounded))
|
||||||
|
.contentTransition(.numericText())
|
||||||
|
.foregroundStyle(snapshot.isStale ? .secondary : .primary)
|
||||||
|
}
|
||||||
|
|
||||||
private func secondaryValues(for snapshot: DeviceSnapshot) -> some View {
|
private func secondaryValues(for snapshot: DeviceSnapshot) -> some View {
|
||||||
let others = snapshot.metrics
|
let others = snapshot.metrics
|
||||||
.filter { $0.id != snapshot.primaryMetric?.id && $0.value != nil }
|
.filter { $0.id != snapshot.primaryMetric?.id && $0.value != nil }
|
||||||
|
|||||||
@@ -76,8 +76,9 @@ struct LevelBubble: View {
|
|||||||
.fill(bubbleColor)
|
.fill(bubbleColor)
|
||||||
.frame(width: bubble, height: bubble)
|
.frame(width: bubble, height: bubble)
|
||||||
// Positiver Pitch heisst: das Heck steht höher, die Blase
|
// Positiver Pitch heisst: das Heck steht höher, die Blase
|
||||||
// wandert also nach oben – in der Ansicht nach hinten.
|
// wandert also nach unten – in der Ansicht nach hinten.
|
||||||
.offset(x: offsetX, y: -offsetY)
|
// Muss zur "Fahrzeug"-Ansicht (VehicleTiltView) passen.
|
||||||
|
.offset(x: offsetX, y: offsetY)
|
||||||
.animation(.spring(duration: 0.35), value: offsetX)
|
.animation(.spring(duration: 0.35), value: offsetX)
|
||||||
.animation(.spring(duration: 0.35), value: offsetY)
|
.animation(.spring(duration: 0.35), value: offsetY)
|
||||||
.opacity(pitch == nil && roll == nil ? 0.25 : 1)
|
.opacity(pitch == nil && roll == nil ? 0.25 : 1)
|
||||||
@@ -138,8 +139,8 @@ struct LevelControls: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HStack(spacing: 24) {
|
HStack(spacing: 24) {
|
||||||
reading("Längs", state.pitch)
|
reading("Längs", LevelDirectionFormatting.pitchTile(state.pitch))
|
||||||
reading("Quer", state.roll)
|
reading("Quer", LevelDirectionFormatting.rollTile(state.roll))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
@@ -239,9 +240,9 @@ struct LevelControls: View {
|
|||||||
state.isKnownUncalibrated ? "Nullpunkt fehlt" : device.sensorOrientation.summary
|
state.isKnownUncalibrated ? "Nullpunkt fehlt" : device.sensorOrientation.summary
|
||||||
}
|
}
|
||||||
|
|
||||||
private func reading(_ title: String, _ value: Double?) -> some View {
|
private func reading(_ title: String, _ text: String) -> some View {
|
||||||
VStack(spacing: 2) {
|
VStack(spacing: 2) {
|
||||||
Text(value.map { String(format: "%.1f°", $0) } ?? "–")
|
Text(text)
|
||||||
.font(.title2.weight(.semibold).monospacedDigit())
|
.font(.title2.weight(.semibold).monospacedDigit())
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ struct WatchBubble: View {
|
|||||||
.fill(color)
|
.fill(color)
|
||||||
.frame(width: bubble, height: bubble)
|
.frame(width: bubble, height: bubble)
|
||||||
// Positiver Pitch heisst: das Heck steht höher, die Blase
|
// Positiver Pitch heisst: das Heck steht höher, die Blase
|
||||||
// wandert nach oben – in der Ansicht also nach hinten.
|
// wandert nach unten – in der Ansicht also nach hinten.
|
||||||
.offset(x: clamped(roll) / range * travel,
|
.offset(x: clamped(roll) / range * travel,
|
||||||
y: -clamped(pitch) / range * travel)
|
y: clamped(pitch) / range * travel)
|
||||||
.animation(.spring(duration: 0.3), value: pitch)
|
.animation(.spring(duration: 0.3), value: pitch)
|
||||||
.animation(.spring(duration: 0.3), value: roll)
|
.animation(.spring(duration: 0.3), value: roll)
|
||||||
.opacity(pitch == nil && roll == nil ? 0.25 : 1)
|
.opacity(pitch == nil && roll == nil ? 0.25 : 1)
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Formatiert Neigungswerte mit vorangestelltem Richtungsbuchstaben statt
|
||||||
|
/// Vorzeichen – "H 1.8°" statt "1.8°", "L 0.9°" statt "-0.9°".
|
||||||
|
///
|
||||||
|
/// Gemeinsam für App und Live-Activity-Widget, deshalb hier statt in
|
||||||
|
/// `Shared/`, das die Extension nicht mitkompiliert (siehe
|
||||||
|
/// `LevelActivityAttributes.swift`).
|
||||||
|
enum LevelDirectionFormatting {
|
||||||
|
/// "1.8°" – Betrag ohne Vorzeichen, für Stellen, die die Richtung schon
|
||||||
|
/// im Wort oder Buchstaben ausdrücken.
|
||||||
|
static func magnitude(_ value: Double?) -> String {
|
||||||
|
guard let value else { return "–" }
|
||||||
|
return String(format: "%.1f°", abs(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Längsneigung als Kachel: positiv (Heck höher) → "H", negativ (Front
|
||||||
|
/// höher) → "F". Muss zum Vorzeichen von `LevelState.pitch` passen.
|
||||||
|
static func pitchTile(_ value: Double?) -> String {
|
||||||
|
guard let value else { return "–" }
|
||||||
|
return "\(value >= 0 ? "H" : "F") \(magnitude(value))"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Querneigung als Kachel: positiv (rechts höher) → "R", negativ (links
|
||||||
|
/// höher) → "L". Muss zum Vorzeichen von `LevelState.roll` passen.
|
||||||
|
static func rollTile(_ value: Double?) -> String {
|
||||||
|
guard let value else { return "–" }
|
||||||
|
return "\(value >= 0 ? "R" : "L") \(magnitude(value))"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,14 +9,6 @@ import ActivityKit
|
|||||||
import WidgetKit
|
import WidgetKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
/// Formatiert Neigungswerte wie in der App – °, eine Nachkommastelle, "–"
|
|
||||||
/// wenn kein Messwert da ist. Eigene Kopie, da die Extension nicht von
|
|
||||||
/// `Shared/Models/Metric.swift` abhängt.
|
|
||||||
private func degreesText(_ value: Double?) -> String {
|
|
||||||
guard let value else { return "–" }
|
|
||||||
return String(format: "%.1f°", value)
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct LevelIcon: View {
|
private struct LevelIcon: View {
|
||||||
let isLevel: Bool
|
let isLevel: Bool
|
||||||
let hasReading: Bool
|
let hasReading: Bool
|
||||||
@@ -73,8 +65,8 @@ private struct LevelBubbleGlyph: View {
|
|||||||
.fill(bubbleColor)
|
.fill(bubbleColor)
|
||||||
.frame(width: bubble, height: bubble)
|
.frame(width: bubble, height: bubble)
|
||||||
// Positiver Pitch heisst: Heck steht höher, die Blase wandert
|
// Positiver Pitch heisst: Heck steht höher, die Blase wandert
|
||||||
// also nach oben – wie bei der echten Wasserwaage in der App.
|
// also nach unten – wie bei der echten Wasserwaage in der App.
|
||||||
.offset(x: offsetX, y: -offsetY)
|
.offset(x: offsetX, y: offsetY)
|
||||||
.opacity(hasReading ? 1 : 0.3)
|
.opacity(hasReading ? 1 : 0.3)
|
||||||
}
|
}
|
||||||
.frame(width: diameter, height: diameter)
|
.frame(width: diameter, height: diameter)
|
||||||
@@ -94,27 +86,24 @@ struct VanAligneiOSWidgetLiveActivity: Widget {
|
|||||||
.activitySystemActionForegroundColor(.white)
|
.activitySystemActionForegroundColor(.white)
|
||||||
|
|
||||||
} dynamicIsland: { context in
|
} dynamicIsland: { context in
|
||||||
|
// Die Dynamic Island bleibt bewusst so klein wie möglich – Libelle
|
||||||
|
// und Zahlenwerte gehören auf Sperrbildschirm/CarPlay, hier reicht
|
||||||
|
// ein Blick auf Icon und, aufgeklappt, die Kurzanweisung.
|
||||||
DynamicIsland {
|
DynamicIsland {
|
||||||
DynamicIslandExpandedRegion(.leading) {
|
DynamicIslandExpandedRegion(.leading) {
|
||||||
LevelBubbleGlyph(pitch: context.state.pitch, roll: context.state.roll,
|
LevelIcon(isLevel: context.state.isLevel, hasReading: context.state.pitch != nil)
|
||||||
isLevel: context.state.isLevel, diameter: 44)
|
.font(.title3)
|
||||||
}
|
|
||||||
DynamicIslandExpandedRegion(.trailing) {
|
|
||||||
VStack(alignment: .trailing, spacing: 2) {
|
|
||||||
Text(degreesText(context.state.pitch)).monospacedDigit()
|
|
||||||
Text(degreesText(context.state.roll)).monospacedDigit()
|
|
||||||
}
|
|
||||||
.font(.headline)
|
|
||||||
}
|
}
|
||||||
DynamicIslandExpandedRegion(.bottom) {
|
DynamicIslandExpandedRegion(.bottom) {
|
||||||
Text(context.state.instruction ?? context.attributes.deviceName)
|
Text(context.state.instruction ?? context.attributes.deviceName)
|
||||||
.font(.subheadline)
|
.font(.caption)
|
||||||
.foregroundStyle(context.state.isLevel ? .green : .primary)
|
.foregroundStyle(context.state.isLevel ? .green : .primary)
|
||||||
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
} compactLeading: {
|
} compactLeading: {
|
||||||
LevelIcon(isLevel: context.state.isLevel, hasReading: context.state.pitch != nil)
|
LevelIcon(isLevel: context.state.isLevel, hasReading: context.state.pitch != nil)
|
||||||
} compactTrailing: {
|
} compactTrailing: {
|
||||||
Text(degreesText(context.state.pitch)).monospacedDigit()
|
EmptyView()
|
||||||
} minimal: {
|
} minimal: {
|
||||||
LevelIcon(isLevel: context.state.isLevel, hasReading: context.state.pitch != nil)
|
LevelIcon(isLevel: context.state.isLevel, hasReading: context.state.pitch != nil)
|
||||||
}
|
}
|
||||||
@@ -130,40 +119,52 @@ private struct LockScreenLevelView: View {
|
|||||||
let attributes: LevelActivityAttributes
|
let attributes: LevelActivityAttributes
|
||||||
let state: LevelActivityAttributes.ContentState
|
let state: LevelActivityAttributes.ContentState
|
||||||
|
|
||||||
var body: some View {
|
/// Muss mit `LevelState.levelTolerance` übereinstimmen.
|
||||||
HStack(spacing: 16) {
|
private let tolerance: Double = 0.5
|
||||||
LevelBubbleGlyph(pitch: state.pitch, roll: state.roll, isLevel: state.isLevel)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
/// Längszeile, z.B. "Heck steht höher 1.8°" – nur wenn ausserhalb der
|
||||||
|
/// Toleranz, genau wie bei `LevelState.instruction` in der App.
|
||||||
|
private var pitchLine: String? {
|
||||||
|
guard let pitch = state.pitch, abs(pitch) > tolerance else { return nil }
|
||||||
|
let label = pitch >= 0 ? "Heck steht höher" : "Front steht höher"
|
||||||
|
return "\(label) \(LevelDirectionFormatting.magnitude(pitch))"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Querzeile, z.B. "links steht höher 0.9°".
|
||||||
|
private var rollLine: String? {
|
||||||
|
guard let roll = state.roll, abs(roll) > tolerance else { return nil }
|
||||||
|
let label = roll >= 0 ? "rechts steht höher" : "links steht höher"
|
||||||
|
return "\(label) \(LevelDirectionFormatting.magnitude(roll))"
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 20) {
|
||||||
|
LevelBubbleGlyph(pitch: state.pitch, roll: state.roll, isLevel: state.isLevel, diameter: 64)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
Text(attributes.deviceName)
|
Text(attributes.deviceName)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
Text(state.instruction ?? "Keine Messwerte")
|
|
||||||
.font(.headline)
|
if pitchLine == nil && rollLine == nil {
|
||||||
|
Text(state.pitch == nil && state.roll == nil ? "Keine Messwerte" : "Steht eben")
|
||||||
|
.font(.title3.weight(.semibold))
|
||||||
.foregroundStyle(state.isLevel ? .green : .white)
|
.foregroundStyle(state.isLevel ? .green : .white)
|
||||||
.lineLimit(2)
|
} else {
|
||||||
|
if let pitchLine {
|
||||||
|
Text(pitchLine).font(.title3.weight(.semibold))
|
||||||
|
}
|
||||||
|
if let rollLine {
|
||||||
|
Text(rollLine).font(.title3.weight(.semibold))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer(minLength: 0)
|
||||||
|
|
||||||
VStack(alignment: .trailing, spacing: 2) {
|
|
||||||
reading("Längs", state.pitch)
|
|
||||||
reading("Quer", state.roll)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding(16)
|
.padding(16)
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func reading(_ title: String, _ value: Double?) -> some View {
|
|
||||||
HStack(spacing: 6) {
|
|
||||||
Text(title)
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Text(degreesText(value))
|
|
||||||
.font(.subheadline.monospacedDigit())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LevelActivityAttributes {
|
extension LevelActivityAttributes {
|
||||||
|
|||||||
Reference in New Issue
Block a user