"?"-Hilfe-Buttons in allen 6 Haupt-Tabs, allen 6 Wizard-Schritten und allen 45 Experte-Menüs öffnen ein Handbuch-Fenster (WKWebView) und springen per Textanker direkt zur passenden Manual-Stelle. build-manual.py generalisiert auf beliebig viele Sprachen (LANGUAGES- Dict) statt hart DE/EN. Manual.en.md: komplette Handübersetzung aller Fließtext-Kapitel. Kapitel 5 (Experte-Referenz) wird pro Sprache automatisch übersetzt, indem L10n.swifts eigenes App-Übersetzungs- Dictionary wiederverwendet wird (714 Einträge geparst) statt einer zweiten, separat gepflegten Übersetzung. Live bestätigt (DE und EN). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
119 lines
5.1 KiB
Swift
119 lines
5.1 KiB
Swift
import SwiftUI
|
|
import WebKit
|
|
|
|
/// Static/stable anchor ids matching the `<a id="...">` markers hand-placed in `Manual.md`
|
|
/// (tab-/step-anchors) and auto-generated by `build-manual.py`'s `schema_anchor()` (Experte
|
|
/// menus) — the two sides are kept in sync by construction: this is the exact same string
|
|
/// transform as the Python function, not a generated file, so there's nothing to regenerate
|
|
/// when a schema's `menuPath` doesn't change.
|
|
enum ManualAnchor {
|
|
static let tabConnect = "tab-connect"
|
|
static let stepWAN = "step-wan"
|
|
static let stepLAN = "step-lan"
|
|
static let stepVLAN = "step-vlan"
|
|
static let stepWifi = "step-wifi"
|
|
static let stepFirewall = "step-firewall"
|
|
static let stepReview = "step-review"
|
|
static let tabOverview = "tab-overview"
|
|
static let tabDevices = "tab-devices"
|
|
static let tabExpert = "tab-expert"
|
|
static let tabBackup = "tab-backup"
|
|
static let tabSettings = "tab-settings"
|
|
|
|
/// Mirrors `build-manual.py`'s `schema_anchor(menu_path)` exactly.
|
|
static func schema(_ menuPath: String) -> String {
|
|
let trimmed = menuPath.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
|
|
return "schema-" + trimmed.replacingOccurrences(of: " ", with: "-")
|
|
}
|
|
}
|
|
|
|
/// Shared across the app (one instance, injected as an `@StateObject` in
|
|
/// `RouterOSAssistantApp`) so any "?"-help button anywhere can request the Handbuch window
|
|
/// jump to a specific section, whether or not that window is already open.
|
|
@MainActor
|
|
final class ManualNavigator: ObservableObject {
|
|
@Published var pendingAnchor: String?
|
|
|
|
func go(to anchor: String) {
|
|
// Reassigning the same value wouldn't trigger `didSet`/a fresh `onChange` in
|
|
// `ManualView` if the user clicks the same help button twice in a row — nil the
|
|
// slot first so every request, including a repeat, actually re-scrolls.
|
|
pendingAnchor = nil
|
|
DispatchQueue.main.async { self.pendingAnchor = anchor }
|
|
}
|
|
}
|
|
|
|
private struct ManualWebView: NSViewRepresentable {
|
|
@ObservedObject var navigator: ManualNavigator
|
|
let language: String
|
|
|
|
/// One HTML file per language (`Manual.html` for "de", `Manual_en.html`, later
|
|
/// `Manual_es.html`, ...) — `build-manual.py` names them the same way (`suffix = ""
|
|
/// if lang == "de" else f"_{lang}"`), so a new language needs no Swift change here,
|
|
/// only a new `Manual.<lang>.md` + an entry in `LANGUAGES` in that script.
|
|
final class Coordinator {
|
|
var loadedLanguage: String?
|
|
}
|
|
|
|
func makeCoordinator() -> Coordinator { Coordinator() }
|
|
|
|
func makeNSView(context: Context) -> WKWebView {
|
|
let webView = WKWebView()
|
|
load(language, into: webView, coordinator: context.coordinator)
|
|
return webView
|
|
}
|
|
|
|
func updateNSView(_ webView: WKWebView, context: Context) {
|
|
if context.coordinator.loadedLanguage != language {
|
|
load(language, into: webView, coordinator: context.coordinator)
|
|
}
|
|
guard let anchor = navigator.pendingAnchor else { return }
|
|
// The page is already loaded — scrolling via JS instead of re-navigating to
|
|
// "Manual.html#anchor" avoids a full page reload/flash on every help-button click.
|
|
webView.evaluateJavaScript(
|
|
"document.getElementById(\(String(reflecting: anchor)))?.scrollIntoView({behavior: 'smooth', block: 'start'});"
|
|
)
|
|
}
|
|
|
|
private func load(_ language: String, into webView: WKWebView, coordinator: Coordinator) {
|
|
let resourceName = language == "de" ? "Manual" : "Manual_\(language)"
|
|
// Falls back to the German file if this language has no translated manual yet
|
|
// (e.g. a language the app UI supports but the manual hasn't been translated
|
|
// into) rather than showing a blank window.
|
|
guard let url = Bundle.main.url(forResource: resourceName, withExtension: "html")
|
|
?? Bundle.main.url(forResource: "Manual", withExtension: "html") else { return }
|
|
webView.loadFileURL(url, allowingReadAccessTo: url)
|
|
coordinator.loadedLanguage = language
|
|
}
|
|
}
|
|
|
|
struct ManualView: View {
|
|
@EnvironmentObject private var navigator: ManualNavigator
|
|
@AppStorage("appLanguage") private var appLanguage: String = "de"
|
|
|
|
var body: some View {
|
|
ManualWebView(navigator: navigator, language: appLanguage)
|
|
.frame(minWidth: 640, minHeight: 480)
|
|
}
|
|
}
|
|
|
|
/// Drop-in "?" toolbar button for any screen — opens (or focuses, if already open) the
|
|
/// Handbuch window scrolled straight to `anchor`. One shared implementation so every tab/
|
|
/// wizard-step/Experte-schema screen wires help the same way.
|
|
struct ManualHelpButton: View {
|
|
let anchor: String
|
|
@AppStorage("appLanguage") private var appLanguage: String = "de"
|
|
@EnvironmentObject private var navigator: ManualNavigator
|
|
@Environment(\.openWindow) private var openWindow
|
|
|
|
var body: some View {
|
|
Button {
|
|
navigator.go(to: anchor)
|
|
openWindow(id: "manual")
|
|
} label: {
|
|
Image(systemName: "questionmark.circle")
|
|
}
|
|
.help(L10n.t("Hilfe zu diesem Bereich im Handbuch öffnen", appLanguage))
|
|
}
|
|
}
|