Files
RouterOS/RouterOSAssistant/Features/Manual/ManualView.swift
T
KayandClaude Sonnet 5 3a26f50800 M31: Handbuch in der App (Textanker, DE+EN)
"?"-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>
2026-09-17 17:15:29 +02:00

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))
}
}