Add Tonuino Manager website and Sparkle updates
This commit is contained in:
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) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user