Files
Tonuino-Manager/Sources/TonuinoManager/UI/SettingsView.swift
T

160 lines
8.5 KiB
Swift

import AppKit
import SwiftUI
import Sparkle
struct SettingsView: View {
@EnvironmentObject private var app: AppModel
@AppStorage("reader.route") private var defaultRoute = "tonuino"
@State private var acoustIDClientKey = ""
@State private var acoustIDKeyMessage: String?
@State private var profileStorageMessage: String?
var body: some View {
Form {
Section("Standardwerte") {
Picker("Bevorzugter Kartenleser", selection: $defaultRoute) {
Text("TonUINO über USB").tag("tonuino")
Text("ACR122U").tag("acr122u")
}
Toggle("Update-Prüfung beim Start", isOn: Binding(
get: { AppDelegate.updater?.updater.automaticallyChecksForUpdates ?? true },
set: { AppDelegate.updater?.updater.automaticallyChecksForUpdates = $0 }
))
}
Section("Hardware") {
LabeledContent("TonUINO-Verbindung", value: app.cards.serialConnected ? app.cards.selectedSerialPort : "Nicht verbunden")
LabeledContent("Verfügbare serielle Ports", value: "\(app.cards.serialPorts.count)")
LabeledContent("PC/SC Reader", value: "\(app.cards.readerNames.count)")
Button("Geräte erneut suchen", systemImage: "arrow.clockwise") { app.cards.refreshSerialPorts(); app.cards.refreshReaders() }
}
Section("Lokale Profile") {
VStack(alignment: .leading, spacing: 4) {
Text("Speicherort")
.font(.caption)
.foregroundStyle(.secondary)
Text(app.library.profileStorageURL.path)
.textSelection(.enabled)
.font(.system(.body, design: .monospaced))
.lineLimit(3)
}
if let error = app.library.profileStorageError {
Label("Profilspeicher nicht erreichbar", systemImage: "externaldrive.badge.exclamationmark")
.foregroundStyle(.orange)
Text("\(error) Die vorhandenen Profile bleiben gespeichert; verbinde das Laufwerk oder wähle einen anderen Speicherort.")
.font(.caption)
.foregroundStyle(.secondary)
}
HStack {
Button("Speicherort ändern…", systemImage: "folder") { chooseProfileStorageLocation() }
if app.library.isProfileStorageCustom {
Button("Standardort verwenden", systemImage: "arrow.uturn.backward") {
changeProfileStorage(to: nil)
}
}
}
.disabled(app.library.isSyncing)
if app.library.isSyncing {
ProgressView(app.library.syncProgress)
.font(.caption)
}
if let profileStorageMessage {
Text(profileStorageMessage)
.font(.caption)
.foregroundStyle(.secondary)
}
Text("Profile und ihre Audiodateien werden beim Wechsel vollständig kopiert. Ein externes Laufwerk muss beim Start verfügbar sein; nach dem erneuten Anschließen wird der Speicher automatisch neu eingelesen.")
.font(.caption)
.foregroundStyle(.secondary)
}
Section("Musikerkennung") {
SecureField("AcoustID-Anwendungsschlüssel", text: $acoustIDClientKey)
.textContentType(.password)
HStack {
Button("Schlüssel speichern", systemImage: "key") {
do {
try AcoustIDCredentials.saveClientKey(acoustIDClientKey)
acoustIDKeyMessage = acoustIDClientKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "Schlüssel entfernt." : "Schlüssel sicher im macOS-Schlüsselbund gespeichert."
} catch { acoustIDKeyMessage = error.localizedDescription }
}
if let acoustIDKeyMessage { Text(acoustIDKeyMessage).font(.caption).foregroundStyle(.secondary) }
}
Text("Registriere die App bei AcoustID und trage hier den Anwendungsschlüssel ein. Er wird im macOS-Schlüsselbund gespeichert.")
.font(.caption).foregroundStyle(.secondary)
Link("AcoustID-Anwendung registrieren", destination: URL(string: "https://acoustid.org/application")!)
}
Section("Updates") {
Text("Version \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unbekannt")")
.foregroundStyle(.secondary)
Button("Auf Updates prüfen", systemImage: "arrow.clockwise") { AppDelegate.updater?.checkForUpdates(nil) }
.disabled(AppDelegate.updater?.updater.canCheckForUpdates != true)
}
Section("Über die App") {
Text("Tonuino Manager für macOS")
Text("Verwaltet TonUINO Musik-SD-Karten und programmiert RFID/NFC-Karten.").foregroundStyle(.secondary)
Link("TonUINO-TNG Firmware", destination: URL(string: "https://github.com/tonuino/TonUINO-TNG")!)
}
Section("Credits & Lizenzen") {
VStack(alignment: .leading, spacing: 6) {
Text("Funktionales Vorbild")
.font(.caption.weight(.medium))
.foregroundStyle(.secondary)
Link("Tonuino Manager von donschoof", destination: URL(string: "https://github.com/donschoof/tonuino-manager")!)
Text("Diese App ist eine native Swift-Neuimplementierung und orientiert sich an diesem ursprünglichen Projekt.")
.font(.caption)
.foregroundStyle(.secondary)
}
VStack(alignment: .leading, spacing: 8) {
Text("Gebündelte Werkzeuge und Bibliotheken")
.font(.caption.weight(.medium))
.foregroundStyle(.secondary)
Link("FFmpeg · GPLv3-Build · Lizenzhinweise", destination: URL(string: "https://ffmpeg.org/legal.html")!)
Link("yt-dlp · Unlicense · Projekt und Lizenz", destination: URL(string: "https://github.com/yt-dlp/yt-dlp/blob/master/LICENSE")!)
Link("Deno · MIT · Projekt und Lizenz", destination: URL(string: "https://github.com/denoland/deno/blob/main/LICENSE.md")!)
Link("Chromaprint / fpcalc · LGPL-2.1 · Projekt und Lizenz", destination: URL(string: "https://github.com/acoustid/chromaprint/blob/master/LICENSE.md")!)
Link("Sparkle · MIT · Projekt und Lizenz", destination: URL(string: "https://github.com/sparkle-project/Sparkle/blob/2.x/LICENSE")!)
}
.font(.callout)
VStack(alignment: .leading, spacing: 4) {
Text("Apple-Systemframeworks")
.font(.caption.weight(.medium))
.foregroundStyle(.secondary)
Text("SwiftUI, AppKit, Foundation, AVFoundation, Combine, CryptoKit, CryptoTokenKit, MediaPlayer, Security und Darwin.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
.formStyle(.grouped)
.padding(.top, 10)
.navigationTitle("Einstellungen")
.onAppear { acoustIDClientKey = AcoustIDCredentials.loadClientKey() ?? "" }
}
private func chooseProfileStorageLocation() {
let panel = NSOpenPanel()
panel.title = "Speicherort für lokale Profile auswählen"
panel.prompt = "Diesen Ordner verwenden"
panel.canChooseFiles = false
panel.canChooseDirectories = true
panel.canCreateDirectories = true
panel.allowsMultipleSelection = false
if panel.runModal() == .OK, let url = panel.url {
changeProfileStorage(to: url)
}
}
private func changeProfileStorage(to url: URL?) {
profileStorageMessage = nil
Task {
do {
try await app.library.changeProfileStorageBase(to: url)
profileStorageMessage = "Speicherort geändert."
} catch {
profileStorageMessage = error.localizedDescription
}
}
}
}