import SwiftUI struct TonuinoSettingsView: View { @EnvironmentObject private var app: AppModel @State private var currentPIN = "" @State private var newPIN = "" @State private var minVolume = 5 @State private var maxVolume = 25 @State private var initialVolume = 15 @State private var standbyOption = 5 @State private var equalizerOption = 1 @State private var pauseWhenRemoved = false @State private var invertButtons = false @State private var adminLockOption = 1 @State private var showResetConfirmation = false private let equalizers = ["Normal", "Pop", "Rock", "Jazz", "Klassik", "Bass"] var body: some View { Form { Section("Verbindung zum TonUINO") { HStack { if app.cards.serialConnected { Label(app.cards.firmwareVersion.map { "Verbunden · \($0)" } ?? "Verbunden · Firmwareversion nicht erkannt", systemImage: "checkmark.circle.fill") .foregroundStyle(.green) Spacer() if app.cards.serialAdminMenuActive { Button("Admin-Menü beenden", systemImage: "stop.circle") { do { try app.cards.exitTonuinoAdminMenu(); app.status = "TonUINO-Adminmenü beendet." } catch { app.report(error) } } } Button("Trennen") { app.cards.disconnectSerial() } } else { Picker("Serieller Port", selection: $app.cards.selectedSerialPort) { ForEach(app.cards.serialPorts, id: \.self) { Text($0).tag($0) } } Button("Suchen", systemImage: "arrow.clockwise") { app.cards.refreshSerialPorts() } Button("Verbinden") { Task { do { try await app.cards.connectSerial() } catch { app.report(error) } } } .disabled(app.cards.selectedSerialPort.isEmpty) } } Text("USB-Menüsteuerung benötigt eine TonUINO-TNG-Firmware mit aktiviertem SerialInputAsCommand. Die Firmware muss im Leerlauf sein. Nach der ersten Änderung bleibt das Admin-Menü für weitere Einstellungen geöffnet; beende es danach hier.") .font(.caption).foregroundStyle(.secondary) } Section("Ausgelesene Geräteeinstellungen") { if let settings = app.cards.tonuinoSettings { Text("Beim Verbinden aus der Startmeldung gelesen. Änderungen, die du danach über das Sprachmenü vornimmst, erscheinen nach erneutem Verbinden.") .font(.caption).foregroundStyle(.secondary) if let version = settings.firmwareSettingsVersion { LabeledContent("Einstellungsformat", value: String(version)) } settingsRow("Lautsprecher · Maximum", settings.speakerMaximum) settingsRow("Lautsprecher · Minimum", settings.speakerMinimum) settingsRow("Lautsprecher · Startlautstärke", settings.speakerInitial) settingsRow("Kopfhörer · Maximum", settings.headphoneMaximum) settingsRow("Kopfhörer · Minimum", settings.headphoneMinimum) settingsRow("Kopfhörer · Startlautstärke", settings.headphoneInitial) settingsRow("Equalizer (Firmwarewert)", settings.equalizer) settingsRow("Abschalt-Timer (Firmwarewert)", settings.standbyTimer) settingsRow("Lautstärketasten vertauscht (0/1)", settings.invertedVolumeButtons) settingsRow("Admin-Menü gesperrt (0/1)", settings.adminMenuLocked) settingsRow("Pause bei Kartenentfernung (0/1)", settings.pauseWhenCardRemoved) } else if app.cards.serialConnected { Text("Diese Firmware hat beim Verbinden keine auslesbaren Einstellungswerte ausgegeben. Das ist bei älteren AiO-Versionen möglich. Die Verbindung und die vorhandenen Menüsteuerungen bleiben davon unberührt.") .font(.caption).foregroundStyle(.secondary) } else { Text("Verbinde den TonUINO, um die von der Firmware ausgegebenen Werte zu lesen.") .font(.caption).foregroundStyle(.secondary) } } Section("Lautstärke") { Text("Wähle die drei Lautstärkewerte aus. Auslesbare aktuelle Werte erscheinen im Abschnitt oben.") .font(.caption).foregroundStyle(.secondary) HStack { volumePicker("Minimum", selection: $minVolume, values: 1...29) volumePicker("Maximum", selection: $maxVolume, values: 2...30) volumePicker("Startlautstärke", selection: $initialVolume, values: 1...30) } if !validVolume { Text("Das Minimum muss kleiner als das Maximum sein, die Startlautstärke muss dazwischen liegen.") .font(.caption).foregroundStyle(.red) } Button("Lautstärke übernehmen", systemImage: "speaker.wave.2") { applyVolume() } .disabled(!app.cards.serialConnected || app.cards.configuringSettings || !validVolume) } Section("Admin-Zugang") { SecureField("Aktuelle Admin-PIN (optional)", text: $currentPIN) .frame(maxWidth: 280) Text("Lass das Feld leer, wenn das Admin-Menü nicht geschützt ist. Eine vorhandene Firmware-PIN besteht aus vier Tastenwerten 1, 2 oder 3.") .font(.caption).foregroundStyle(.secondary) } Section("Abschalt-Timer") { Picker("TonUINO nach Inaktivität ausschalten", selection: $standbyOption) { Text("5 Minuten").tag(1) Text("15 Minuten").tag(2) Text("30 Minuten").tag(3) Text("60 Minuten").tag(4) Text("Nie").tag(5) } HStack { Button("Timer übernehmen", systemImage: "timer") { apply(menuItem: 8, option: standbyOption, message: "Abschalt-Timer an TonUINO übermittelt.") } .disabled(!app.cards.serialConnected || app.cards.configuringSettings) Spacer() settingProgress } } Section("Wiedergabe") { Picker("Equalizer", selection: $equalizerOption) { ForEach(equalizers.indices, id: \.self) { index in Text(equalizers[index]).tag(index + 1) } } HStack { Button("Equalizer übernehmen", systemImage: "slider.horizontal.3") { apply(menuItem: 5, option: equalizerOption, message: "Equalizer an TonUINO übermittelt.") } .disabled(!app.cards.serialConnected || app.cards.configuringSettings) Spacer() settingProgress } Toggle("Bei Entfernen der Karte pausieren", isOn: $pauseWhenRemoved) Button("Wiedergabeoption übernehmen", systemImage: "playpause") { apply(menuItem: 13, option: pauseWhenRemoved ? 2 : 1, message: "Verhalten beim Entfernen der Karte an TonUINO übermittelt.") } .disabled(!app.cards.serialConnected || app.cards.configuringSettings) } Section("Tasten") { Toggle("Lautstärketasten vertauschen", isOn: $invertButtons) Button("Tastenoption übernehmen", systemImage: "arrow.up.arrow.down") { apply(menuItem: 10, option: invertButtons ? 2 : 1, message: "Tastenoption an TonUINO übermittelt.") } .disabled(!app.cards.serialConnected || app.cards.configuringSettings) } Section("Admin-Menü") { Picker("Zugangsschutz", selection: $adminLockOption) { Text("Entsperrt").tag(1) Text("Gesperrt, ohne PIN").tag(2) Text("Gesperrt, mit neuer PIN").tag(3) } if adminLockOption == 3 { SecureField("Neue 4-stellige PIN (nur 1, 2, 3)", text: $newPIN) .frame(maxWidth: 280) } Button("Zugangsschutz übernehmen", systemImage: "lock") { apply(menuItem: 12, option: adminLockOption, message: "Zugangsschutz an TonUINO übermittelt.", newPIN: adminLockOption == 3 ? newPIN : "") } .disabled(!app.cards.serialConnected || app.cards.configuringSettings || (adminLockOption == 3 && (newPIN.count != 4 || !newPIN.allSatisfy({ ["1", "2", "3"].contains(String($0)) })))) } Section("Zurücksetzen") { Text("Setzt die in der TonUINO-Firmware gespeicherten Einstellungen auf Werkseinstellungen zurück.") .font(.caption).foregroundStyle(.secondary) Button("TonUINO-Einstellungen zurücksetzen…", systemImage: "arrow.counterclockwise", role: .destructive) { showResetConfirmation = true } .disabled(!app.cards.serialConnected || app.cards.configuringSettings) } Section { Text("Die App steuert Änderungen über das Admin-Sprachmenü der Firmware. Folge den TonUINO-Audioansagen und achte auf die Bestätigung. Das automatische Auslesen hängt davon ab, ob die Firmware ihre Einstellungen beim Start über USB meldet.") .font(.caption).foregroundStyle(.secondary) } } .formStyle(.grouped) .padding(.top, 10) .navigationTitle("TonUINO-Einstellungen") .onAppear { app.cards.refreshSerialPorts() } .confirmationDialog("TonUINO-Einstellungen zurücksetzen?", isPresented: $showResetConfirmation, titleVisibility: .visible) { Button("Einstellungen zurücksetzen", role: .destructive) { Task { do { try await app.cards.resetTonuinoSettings(currentPIN: currentPIN) app.status = "TonUINO-Einstellungen wurden zurückgesetzt." } catch { app.report(error) } } } Button("Abbrechen", role: .cancel) { } } message: { Text("Alle TonUINO-Einstellungen im EEPROM werden gelöscht. Die SD-Karte und NFC-Karten bleiben unverändert.") } .overlay { if app.cards.configuringSettings { ProgressView("Einstellungen werden übertragen…") .padding(18) .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12)) } } } private var settingProgress: some View { Group { if app.cards.configuringSettings { ProgressView().controlSize(.small) } } } private var validVolume: Bool { return minVolume < maxVolume && (minVolume...maxVolume).contains(initialVolume) } private func volumePicker(_ title: String, selection: Binding, values: ClosedRange) -> some View { Picker(title, selection: selection) { ForEach(values, id: \.self) { value in Text(String(value)).tag(value) } } .frame(maxWidth: 180) } @ViewBuilder private func settingsRow(_ title: String, _ value: Int?) -> some View { if let value { LabeledContent(title, value: String(value)) } } private func applyVolume() { Task { do { try await app.cards.applyTonuinoVolume(minimum: minVolume, maximum: maxVolume, initial: initialVolume, currentPIN: currentPIN) app.status = "Lautstärkeeinstellungen an TonUINO übermittelt." } catch { app.report(error) } } } private func apply(menuItem: Int, option: Int, message: String, newPIN: String = "") { Task { do { try await app.cards.applyTonuinoSetting(menuItem: menuItem, option: option, currentPIN: currentPIN, newPIN: newPIN) if menuItem == 12 { currentPIN = option == 3 ? newPIN : "" } app.status = message } catch { app.report(error) } } } }