.help(...)-Tooltips auf jedem Eingabefeld/Picker/Toggle in Connect- sowie allen Einrichten-Schritten (WAN/LAN/VLAN/WLAN/Firewall) -- kurze Erklärung was der Wert bedeutet und welche Auswirkung er hat, passend zum Laien-Anspruch der App. Sicherungen-Tab: Speicherort jetzt änderbar (Ordner wählen über nativen macOS-Dialog, Zurücksetzen auf Standard). BackupService hält den gewählten Pfad in UserDefaults (BackupService.customDirectoryURL), fällt ohne Auswahl weiter auf ~/Library/Application Support/... zurück. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HReLXMbmPvtQ23p1iWiJNW
131 lines
4.9 KiB
Swift
131 lines
4.9 KiB
Swift
import SwiftUI
|
|
|
|
struct ConnectView: View {
|
|
@ObservedObject private var connectionService: ConnectionService
|
|
@StateObject private var viewModel: ConnectViewModel
|
|
|
|
init(connectionService: ConnectionService) {
|
|
self.connectionService = connectionService
|
|
_viewModel = StateObject(wrappedValue: ConnectViewModel(connectionService: connectionService))
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationSplitView {
|
|
Form {
|
|
Section("Verbindung") {
|
|
TextField("IP-Adresse oder Hostname", text: $viewModel.host)
|
|
.help("Die Adresse deines Routers im Netzwerk. Werkseinstellung bei Mikrotik ist meist 192.168.88.1.")
|
|
TextField("Benutzername", text: $viewModel.username)
|
|
.help("Der Admin-Benutzername deines Routers. Werkseinstellung ist meist \"admin\".")
|
|
SecureField("Passwort", text: $viewModel.password)
|
|
.help("Das Passwort für diesen Benutzer. Bei unverändertem Werkszustand oft leer.")
|
|
Toggle("Passwort merken", isOn: $viewModel.rememberPassword)
|
|
.help("Speichert das Passwort verschlüsselt in der macOS-Schlüsselbundverwaltung, damit du es nicht jedes Mal neu eingeben musst.")
|
|
}
|
|
|
|
Section {
|
|
Button("Verbinden") {
|
|
viewModel.connect()
|
|
}
|
|
.disabled(viewModel.host.isEmpty || viewModel.username.isEmpty)
|
|
}
|
|
|
|
statusSection
|
|
}
|
|
.formStyle(.grouped)
|
|
.frame(minWidth: 320)
|
|
} detail: {
|
|
deviceDetail
|
|
}
|
|
.onAppear { viewModel.onAppear() }
|
|
.alert(
|
|
"Unbekanntes Zertifikat",
|
|
isPresented: certificateAlertBinding,
|
|
presenting: certificateFingerprint
|
|
) { fingerprint in
|
|
Button("Vertrauen und verbinden") {
|
|
viewModel.trustAndRetry(fingerprint: fingerprint)
|
|
}
|
|
Button("Abbrechen", role: .cancel) {}
|
|
} message: { fingerprint in
|
|
Text("Der Router hat sich mit einem unbekannten Zertifikat gemeldet.\nFingerabdruck: \(fingerprint)\n\nNur bestätigen, wenn dies dein eigenes Gerät im lokalen Netzwerk ist.")
|
|
}
|
|
}
|
|
|
|
private var certificateFingerprint: String? {
|
|
if case .needsCertificateConfirmation(let fingerprint) = connectionService.state {
|
|
return fingerprint
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private var certificateAlertBinding: Binding<Bool> {
|
|
Binding(
|
|
get: { certificateFingerprint != nil },
|
|
set: { _ in }
|
|
)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var statusSection: some View {
|
|
switch connectionService.state {
|
|
case .idle, .needsCertificateConfirmation:
|
|
EmptyView()
|
|
case .connecting:
|
|
Section {
|
|
HStack {
|
|
ProgressView()
|
|
Text("Verbinde…")
|
|
}
|
|
}
|
|
case .connected(let kind):
|
|
Section {
|
|
Label("Verbunden (\(kind == .rest ? "REST-API" : "SSH"))", systemImage: "checkmark.circle.fill")
|
|
.foregroundStyle(.green)
|
|
}
|
|
case .failed(let message):
|
|
Section {
|
|
Label(message, systemImage: "exclamationmark.triangle.fill")
|
|
.foregroundStyle(.red)
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var deviceDetail: some View {
|
|
if let info = connectionService.deviceInfo {
|
|
List {
|
|
Section("Gerät") {
|
|
LabeledContent("Modell", value: info.boardName)
|
|
LabeledContent("RouterOS-Version", value: info.routerOSVersion)
|
|
LabeledContent("Architektur", value: info.architecture)
|
|
LabeledContent("Laufzeit", value: info.uptime)
|
|
}
|
|
Section("Interfaces") {
|
|
ForEach(connectionService.interfaces) { interface in
|
|
HStack {
|
|
Image(systemName: interface.running ? "circle.fill" : "circle")
|
|
.foregroundStyle(interface.running ? .green : .secondary)
|
|
.font(.caption)
|
|
VStack(alignment: .leading) {
|
|
Text(interface.name).bold()
|
|
Text(interface.type).font(.caption).foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
ContentUnavailableView(
|
|
"Nicht verbunden",
|
|
systemImage: "network.slash",
|
|
description: Text("Verbinde dich mit deinem Router, um Geräteinformationen zu sehen.")
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
ConnectView(connectionService: ConnectionService())
|
|
}
|