Add Tonuino Manager website and Sparkle updates

This commit is contained in:
maddin committed 2026-10-31 16:40:48 +01:00
commit 1cac427ff2
44 files changed
+5861

No files matched your search

@@ -0,0 +1,244 @@
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<Int>, values: ClosedRange<Int>) -> 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) }
}
}
}