Neues natives macOS-Settings-Fenster mit Sprache, Auto-Update-Check,
Farbschema (Standard/Kontrastreich), echter Textgrößen-Skalierung,
Bedienelement-Größe und LAN-Scanner-Refreshraten/Sparkline-Einstellungen.
- AppPreferences.swift: zentrale AppStorage-Keys, ColorTheme/AppTextSize/
UIDensity
- SettingsView.swift: 3-Tab-Settings-Scene
- .environment(\.dynamicTypeSize) erwies sich auf macOS als wirkungslos
(per ImageRenderer-Snapshot bewiesen) — durch eigenen appFontScale-
Mechanismus ersetzt, ~110 .font(...)-Aufrufe app-weit umgestellt
- Farbschema auf Wunsch auch auf Experte-Tab-Sidebar ausgeweitet
- Docs aktualisiert: README/HANDOFF/CHATLOG/Manual.md+PDF, found.md
Live bestätigt nach mehreren Nachbesserungsrunden ("passt, lassen wir so").
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
86 lines
5.1 KiB
Swift
86 lines
5.1 KiB
Swift
import SwiftUI
|
|
|
|
struct VlanStepView: View {
|
|
@ObservedObject var viewModel: SetupViewModel
|
|
let availableInterfaces: [NetworkInterface]
|
|
@AppStorage("appLanguage") private var appLanguage: String = "de"
|
|
|
|
var body: some View {
|
|
Form {
|
|
Section {
|
|
Toggle(
|
|
L10n.t("Zusätzliches Netzwerk einrichten (VLAN, optional)", appLanguage),
|
|
isOn: Binding(
|
|
get: { viewModel.vlanSectionEnabled },
|
|
set: { viewModel.setVlanSectionEnabled($0) }
|
|
)
|
|
)
|
|
.help(L10n.t("Erstellt ein zusätzliches Netzwerk mit eigenem Adressbereich und eigenem DHCP-Server.", appLanguage))
|
|
Text(L10n.t("Ein VLAN ist ein zusätzliches Netzwerk mit eigenem Adressbereich — z.B. für Gäste oder smarte Geräte. Ob es vom Hauptnetzwerk abgeschottet ist, legst du unten pro Netzwerk über \"Von anderen Netzwerken isolieren\" fest. Wenn du unsicher bist, ob du das brauchst, überspring diesen Schritt einfach.", appLanguage))
|
|
.appFont(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
if viewModel.vlanSectionEnabled {
|
|
ForEach($viewModel.vlans) { $vlan in
|
|
Section("\(L10n.t("Netzwerk", appLanguage)) \"\(vlan.name)\"") {
|
|
TextField(L10n.t("Name", appLanguage), text: $vlan.name)
|
|
.help(L10n.t("Ein Name zur Wiedererkennung, z.B. \"Gäste\" oder \"Smart Home\".", appLanguage))
|
|
Stepper("\(L10n.t("VLAN-ID", appLanguage)): \(vlan.vlanID)", value: $vlan.vlanID, in: 2...4094)
|
|
.help(L10n.t("Eine eindeutige Nummer zur technischen Unterscheidung dieses Netzwerks. Muss nur innerhalb deines Routers einmalig sein.", appLanguage))
|
|
Picker(L10n.t("Basis-Anschluss", appLanguage), selection: $vlan.parentInterface) {
|
|
ForEach(availableInterfaces) { interface in
|
|
Text(interface.name).tag(interface.name)
|
|
}
|
|
}
|
|
.help(L10n.t("Der physische Anschluss, auf dem dieses zusätzliche Netzwerk aufbaut.", appLanguage))
|
|
TextField(L10n.t("Router-Adresse (z.B. 192.168.20.1/24)", appLanguage), text: $vlan.routerAddress)
|
|
.help(L10n.t("Die Adresse des Routers innerhalb dieses zusätzlichen Netzwerks.", appLanguage))
|
|
TextField(L10n.t("Netzwerk (z.B. 192.168.20.0/24)", appLanguage), text: $vlan.networkAddress)
|
|
.help(L10n.t("Der komplette Adressbereich dieses zusätzlichen Netzwerks.", appLanguage))
|
|
TextField(L10n.t("Automatische Adressvergabe von", appLanguage), text: $vlan.poolRangeStart)
|
|
.help(L10n.t("Ab welcher Adresse Geräte in diesem Netzwerk automatisch eine Adresse bekommen.", appLanguage))
|
|
TextField(L10n.t("bis", appLanguage), text: $vlan.poolRangeEnd)
|
|
.help(L10n.t("Bis zu welcher Adresse Geräte in diesem Netzwerk automatisch eine Adresse bekommen.", appLanguage))
|
|
|
|
Toggle(L10n.t("Von anderen Netzwerken isolieren", appLanguage), isOn: $vlan.isolated)
|
|
.help(L10n.t("Verhindert Datenverkehr zwischen diesem und allen anderen konfigurierten LAN-/VLAN-Netzwerken. Der Internetzugriff bleibt erhalten. Wird im Firewall-Schritt umgesetzt.", appLanguage))
|
|
|
|
Button(L10n.t("Netzwerk entfernen", appLanguage), role: .destructive) {
|
|
viewModel.removeVlan(vlan.id)
|
|
}
|
|
}
|
|
}
|
|
|
|
Button(L10n.t("Weiteres Netzwerk hinzufügen", appLanguage)) {
|
|
viewModel.addVlan()
|
|
}
|
|
}
|
|
|
|
Section {
|
|
HStack {
|
|
Button(L10n.t("Zurück", appLanguage)) { viewModel.goBack() }
|
|
Spacer()
|
|
Button(L10n.t("Weiter", appLanguage)) { viewModel.goNext() }
|
|
.disabled(!isStepValid)
|
|
}
|
|
}
|
|
}
|
|
.formStyle(.grouped)
|
|
.navigationTitle(LocalizedStringKey(L10n.t("Zusätzliche Netzwerke (VLAN)", appLanguage)))
|
|
}
|
|
|
|
/// Only gates on filled-in fields while the VLAN section is actually enabled — with it off,
|
|
/// `viewModel.vlans` is empty (see `setVlanSectionEnabled`) and there's nothing to validate.
|
|
/// Needed as of the fields-start-empty change: previously every VLAN's address fields had a
|
|
/// valid auto-derived default, so "Weiter" was always safe to press; now an incompletely
|
|
/// filled-in VLAN would otherwise reach `buildCommands()` with blank arguments.
|
|
private var isStepValid: Bool {
|
|
guard viewModel.vlanSectionEnabled else { return true }
|
|
return viewModel.vlans.allSatisfy { vlan in
|
|
!vlan.name.isEmpty && !vlan.networkAddress.isEmpty && !vlan.routerAddress.isEmpty
|
|
&& !vlan.poolRangeStart.isEmpty && !vlan.poolRangeEnd.isEmpty
|
|
}
|
|
}
|
|
}
|