import SwiftUI import WebKit /// Static/stable anchor ids matching the `` 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..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)) } }