Add landscape layouts for level display, vehicle view, and alignment assistant
Rotating the phone into landscape while leveling used to leave a portrait-shaped column of controls that needed scrolling to take in. All three level-integration screens now branch on verticalSizeClass: the bubble/vehicle status in the device screen and the full alignment assistant (advice, readings, and wedge heights) lay out side-by-side instead of stacked, sized to fit without scrolling. The device screen also measures its actual available height via a GeometryReader and feeds it to the level display so the bubble/vehicle graphic scales to fill the landscape screen rather than being capped at a fixed size, and the connection-status section moves to the bottom of the list in landscape so it doesn't compete with the display for space. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
21f5d8de62
commit
8d975d2c4d
@@ -105,47 +105,76 @@ struct LevelBubble: View {
|
||||
struct LevelControls: View {
|
||||
let device: ConfiguredDevice
|
||||
let state: LevelState
|
||||
/// Höhe der umgebenden Liste, von `DeviceDetailView` per `GeometryReader`
|
||||
/// gemessen. Nur im Querformat gebraucht, um die Anzeige auf
|
||||
/// Bildschirmhöhe zu bringen statt sie auf einen festen Wert zu kappen.
|
||||
var availableHeight: CGFloat?
|
||||
|
||||
@Environment(BluetoothManager.self) private var bluetooth
|
||||
@Environment(DeviceStore.self) private var store
|
||||
@Environment(LevelActivityManager.self) private var levelActivity
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
@State private var showAssistant = false
|
||||
@AppStorage("levelDisplayStyle") private var displayStyle: LevelDisplayStyle = .bubble
|
||||
|
||||
/// iPhone im Querformat meldet eine kompakte Höhe – das ist das
|
||||
/// zuverlässige Signal dafür, nicht die Geräteausrichtung selbst.
|
||||
private var isLandscape: Bool { verticalSizeClass == .compact }
|
||||
|
||||
/// Verfügbare Höhe abzüglich grober Reserve für Listenränder und die
|
||||
/// eigene vertikale Auffüllung – genug, um praktisch den ganzen
|
||||
/// Bildschirm zu nutzen, ohne über den unteren Rand hinauszuschiessen.
|
||||
private var landscapeContentHeight: CGFloat {
|
||||
max(150, (availableHeight ?? 350) - 56)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
VStack(spacing: 16) {
|
||||
Picker("Darstellung", selection: $displayStyle) {
|
||||
ForEach(LevelDisplayStyle.allCases) { style in
|
||||
Text(style.title).tag(style)
|
||||
if isLandscape {
|
||||
HStack(alignment: .center, spacing: 20) {
|
||||
display
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
VStack(spacing: 12) {
|
||||
picker
|
||||
if let instruction = state.instruction {
|
||||
Label(instruction,
|
||||
systemImage: state.isLevel ? "checkmark.circle.fill" : "arrow.up.circle")
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(state.isLevel ? Color.green : Color.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
HStack(spacing: 16) {
|
||||
reading("Längs", LevelDirectionFormatting.pitchTile(state.pitch))
|
||||
reading("Quer", LevelDirectionFormatting.rollTile(state.roll))
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.frame(height: landscapeContentHeight)
|
||||
.padding(.vertical, 4)
|
||||
} else {
|
||||
VStack(spacing: 16) {
|
||||
picker
|
||||
|
||||
display
|
||||
|
||||
if let instruction = state.instruction {
|
||||
Label(instruction,
|
||||
systemImage: state.isLevel ? "checkmark.circle.fill" : "arrow.up.circle")
|
||||
.font(.headline)
|
||||
.foregroundStyle(state.isLevel ? Color.green : Color.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
HStack(spacing: 24) {
|
||||
reading("Längs", LevelDirectionFormatting.pitchTile(state.pitch))
|
||||
reading("Quer", LevelDirectionFormatting.rollTile(state.roll))
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
|
||||
switch displayStyle {
|
||||
case .bubble:
|
||||
LevelBubble(pitch: state.pitch, roll: state.roll)
|
||||
.frame(maxHeight: 220)
|
||||
case .vehicle:
|
||||
VehicleTiltView(pitch: state.pitch, roll: state.roll,
|
||||
style: device.vehicleGraphicStyle)
|
||||
}
|
||||
|
||||
if let instruction = state.instruction {
|
||||
Label(instruction,
|
||||
systemImage: state.isLevel ? "checkmark.circle.fill" : "arrow.up.circle")
|
||||
.font(.headline)
|
||||
.foregroundStyle(state.isLevel ? Color.green : Color.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
|
||||
HStack(spacing: 24) {
|
||||
reading("Längs", LevelDirectionFormatting.pitchTile(state.pitch))
|
||||
reading("Quer", LevelDirectionFormatting.rollTile(state.roll))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
Section {
|
||||
@@ -220,6 +249,28 @@ struct LevelControls: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var picker: some View {
|
||||
Picker("Darstellung", selection: $displayStyle) {
|
||||
ForEach(LevelDisplayStyle.allCases) { style in
|
||||
Text(style.title).tag(style)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var display: some View {
|
||||
switch displayStyle {
|
||||
case .bubble:
|
||||
LevelBubble(pitch: state.pitch, roll: state.roll)
|
||||
.frame(maxHeight: isLandscape ? landscapeContentHeight : 220)
|
||||
case .vehicle:
|
||||
VehicleTiltView(pitch: state.pitch, roll: state.roll,
|
||||
style: device.vehicleGraphicStyle, compact: isLandscape,
|
||||
compactPanelHeight: max(60, landscapeContentHeight - 40))
|
||||
}
|
||||
}
|
||||
|
||||
private var isLive: Bool { bluetooth.linkStates[device.id] == .live }
|
||||
|
||||
private var liveActivityBinding: Binding<Bool> {
|
||||
|
||||
Reference in New Issue
Block a user