Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46a4da5e37 | ||
|
|
45aa7cf984 |
|
After Width: | Height: | Size: 341 KiB |
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "CaliforniaSide.png",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 },
|
||||||
|
"properties" : { "template-rendering-intent" : "template" }
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "VehicleRear.png",
|
"filename" : "VansterRear.png",
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "VehicleSide.png",
|
"filename" : "VansterSide.png",
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
Before Width: | Height: | Size: 253 KiB After Width: | Height: | Size: 253 KiB |
@@ -5,9 +5,11 @@ import Observation
|
|||||||
/// Startet, aktualisiert und beendet die Live Activity des Neigungsmessers.
|
/// Startet, aktualisiert und beendet die Live Activity des Neigungsmessers.
|
||||||
///
|
///
|
||||||
/// Es läuft höchstens eine Aktivität gleichzeitig – mehr als einen
|
/// Es läuft höchstens eine Aktivität gleichzeitig – mehr als einen
|
||||||
/// Neigungsmesser gibt es im aktiven Profil ohnehin nicht. Seit iOS 17 zeigt
|
/// Neigungsmesser gibt es im aktiven Profil ohnehin nicht. Seit iOS 26 zeigt
|
||||||
/// CarPlay eine laufende Live Activity automatisch im Dashboard an; ein
|
/// CarPlay eine laufende Live Activity automatisch im Dashboard an; ein
|
||||||
/// eigenes CarPlay-App-Target braucht es dafür nicht.
|
/// eigenes CarPlay-App-Target braucht es dafür nicht – die Inhalte kommen
|
||||||
|
/// aber nicht von der Sperrbildschirm-Ansicht, sondern von der
|
||||||
|
/// `.small`-Aktivitätsfamilie (siehe `VanAligneiOSWidgetLiveActivity`).
|
||||||
@Observable
|
@Observable
|
||||||
final class LevelActivityManager {
|
final class LevelActivityManager {
|
||||||
private(set) var trackedDeviceID: UUID?
|
private(set) var trackedDeviceID: UUID?
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ struct AlignmentAssistantView: View {
|
|||||||
LevelBubble(pitch: state.pitch, roll: state.roll)
|
LevelBubble(pitch: state.pitch, roll: state.roll)
|
||||||
.frame(maxWidth: 320)
|
.frame(maxWidth: 320)
|
||||||
case .vehicle:
|
case .vehicle:
|
||||||
VehicleTiltView(pitch: state.pitch, roll: state.roll)
|
VehicleTiltView(pitch: state.pitch, roll: state.roll,
|
||||||
|
style: device.vehicleGraphicStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isLive { disconnectedBanner }
|
if !isLive { disconnectedBanner }
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ struct LevelSetupView: View {
|
|||||||
store.devices.first { $0.id == device.id } ?? device
|
store.devices.first { $0.id == device.id } ?? device
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let graphicColumns = [GridItem(.adaptive(minimum: 76), spacing: 12)]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
List {
|
||||||
Section {
|
Section {
|
||||||
@@ -80,6 +82,44 @@ struct LevelSetupView: View {
|
|||||||
} footer: {
|
} footer: {
|
||||||
Text(calibrationHint)
|
Text(calibrationHint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
LazyVGrid(columns: graphicColumns, spacing: 12) {
|
||||||
|
ForEach(VehicleGraphicStyle.allCases) { style in
|
||||||
|
Button {
|
||||||
|
var updated = currentDevice
|
||||||
|
updated.vehicleGraphicStyle = style
|
||||||
|
store.update(updated)
|
||||||
|
} label: {
|
||||||
|
VStack(spacing: 4) {
|
||||||
|
Image(style.sideImageName)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.padding(10)
|
||||||
|
.frame(width: 60, height: 60)
|
||||||
|
.background(currentDevice.vehicleGraphicStyle == style
|
||||||
|
? Color.accentColor.opacity(0.18)
|
||||||
|
: Color.secondary.opacity(0.08),
|
||||||
|
in: .rect(cornerRadius: 12))
|
||||||
|
.foregroundStyle(currentDevice.vehicleGraphicStyle == style
|
||||||
|
? Color.accentColor : Color.primary)
|
||||||
|
Text(style.title)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(currentDevice.vehicleGraphicStyle == style
|
||||||
|
? Color.accentColor : Color.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.accessibilityLabel(style.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
} header: {
|
||||||
|
Text("Fahrzeuggrafik")
|
||||||
|
} footer: {
|
||||||
|
Text("Wird in der Fahrzeug-Darstellung des Neigungsmessers verwendet. "
|
||||||
|
+ "Weitere Grafiken folgen.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Neigungsmesser einrichten")
|
.navigationTitle("Neigungsmesser einrichten")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
|||||||
@@ -127,7 +127,8 @@ struct LevelControls: View {
|
|||||||
LevelBubble(pitch: state.pitch, roll: state.roll)
|
LevelBubble(pitch: state.pitch, roll: state.roll)
|
||||||
.frame(maxHeight: 220)
|
.frame(maxHeight: 220)
|
||||||
case .vehicle:
|
case .vehicle:
|
||||||
VehicleTiltView(pitch: state.pitch, roll: state.roll)
|
VehicleTiltView(pitch: state.pitch, roll: state.roll,
|
||||||
|
style: device.vehicleGraphicStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let instruction = state.instruction {
|
if let instruction = state.instruction {
|
||||||
|
|||||||
@@ -15,22 +15,23 @@ import SwiftUI
|
|||||||
struct VehicleTiltView: View {
|
struct VehicleTiltView: View {
|
||||||
let pitch: Double?
|
let pitch: Double?
|
||||||
let roll: Double?
|
let roll: Double?
|
||||||
|
var style: VehicleGraphicStyle = .vanster
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
tiltPanel(image: "VehicleSide",
|
tiltPanel(image: style.sideImageName,
|
||||||
angle: pitch,
|
angle: pitch,
|
||||||
title: "Längs",
|
title: "Längs",
|
||||||
lowerLabel: "Front",
|
lowerLabel: "Front",
|
||||||
upperLabel: "Heck",
|
upperLabel: "Heck",
|
||||||
aspect: VehicleTilt.sideAspect)
|
aspect: style.sideAspect)
|
||||||
|
|
||||||
tiltPanel(image: "VehicleRear",
|
tiltPanel(image: style.rearImageName,
|
||||||
angle: roll,
|
angle: roll,
|
||||||
title: "Quer",
|
title: "Quer",
|
||||||
lowerLabel: "links",
|
lowerLabel: "links",
|
||||||
upperLabel: "rechts",
|
upperLabel: "rechts",
|
||||||
aspect: VehicleTilt.rearAspect)
|
aspect: style.rearAspect)
|
||||||
|
|
||||||
// Ohne diesen Hinweis nähme man den Bildwinkel für den echten.
|
// Ohne diesen Hinweis nähme man den Bildwinkel für den echten.
|
||||||
Text(String(format: "Neigung %.0f-fach überhöht dargestellt – "
|
Text(String(format: "Neigung %.0f-fach überhöht dargestellt – "
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "VehicleRear.png",
|
"filename" : "VansterRear.png",
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"filename" : "VehicleSide.png",
|
"filename" : "VansterSide.png",
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
@@ -14,9 +14,9 @@ struct WatchVehicleView: View {
|
|||||||
/// Die angeschriebenen Gradzahlen bleiben echt.
|
/// Die angeschriebenen Gradzahlen bleiben echt.
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
panel(image: "VehicleSide", angle: pitch, title: "Längs",
|
panel(image: "VansterSide", angle: pitch, title: "Längs",
|
||||||
lower: "Front", upper: "Heck", aspect: VehicleTilt.sideAspect)
|
lower: "Front", upper: "Heck", aspect: VehicleTilt.sideAspect)
|
||||||
panel(image: "VehicleRear", angle: roll, title: "Quer",
|
panel(image: "VansterRear", angle: roll, title: "Quer",
|
||||||
lower: "links", upper: "rechts", aspect: VehicleTilt.rearAspect)
|
lower: "links", upper: "rechts", aspect: VehicleTilt.rearAspect)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ struct ConfiguredDevice: Identifiable, Codable, Hashable, Sendable {
|
|||||||
//var fridgeZoneMode: FridgeZoneMode = .automatic
|
//var fridgeZoneMode: FridgeZoneMode = .automatic
|
||||||
/// Nur für den Neigungsmesser: wie er im Fahrzeug sitzt.
|
/// Nur für den Neigungsmesser: wie er im Fahrzeug sitzt.
|
||||||
var sensorOrientation = SensorOrientation.identity
|
var sensorOrientation = SensorOrientation.identity
|
||||||
|
/// Nur für den Neigungsmesser: welche Fahrzeuggrafik die Fahrzeug-Ansicht zeigt.
|
||||||
|
var vehicleGraphicStyle = VehicleGraphicStyle.vanster
|
||||||
|
|
||||||
init(id: UUID = UUID(),
|
init(id: UUID = UUID(),
|
||||||
name: String,
|
name: String,
|
||||||
@@ -135,5 +137,7 @@ struct ConfiguredDevice: Identifiable, Codable, Hashable, Sendable {
|
|||||||
// ?? .automatic
|
// ?? .automatic
|
||||||
sensorOrientation = try container.decodeIfPresent(SensorOrientation.self,
|
sensorOrientation = try container.decodeIfPresent(SensorOrientation.self,
|
||||||
forKey: .sensorOrientation) ?? .identity
|
forKey: .sensorOrientation) ?? .identity
|
||||||
|
vehicleGraphicStyle = try container.decodeIfPresent(VehicleGraphicStyle.self,
|
||||||
|
forKey: .vehicleGraphicStyle) ?? .vanster
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Welche Silhouette die Fahrzeug-Ansicht des Neigungsmessers zeigt.
|
||||||
|
///
|
||||||
|
/// Jeder Fall ist ein zusammengehöriges Bilderset aus Seiten- und
|
||||||
|
/// Heckansicht in Assets.xcassets, beide als Vorlage ("template")
|
||||||
|
/// exportiert, damit `foregroundStyle` sie einfärben kann. Im Picker wird
|
||||||
|
/// nur die Seitenansicht gezeigt.
|
||||||
|
enum VehicleGraphicStyle: String, CaseIterable, Identifiable, Codable, Sendable {
|
||||||
|
case vanster
|
||||||
|
case california
|
||||||
|
|
||||||
|
var id: String { rawValue }
|
||||||
|
|
||||||
|
var title: String {
|
||||||
|
switch self {
|
||||||
|
case .vanster: return "Vanster"
|
||||||
|
case .california: return "California"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sideImageName: String {
|
||||||
|
switch self {
|
||||||
|
case .vanster: return "VansterSide"
|
||||||
|
case .california: return "CaliforniaSide"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Für California liegt noch keine eigene Heckansicht vor, deshalb
|
||||||
|
/// vorerst die von Vanster.
|
||||||
|
var rearImageName: String {
|
||||||
|
switch self {
|
||||||
|
case .vanster, .california: return "VansterRear"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Seitenverhältnis der jeweiligen Bilder – ohne das würde eine Grafik
|
||||||
|
/// mit anderen Proportionen in der gemeinsamen Bildhülle verzerrt oder
|
||||||
|
/// ungewollt verkleinert erscheinen.
|
||||||
|
var sideAspect: Double {
|
||||||
|
switch self {
|
||||||
|
case .vanster: return VehicleTilt.sideAspect
|
||||||
|
case .california: return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rearAspect: Double {
|
||||||
|
switch self {
|
||||||
|
case .vanster, .california: return VehicleTilt.rearAspect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,5 +32,11 @@ gefüllte Form, in der die Details erhalten bleiben.
|
|||||||
```bash
|
```bash
|
||||||
swiftc -O -o /tmp/make-vehicle-art Tools/make-vehicle-art.swift
|
swiftc -O -o /tmp/make-vehicle-art Tools/make-vehicle-art.swift
|
||||||
sips --resampleHeight 600 heckansicht.png --out /tmp/rear.png
|
sips --resampleHeight 600 heckansicht.png --out /tmp/rear.png
|
||||||
/tmp/make-vehicle-art /tmp/rear.png CamperMonitor/Assets.xcassets/VehicleRear.imageset/VehicleRear.png
|
/tmp/make-vehicle-art /tmp/rear.png CamperMonitor/Assets.xcassets/VansterRear.imageset/VansterRear.png
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Jedes Fahrzeug-Grafikset (siehe `VehicleGraphicStyle`) hat sein eigenes
|
||||||
|
Bildpaar `<Name>Side`/`<Name>Rear`. Hat die Vorlage schon einen
|
||||||
|
transparenten Hintergrund, geht die Quelldatei direkt ins Skript. Liegt sie
|
||||||
|
auf weißem Grund, muss der Rand vorher weg – sonst wird die ganze Fläche
|
||||||
|
mitgefärbt.
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ private struct LevelBubbleGlyph: View {
|
|||||||
struct VanAligneiOSWidgetLiveActivity: Widget {
|
struct VanAligneiOSWidgetLiveActivity: Widget {
|
||||||
var body: some WidgetConfiguration {
|
var body: some WidgetConfiguration {
|
||||||
ActivityConfiguration(for: LevelActivityAttributes.self) { context in
|
ActivityConfiguration(for: LevelActivityAttributes.self) { context in
|
||||||
LockScreenLevelView(attributes: context.attributes, state: context.state)
|
LevelActivityContent(attributes: context.attributes, state: context.state)
|
||||||
.activityBackgroundTint(Color(white: 0.08))
|
.activityBackgroundTint(Color(white: 0.08))
|
||||||
.activitySystemActionForegroundColor(.white)
|
.activitySystemActionForegroundColor(.white)
|
||||||
|
|
||||||
@@ -109,12 +109,60 @@ struct VanAligneiOSWidgetLiveActivity: Widget {
|
|||||||
}
|
}
|
||||||
.keylineTint(context.state.isLevel ? .green : .orange)
|
.keylineTint(context.state.isLevel ? .green : .orange)
|
||||||
}
|
}
|
||||||
|
.supplementalActivityFamilies([.small])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sperrbildschirm- und Banner-Ansicht. Dieselbe Ansicht landet automatisch
|
/// Wählt je nach Darstellungsgrösse zwischen Sperrbildschirm- und
|
||||||
/// auch im CarPlay-Dashboard, sobald das iPhone verbunden ist – ein eigenes
|
/// Kompaktansicht.
|
||||||
/// CarPlay-App-Target ist dafür nicht nötig.
|
///
|
||||||
|
/// Ohne `.small`-Familie fällt CarPlay (wie die Smart Stack der Uhr) auf
|
||||||
|
/// `compactLeading`/`compactTrailing` der Dynamic Island zurück – dort steht
|
||||||
|
/// nur das generische Symbol und, weil `compactTrailing` leer bleibt, gar
|
||||||
|
/// kein Text. Erst die `.small`-Familie liefert CarPlay Libelle und Text.
|
||||||
|
private struct LevelActivityContent: View {
|
||||||
|
let attributes: LevelActivityAttributes
|
||||||
|
let state: LevelActivityAttributes.ContentState
|
||||||
|
|
||||||
|
@Environment(\.activityFamily) private var activityFamily
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
switch activityFamily {
|
||||||
|
case .small:
|
||||||
|
CompactLevelView(attributes: attributes, state: state)
|
||||||
|
default:
|
||||||
|
LockScreenLevelView(attributes: attributes, state: state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kompaktform für CarPlay-Dashboard und Smart Stack der Uhr: Libelle plus
|
||||||
|
/// eine Textzeile, ohne die längere Zwei-Zeilen-Aufschlüsselung des
|
||||||
|
/// Sperrbildschirms.
|
||||||
|
private struct CompactLevelView: View {
|
||||||
|
let attributes: LevelActivityAttributes
|
||||||
|
let state: LevelActivityAttributes.ContentState
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
LevelBubbleGlyph(pitch: state.pitch, roll: state.roll, isLevel: state.isLevel, diameter: 40)
|
||||||
|
|
||||||
|
Text(state.instruction
|
||||||
|
?? (state.pitch == nil && state.roll == nil ? "Keine Messwerte" : attributes.deviceName))
|
||||||
|
.font(.callout.weight(.semibold))
|
||||||
|
.foregroundStyle(state.isLevel ? .green : .white)
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.8)
|
||||||
|
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sperrbildschirm- und Banner-Ansicht.
|
||||||
private struct LockScreenLevelView: View {
|
private struct LockScreenLevelView: View {
|
||||||
let attributes: LevelActivityAttributes
|
let attributes: LevelActivityAttributes
|
||||||
let state: LevelActivityAttributes.ContentState
|
let state: LevelActivityAttributes.ContentState
|
||||||
|
|||||||