import SwiftUI struct ExpertView: View { @ObservedObject var connectionService: ConnectionService @StateObject private var viewModel: ExpertViewModel @AppStorage("appLanguage") private var appLanguage: String = "de" @AppStorage(AppPreferences.colorThemeKey) private var colorThemeRaw: String = ColorTheme.standard.rawValue private var colorTheme: ColorTheme { ColorTheme(rawValue: colorThemeRaw) ?? .standard } /// A category's presence in this set means "collapsed". Starts with every category /// collapsed (Nutzerwunsch: "standardmäßig sollten beim Öffnen des Experte-Tab alle /// Sektion zugeklappt sein") — click a header to fold its menu list open again. @State private var collapsedCategories: Set = Set(RouterOSMenuCategory.allCases) @State private var isCustomPathExpanded = false init(connectionService: ConnectionService) { self.connectionService = connectionService _viewModel = StateObject(wrappedValue: ExpertViewModel(connectionService: connectionService)) } var body: some View { Group { if connectionService.credentials == nil { ContentUnavailableView( LocalizedStringKey(L10n.t("Nicht verbunden", appLanguage)), systemImage: "network.slash", description: Text(L10n.t("Verbinde dich zuerst im Tab \"Verbinden\" mit deinem Router.", appLanguage)) ) } else { NavigationSplitView { List { ForEach(RouterOSMenuCategory.allCases) { category in let schemas = RouterOSSchemaCatalog.schemas(in: category) if !schemas.isEmpty { let isExpanded = !collapsedCategories.contains(category) Section { if isExpanded { ForEach(schemas) { schema in let isSelected = viewModel.selectedSchema?.menuPath == schema.menuPath Button { viewModel.open(schema) } label: { Text(L10n.t(schema.displayName, appLanguage)) .frame(maxWidth: .infinity, alignment: .leading) .padding(.vertical, 4) .padding(.horizontal, 6) .background( RoundedRectangle(cornerRadius: 6) .fill(isSelected ? Color.blue.opacity(0.25) : Color.clear) ) .contentShape(Rectangle()) } .buttonStyle(.plain) } } } header: { sectionHeader(L10n.t(category.rawValue, appLanguage), isExpanded: isExpanded, categoryColor: colorTheme.color(for: category)) { if isExpanded { collapsedCategories.insert(category) } else { collapsedCategories.remove(category) } } } } } Section { if isCustomPathExpanded { Text(L10n.t("Jeder RouterOS-Menüpfad ist erreichbar, auch wenn er oben nicht gelistet ist — die Felder erscheinen dann generisch (Schlüssel/Wert), ohne kuratierte Erklärung.", appLanguage)) .appFont(.caption) .foregroundStyle(.secondary) TextField(L10n.t("Menüpfad, z.B. /ip firewall filter", appLanguage), text: $viewModel.customMenuPath) .help(L10n.t("Der RouterOS-CLI-Pfad, z.B. \"/interface wireguard\" oder \"/routing ospf instance\".", appLanguage)) TextField(L10n.t("REST-Pfad, z.B. ip/firewall/filter", appLanguage), text: $viewModel.customRestPath) .help(L10n.t("Derselbe Pfad in REST-API-Schreibweise: führende \"/\" weg, Leerzeichen zu \"/\".", appLanguage)) Button(L10n.t("Öffnen", appLanguage)) { viewModel.openCustomPath() } .disabled(viewModel.customMenuPath.isEmpty || viewModel.customRestPath.isEmpty) } } header: { sectionHeader(L10n.t("Eigener Menüpfad", appLanguage), isExpanded: isCustomPathExpanded) { isCustomPathExpanded.toggle() } } } .listStyle(.sidebar) .navigationTitle(LocalizedStringKey(L10n.t("Experte", appLanguage))) .toolbar { ToolbarItem { ManualHelpButton(anchor: ManualAnchor.tabExpert) } } } detail: { if let schema = viewModel.selectedSchema { ExpertMenuDetailView( viewModel: viewModel, schema: schema ) } else { ContentUnavailableView( LocalizedStringKey(L10n.t("Kein Menü ausgewählt", appLanguage)), systemImage: "list.bullet.rectangle", description: Text(L10n.t("Wähle links einen RouterOS-Bereich aus.", appLanguage)) ) } } } } } /// Custom Section header — plain `Section("title")` rendered as macOS' default tiny /// gray-caps sidebar label, too easy to miss when scanning nine menu categories (Nutzerwunsch: /// "müssen prominenter sein und eingefärbt"). `.textCase(nil)` is required here, or macOS /// forces its own uppercase-small-caps styling back on top of this custom view regardless of /// the font/background set below. `Color.accentColor` (not a fixed RGB value) so the tint /// stays visible and correctly contrasted in both light and dark mode automatically. /// /// The chevron is drawn explicitly here rather than relying on `Section(isExpanded:)`'s /// built-in disclosure triangle — that one is macOS sidebar-standard hover-only (matches /// Finder), which the user explicitly didn't want ("der Pfeil sollte immer zu sehen sein und /// nicht erst beim Hovern"). The whole header is a plain `Button` toggling `onToggle`, with /// the section's own content shown/hidden by the caller via a plain `if isExpanded` instead /// of the `Section(isExpanded:)` API, since that API owns its own (hover-only) chevron. private func sectionHeader(_ title: String, isExpanded: Bool, categoryColor: Color? = nil, onToggle: @escaping () -> Void) -> some View { Button(action: onToggle) { HStack(spacing: 6) { Image(systemName: "chevron.right") .appFont(.caption, bold: true) .foregroundStyle(.secondary) .rotationEffect(.degrees(isExpanded ? 90 : 0)) if let categoryColor { Circle().fill(categoryColor).frame(width: 8, height: 8) } Text(title) .appFont(.title3, bold: true) .foregroundStyle(.primary) } .padding(.vertical, 6) .padding(.horizontal, 8) .frame(maxWidth: .infinity, alignment: .leading) .background(RoundedRectangle(cornerRadius: 6).fill(Color.accentColor.opacity(0.18))) .contentShape(Rectangle()) } .buttonStyle(.plain) .textCase(nil) } } #Preview { ExpertView(connectionService: ConnectionService()) }