Ersetzt die zehn wählbaren Symbole durch eine kürzere, konsistentere Auswahl (car, car.side, suv.side, truck.pickup.side, box.truck, bus) und setzt suv.side als neuen Standard für neu angelegte Fahrzeuge. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
233 lines
9.1 KiB
Swift
233 lines
9.1 KiB
Swift
import SwiftUI
|
||
|
||
/// Fahrzeuge anlegen, umbenennen und entfernen.
|
||
struct ProfilesView: View {
|
||
@Environment(DeviceStore.self) private var store
|
||
@Environment(BluetoothManager.self) private var bluetooth
|
||
@Environment(\.dismiss) private var dismiss
|
||
|
||
@State private var editing: Profile?
|
||
@State private var pendingDeletion: Profile?
|
||
|
||
var body: some View {
|
||
NavigationStack {
|
||
List {
|
||
Section {
|
||
ForEach(store.profiles) { profile in
|
||
row(for: profile)
|
||
}
|
||
} footer: {
|
||
Text("Antippen wählt das Fahrzeug aus, das ⓘ öffnet Name, Symbol "
|
||
+ "und Maße. Jedes Fahrzeug hat seine eigenen Geräte, und die "
|
||
+ "App liest immer nur die des gewählten aus.")
|
||
}
|
||
|
||
Section {
|
||
Button("Fahrzeug hinzufügen", systemImage: "plus") {
|
||
let profile = store.addProfile(named: "Camper \(store.profiles.count + 1)")
|
||
editing = profile
|
||
}
|
||
}
|
||
}
|
||
.navigationTitle("Fahrzeuge")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar {
|
||
ToolbarItem(placement: .confirmationAction) {
|
||
Button("Fertig") { dismiss() }
|
||
}
|
||
}
|
||
.sheet(item: $editing) { profile in
|
||
ProfileEditView(profile: profile)
|
||
}
|
||
.confirmationDialog("Fahrzeug entfernen?",
|
||
isPresented: .init(get: { pendingDeletion != nil },
|
||
set: { if !$0 { pendingDeletion = nil } }),
|
||
titleVisibility: .visible) {
|
||
Button("Entfernen", role: .destructive) {
|
||
if let pendingDeletion {
|
||
store.removeProfile(pendingDeletion)
|
||
bluetooth.refreshConfiguration()
|
||
}
|
||
pendingDeletion = nil
|
||
}
|
||
} message: {
|
||
if let pendingDeletion {
|
||
Text("„\(pendingDeletion.name)“ und die \(store.deviceCount(in: pendingDeletion)) "
|
||
+ "darin eingerichteten Geräte werden gelöscht.")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private func row(for profile: Profile) -> some View {
|
||
HStack(spacing: 12) {
|
||
Label {
|
||
VStack(alignment: .leading, spacing: 2) {
|
||
Text(profile.name)
|
||
Text(summary(for: profile))
|
||
.font(.caption)
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
} icon: {
|
||
Image(systemName: profile.symbol)
|
||
}
|
||
// Nur dieser Bereich wählt aus; der Knopf rechts bleibt frei.
|
||
.contentShape(.rect)
|
||
.onTapGesture {
|
||
store.selectProfile(profile)
|
||
bluetooth.refreshConfiguration()
|
||
}
|
||
|
||
Spacer(minLength: 0)
|
||
|
||
if profile.id == store.activeProfileID {
|
||
Image(systemName: "checkmark")
|
||
.foregroundStyle(.tint)
|
||
.fontWeight(.semibold)
|
||
}
|
||
|
||
// Zum Bearbeiten reichte bisher nur das Wischen - das findet
|
||
// niemand, erst recht nicht für die Fahrzeugmaße.
|
||
Button {
|
||
editing = profile
|
||
} label: {
|
||
Image(systemName: "info.circle")
|
||
.font(.title3)
|
||
}
|
||
.buttonStyle(.borderless)
|
||
.accessibilityLabel("\(profile.name) bearbeiten")
|
||
}
|
||
.swipeActions(edge: .trailing) {
|
||
// Das letzte Fahrzeug muss bleiben, sonst hätten Geräte keinen Ort.
|
||
if store.hasMultipleProfiles {
|
||
Button("Entfernen", systemImage: "trash", role: .destructive) {
|
||
pendingDeletion = profile
|
||
}
|
||
}
|
||
Button("Bearbeiten", systemImage: "pencil") {
|
||
editing = profile
|
||
}
|
||
.tint(.gray)
|
||
}
|
||
}
|
||
|
||
private func summary(for profile: Profile) -> String {
|
||
let count = store.deviceCount(in: profile)
|
||
var parts = [count == 1 ? "1 Gerät" : "\(count) Geräte"]
|
||
if profile.trackWidth != nil || profile.wheelbase != nil {
|
||
parts.append("Maße hinterlegt")
|
||
}
|
||
return parts.joined(separator: " · ")
|
||
}
|
||
}
|
||
|
||
/// Name, Symbol und Maße eines Fahrzeugs ändern.
|
||
private struct ProfileEditView: View {
|
||
|
||
/// Nimmt Komma wie Punkt an – auf einer deutschen Tastatur liegt das Komma
|
||
/// näher. Unplausible Werte werden verworfen, damit die Keilhöhe nicht
|
||
/// stillschweigend Unsinn ergibt.
|
||
static func metres(from text: String) -> Double? {
|
||
let cleaned = text.replacingOccurrences(of: ",", with: ".")
|
||
.trimmingCharacters(in: .whitespaces)
|
||
guard let value = Double(cleaned), value > 0.5, value < 12 else { return nil }
|
||
return value
|
||
}
|
||
|
||
static func text(from value: Double?) -> String {
|
||
guard let value else { return "" }
|
||
return String(format: "%.2f", value).replacingOccurrences(of: ".", with: ",")
|
||
}
|
||
|
||
let profile: Profile
|
||
|
||
@Environment(DeviceStore.self) private var store
|
||
@Environment(\.dismiss) private var dismiss
|
||
|
||
@State private var name = ""
|
||
@State private var symbol = "suv.side"
|
||
@State private var trackWidth = ""
|
||
@State private var wheelbase = ""
|
||
|
||
private let columns = [GridItem(.adaptive(minimum: 60), spacing: 12)]
|
||
|
||
var body: some View {
|
||
NavigationStack {
|
||
Form {
|
||
Section("Name") {
|
||
TextField("Name", text: $name)
|
||
}
|
||
Section {
|
||
LabeledContent("Spurweite") {
|
||
HStack {
|
||
TextField("z. B. 2,00", text: $trackWidth)
|
||
.keyboardType(.decimalPad)
|
||
.multilineTextAlignment(.trailing)
|
||
Text("m").foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
LabeledContent("Radstand") {
|
||
HStack {
|
||
TextField("z. B. 3,50", text: $wheelbase)
|
||
.keyboardType(.decimalPad)
|
||
.multilineTextAlignment(.trailing)
|
||
Text("m").foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
} header: {
|
||
Text("Fahrzeugmaße")
|
||
} footer: {
|
||
Text("Nur für den Ausrichtungs-Assistenten: Aus Neigung und "
|
||
+ "Abstand errechnet er, wie hoch der Auffahrkeil sein muss. "
|
||
+ "Ohne Angabe entfällt diese Anzeige.")
|
||
}
|
||
|
||
Section("Symbol") {
|
||
LazyVGrid(columns: columns, spacing: 12) {
|
||
ForEach(Profile.symbols, id: \.self) { candidate in
|
||
Button {
|
||
symbol = candidate
|
||
} label: {
|
||
Image(systemName: candidate)
|
||
.font(.title2)
|
||
.frame(width: 52, height: 52)
|
||
.background(symbol == candidate ? Color.accentColor.opacity(0.18)
|
||
: Color.secondary.opacity(0.08),
|
||
in: .rect(cornerRadius: 12))
|
||
.foregroundStyle(symbol == candidate ? Color.accentColor : Color.primary)
|
||
}
|
||
.buttonStyle(.plain)
|
||
}
|
||
}
|
||
.padding(.vertical, 4)
|
||
}
|
||
}
|
||
.navigationTitle("Fahrzeug")
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar {
|
||
ToolbarItem(placement: .cancellationAction) {
|
||
Button("Abbrechen") { dismiss() }
|
||
}
|
||
ToolbarItem(placement: .confirmationAction) {
|
||
Button("Sichern") {
|
||
var updated = profile
|
||
updated.name = name.trimmingCharacters(in: .whitespaces)
|
||
updated.symbol = symbol
|
||
updated.trackWidth = Self.metres(from: trackWidth)
|
||
updated.wheelbase = Self.metres(from: wheelbase)
|
||
store.update(updated)
|
||
dismiss()
|
||
}
|
||
.disabled(name.trimmingCharacters(in: .whitespaces).isEmpty)
|
||
}
|
||
}
|
||
.onAppear {
|
||
name = profile.name
|
||
symbol = profile.symbol
|
||
trackWidth = Self.text(from: profile.trackWidth)
|
||
wheelbase = Self.text(from: profile.wheelbase)
|
||
}
|
||
}
|
||
}
|
||
}
|