From 4ecc5bb3d1517e3e94492f6775f22a7153511dc5 Mon Sep 17 00:00:00 2001 From: mrFq1 <1xxbx0il0@mozmail.com> Date: Sat, 3 Jun 2023 14:46:31 +0800 Subject: [PATCH] feat: AppKit toolbar --- .../DashboardViewContoller.swift | 270 ++++++++++++++++++ .../NotificationNames.swift | 16 ++ .../Connections/ConnectionsView.swift | 13 +- .../Views/ContentTabs/Logs/LogsView.swift | 18 +- .../ContentTabs/Providers/ProvidersView.swift | 8 + .../ContentTabs/Proxies/ProxiesView.swift | 8 + .../Views/ContentTabs/Rules/RulesView.swift | 4 + .../Views/SidebarView/SidebarItem.swift | 31 +- .../Views/SidebarView/SidebarItemView.swift | 28 -- .../Views/SidebarView/SidebarListView.swift | 28 +- .../Views/SidebarView/SidebarView.swift | 13 + ClashX Dashboard/AppDelegate.swift | 13 + 12 files changed, 381 insertions(+), 69 deletions(-) create mode 100644 ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/DashboardViewContoller.swift create mode 100644 ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/NotificationNames.swift delete mode 100644 ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/SidebarView/SidebarItemView.swift diff --git a/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/DashboardViewContoller.swift b/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/DashboardViewContoller.swift new file mode 100644 index 0000000..3badef1 --- /dev/null +++ b/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/DashboardViewContoller.swift @@ -0,0 +1,270 @@ +// +// DashboardViewContoller.swift +// ClashX +// +// Created by yicheng on 2018/8/28. +// Copyright © 2018年 west2online. All rights reserved. +// + +import Cocoa +import SwiftUI + +public class DashboardWindowController: NSWindowController { + public var onWindowClose: (() -> Void)? + + public static func create() -> DashboardWindowController { + let win = NSWindow() + win.center() + let wc = DashboardWindowController(window: win) + wc.contentViewController = DashboardViewContoller() + return wc + } + + public override func showWindow(_ sender: Any?) { + super.showWindow(sender) + NSApp.activate(ignoringOtherApps: true) + window?.makeKeyAndOrderFront(self) + window?.delegate = self + } +} + +extension DashboardWindowController: NSWindowDelegate { + public func windowWillClose(_ notification: Notification) { + NSApp.setActivationPolicy(.accessory) + onWindowClose?() + if let contentVC = contentViewController as? DashboardViewContoller, let win = window { + if !win.styleMask.contains(.fullScreen) { + contentVC.lastSize = win.frame.size + } + } + } +} + +class DashboardViewContoller: NSViewController { + let contentView = NSHostingView(rootView: DashboardView()) + let minSize = NSSize(width: 920, height: 580) + var lastSize: CGSize? { + set { + if let size = newValue { + UserDefaults.standard.set(NSStringFromSize(size), forKey: "ClashWebViewContoller.lastSize") + } + } + get { + if let str = UserDefaults.standard.value(forKey: "ClashWebViewContoller.lastSize") as? String { + return NSSizeFromString(str) as CGSize + } + return nil + } + } + + let effectView = NSVisualEffectView() + + private let levels = [ + ClashLogLevel.silent, + .error, + .warning, + .info, + .debug + ] + + private var sidebarItemObserver: NSObjectProtocol? + + func createWindowController() -> NSWindowController { + let sb = NSStoryboard(name: "Main", bundle: Bundle.main) + let vc = sb.instantiateController(withIdentifier: "DashboardViewContoller") as! DashboardViewContoller + let wc = NSWindowController(window: NSWindow()) + wc.contentViewController = vc + return wc + } + + override func loadView() { + view = contentView + } + + override func viewDidLoad() { + super.viewDidLoad() + + sidebarItemObserver = NotificationCenter.default.addObserver(forName: .sidebarItemChanged, object: nil, queue: .main) { + guard let item = $0.userInfo?["item"] as? SidebarItem else { return } + + var items = [NSToolbarItem.Identifier]() + items.append(.toggleSidebar) + + switch item { + case .overview, .config: + break + case .proxies, .providers: + items.append(.hideNamesItem) + items.append(.searchItem) + case .conns, .rules: + items.append(.searchItem) + case .logs: + items.append(.logLevelItem) + items.append(.searchItem) + } + self.reinitToolbar(items) + } + } + + public override func viewWillAppear() { + super.viewWillAppear() + view.window?.styleMask.insert(.fullSizeContentView) + + view.window?.isOpaque = false + view.window?.styleMask.insert(.closable) + view.window?.styleMask.insert(.resizable) + view.window?.styleMask.insert(.miniaturizable) + + let toolbar = NSToolbar(identifier: .init("DashboardToolbar")) + toolbar.displayMode = .iconOnly + toolbar.delegate = self + + view.window?.toolbar = toolbar + view.window?.title = "Dashboard" + reinitToolbar([]) + + view.window?.minSize = minSize + if let lastSize = lastSize, lastSize != .zero { + view.window?.setContentSize(lastSize) + } + view.window?.center() + if NSApp.activationPolicy() == .accessory { + NSApp.setActivationPolicy(.regular) + } + } + + func reinitToolbar(_ items: [NSToolbarItem.Identifier]) { + guard let toolbar = view.window?.toolbar else { return } + + toolbar.items.enumerated().reversed().forEach { + toolbar.removeItem(at: $0.offset) + } + + items.reversed().forEach { + toolbar.insertItem(withItemIdentifier: $0, at: 0) + } + } + + deinit { + if let sidebarItemObserver { + NotificationCenter.default.removeObserver(sidebarItemObserver) + } + NSApp.setActivationPolicy(.accessory) + } +} + + +extension NSToolbarItem.Identifier { + static let hideNamesItem = NSToolbarItem.Identifier("HideNamesItem") + static let stopConnsItem = NSToolbarItem.Identifier("StopConnsItem") + static let logLevelItem = NSToolbarItem.Identifier("LogLevelItem") + static let searchItem = NSToolbarItem.Identifier("SearchItem") +} + +extension DashboardViewContoller: NSSearchFieldDelegate { + + func controlTextDidChange(_ obj: Notification) { + guard let obj = obj.object as? NSSearchField else { return } + NotificationCenter.default.post(name: .toolbarSearchString, object: nil, userInfo: ["String": obj.stringValue]) + } + + @IBAction func stopConns(_ sender: NSToolbarItem) { + NotificationCenter.default.post(name: .stopConns, object: nil) + } + + @IBAction func hideNames(_ sender: NSToolbarItem) { + switch sender.tag { + case 0: + sender.tag = 1 + sender.image = NSImage(systemSymbolName: "eyeglasses", accessibilityDescription: nil) + case 1: + sender.tag = 0 + sender.image = NSImage(systemSymbolName: "wand.and.stars", accessibilityDescription: nil) + default: + break + } + + NotificationCenter.default.post(name: .hideNames, object: nil, userInfo: ["hide": sender.tag == 1]) + } + + @objc func setLogLevel(_ sender: NSToolbarItemGroup) { + guard sender.selectedIndex < levels.count, sender.selectedIndex >= 0 else { return } + let level = levels[sender.selectedIndex] + + NotificationCenter.default.post(name: .logLevelChanged, object: nil, userInfo: ["level": level]) + } + +} + +extension DashboardViewContoller: NSToolbarDelegate, NSToolbarItemValidation { + + func validateToolbarItem(_ item: NSToolbarItem) -> Bool { + return true + } + + func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { + + switch itemIdentifier { + case .searchItem: + let item = NSSearchToolbarItem(itemIdentifier: .searchItem) + item.resignsFirstResponderWithCancel = true + item.searchField.delegate = self + item.toolTip = "Search" + return item + case .toggleSidebar: + return NSTrackingSeparatorToolbarItem(itemIdentifier: .toggleSidebar) + case .logLevelItem: + + let titles = levels.map { + $0.rawValue.capitalized + } + + let group = NSToolbarItemGroup(itemIdentifier: .logLevelItem, titles: titles, selectionMode: .selectOne, labels: titles, target: nil, action: #selector(setLogLevel(_:))) + group.selectionMode = .selectOne + group.controlRepresentation = .collapsed + group.selectedIndex = levels.firstIndex(of: ConfigManager.selectLoggingApiLevel) ?? 0 + + return group + case .hideNamesItem: + let item = NSToolbarItem(itemIdentifier: .hideNamesItem) + item.target = self + item.action = #selector(hideNames(_:)) + item.isBordered = true + item.tag = 0 + item.image = NSImage(systemSymbolName: "wand.and.stars", accessibilityDescription: nil) + return item + case .stopConnsItem: + let item = NSToolbarItem(itemIdentifier: .stopConnsItem) + item.target = self + item.action = #selector(stopConns(_:)) + item.isBordered = true + item.image = NSImage(systemSymbolName: "stop.circle.fill", accessibilityDescription: nil) + return item + default: + break + } + + return nil + } + + + func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + [ + .toggleSidebar, + .stopConnsItem, + .hideNamesItem, + .logLevelItem, + .searchItem + ] + } + + func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + [ + .toggleSidebar, + .stopConnsItem, + .hideNamesItem, + .logLevelItem, + .searchItem + ] + } +} diff --git a/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/NotificationNames.swift b/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/NotificationNames.swift new file mode 100644 index 0000000..88d61c1 --- /dev/null +++ b/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/NotificationNames.swift @@ -0,0 +1,16 @@ +// +// NotificationNames.swift +// +// +// + +import Foundation + +extension NSNotification.Name { + static let sidebarItemChanged = NSNotification.Name("SidebarItemChanged") + + static let toolbarSearchString = NSNotification.Name("ToolbarSearchString") + static let stopConns = NSNotification.Name("StopConns") + static let hideNames = NSNotification.Name("HideNames") + static let logLevelChanged = NSNotification.Name("LogLevelChanged") +} diff --git a/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/ContentTabs/Connections/ConnectionsView.swift b/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/ContentTabs/Connections/ConnectionsView.swift index bee6d84..b4c6d9b 100644 --- a/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/ContentTabs/Connections/ConnectionsView.swift +++ b/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/ContentTabs/Connections/ConnectionsView.swift @@ -18,16 +18,27 @@ struct ConnectionsView: View { filterString: searchString) .background(Color(nsColor: .textBackgroundColor)) .searchable(text: $searchString) + .onReceive(NotificationCenter.default.publisher(for: .toolbarSearchString)) { + guard let string = $0.userInfo?["String"] as? String else { return } + searchString = string + } + .onReceive(NotificationCenter.default.publisher(for: .stopConns)) { _ in + stopConns() + } .toolbar { ToolbarItem { Button { - ApiRequest.closeAllConnection() + stopConns() } label: { Image(systemName: "stop.circle.fill") } } } } + + func stopConns() { + ApiRequest.closeAllConnection() + } } struct ConnectionsView_Previews: PreviewProvider { diff --git a/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/ContentTabs/Logs/LogsView.swift b/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/ContentTabs/Logs/LogsView.swift index 5b11790..775c16b 100644 --- a/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/ContentTabs/Logs/LogsView.swift +++ b/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/ContentTabs/Logs/LogsView.swift @@ -46,6 +46,14 @@ struct LogsView: View { TableColumn("", value: \.log) } .searchable(text: $searchString) + .onReceive(NotificationCenter.default.publisher(for: .toolbarSearchString)) { + guard let string = $0.userInfo?["String"] as? String else { return } + searchString = string + } + .onReceive(NotificationCenter.default.publisher(for: .logLevelChanged)) { + guard let level = $0.userInfo?["level"] as? ClashLogLevel else { return } + logLevelChanged(level) + } .toolbar { ToolbarItem { Picker("", selection: $logLevel) { @@ -62,13 +70,17 @@ struct LogsView: View { .pickerStyle(.menu) .onChange(of: logLevel) { newValue in guard newValue != ConfigManager.selectLoggingApiLevel else { return } - logStorage.logs.removeAll() - ConfigManager.selectLoggingApiLevel = newValue - ApiRequest.shared.resetLogStreamApi() + logLevelChanged(newValue) } } } } + + func logLevelChanged(_ level: ClashLogLevel) { + logStorage.logs.removeAll() + ConfigManager.selectLoggingApiLevel = level + ApiRequest.shared.resetLogStreamApi() + } } struct LogsView_Previews: PreviewProvider { diff --git a/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/ContentTabs/Providers/ProvidersView.swift b/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/ContentTabs/Providers/ProvidersView.swift index d4a2bc7..3ab574d 100644 --- a/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/ContentTabs/Providers/ProvidersView.swift +++ b/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/ContentTabs/Providers/ProvidersView.swift @@ -20,6 +20,14 @@ struct ProvidersView: View { EmptyView() } .searchable(text: $searchString.string) + .onReceive(NotificationCenter.default.publisher(for: .toolbarSearchString)) { + guard let string = $0.userInfo?["String"] as? String else { return } + searchString.string = string + } + .onReceive(NotificationCenter.default.publisher(for: .hideNames)) { + guard let hide = $0.userInfo?["hide"] as? Bool else { return } + hideProxyNames.hide = hide + } .environmentObject(searchString) .onAppear { loadProviders() diff --git a/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/ContentTabs/Proxies/ProxiesView.swift b/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/ContentTabs/Proxies/ProxiesView.swift index 77ebb09..48790e1 100644 --- a/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/ContentTabs/Proxies/ProxiesView.swift +++ b/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/ContentTabs/Proxies/ProxiesView.swift @@ -34,6 +34,14 @@ struct ProxiesView: View { EmptyView() } .searchable(text: $searchString.string) + .onReceive(NotificationCenter.default.publisher(for: .toolbarSearchString)) { + guard let string = $0.userInfo?["String"] as? String else { return } + searchString.string = string + } + .onReceive(NotificationCenter.default.publisher(for: .hideNames)) { + guard let hide = $0.userInfo?["hide"] as? Bool else { return } + hideProxyNames.hide = hide + } .environmentObject(searchString) .onAppear { loadProxies() diff --git a/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/ContentTabs/Rules/RulesView.swift b/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/ContentTabs/Rules/RulesView.swift index c122ae0..4b000aa 100644 --- a/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/ContentTabs/Rules/RulesView.swift +++ b/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/ContentTabs/Rules/RulesView.swift @@ -29,6 +29,10 @@ struct RulesView: View { } } .searchable(text: $searchString) + .onReceive(NotificationCenter.default.publisher(for: .toolbarSearchString)) { + guard let string = $0.userInfo?["String"] as? String else { return } + searchString = string + } .onAppear { ruleItems.removeAll() ApiRequest.getRules { diff --git a/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/SidebarView/SidebarItem.swift b/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/SidebarView/SidebarItem.swift index c445966..2d4514b 100644 --- a/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/SidebarView/SidebarItem.swift +++ b/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/SidebarView/SidebarItem.swift @@ -7,27 +7,12 @@ import Cocoa import SwiftUI - -class SidebarItems: ObservableObject, Identifiable { - let id = UUID() - @Published var items: [SidebarItem] - @Published var selectedIndex = 0 - - init(_ items: [SidebarItem]) { - self.items = items - } -} - -class SidebarItem: ObservableObject { - let id = UUID() - let name: String - let icon: String - let view: AnyView - - - init(name: String, icon: String, view: AnyView) { - self.name = name - self.icon = icon - self.view = view - } +enum SidebarItem: String { + case overview = "Overview" + case proxies = "Proxies" + case providers = "Providers" + case rules = "Rules" + case conns = "Conns" + case config = "Config" + case logs = "Logs" } diff --git a/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/SidebarView/SidebarItemView.swift b/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/SidebarView/SidebarItemView.swift deleted file mode 100644 index 3f6fdf8..0000000 --- a/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/SidebarView/SidebarItemView.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// SidebarItemView.swift -// ClashX Dashboard -// -// - -import SwiftUI - -struct SidebarItemView: View { - - @State var item: SidebarItem - - @Binding var selectionName: String? - - var body: some View { - NavigationLink(destination: item.view, tag: item.name, selection: $selectionName) { - Label(item.name, systemImage: item.icon) - } - } -} - -//struct SidebarItemView_Previews: PreviewProvider { -// static var previews: some View { -// SidebarItemView(item: .init(name: "Overview", -// icon: "chart.bar.xaxis", -// view: AnyView(OverviewView()))) -// } -//} diff --git a/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/SidebarView/SidebarListView.swift b/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/SidebarView/SidebarListView.swift index 80ae113..dd1f88c 100644 --- a/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/SidebarView/SidebarListView.swift +++ b/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/SidebarView/SidebarListView.swift @@ -14,45 +14,45 @@ struct SidebarListView: View { List { NavigationLink(destination: OverviewView(), - tag: "Overview", + tag: SidebarItem.overview.rawValue, selection: $selectionName) { - Label("Overview", systemImage: "chart.bar.xaxis") + Label(SidebarItem.overview.rawValue, systemImage: "chart.bar.xaxis") } NavigationLink(destination: ProxiesView(), - tag: "Proxies", + tag: SidebarItem.proxies.rawValue, selection: $selectionName) { - Label("Proxies", systemImage: "globe.asia.australia") + Label(SidebarItem.proxies.rawValue, systemImage: "globe.asia.australia") } NavigationLink(destination: ProvidersView(), - tag: "Providers", + tag: SidebarItem.providers.rawValue, selection: $selectionName) { - Label("Providers", systemImage: "link.icloud") + Label(SidebarItem.providers.rawValue, systemImage: "link.icloud") } NavigationLink(destination: RulesView(), - tag: "Rules", + tag: SidebarItem.rules.rawValue, selection: $selectionName) { - Label("Rules", systemImage: "waveform.and.magnifyingglass") + Label(SidebarItem.rules.rawValue, systemImage: "waveform.and.magnifyingglass") } NavigationLink(destination: ConnectionsView(), - tag: "Conns", + tag: SidebarItem.conns.rawValue, selection: $selectionName) { - Label("Conns", systemImage: "app.connected.to.app.below.fill") + Label(SidebarItem.conns.rawValue, systemImage: "app.connected.to.app.below.fill") } NavigationLink(destination: ConfigView(), - tag: "Config", + tag: SidebarItem.config.rawValue, selection: $selectionName) { - Label("Config", systemImage: "slider.horizontal.3") + Label(SidebarItem.config.rawValue, systemImage: "slider.horizontal.3") } NavigationLink(destination: LogsView(), - tag: "Logs", + tag: SidebarItem.logs.rawValue, selection: $selectionName) { - Label("Logs", systemImage: "wand.and.stars.inverse") + Label(SidebarItem.logs.rawValue, systemImage: "wand.and.stars.inverse") } diff --git a/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/SidebarView/SidebarView.swift b/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/SidebarView/SidebarView.swift index 33128da..b1d7246 100644 --- a/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/SidebarView/SidebarView.swift +++ b/ClashX Dashboard Kit/Sources/ClashX Dashboard Kit/Views/SidebarView/SidebarView.swift @@ -27,6 +27,8 @@ struct SidebarView: View { ConfigManager.selectLoggingApiLevel = .info } + sidebarItemChanged(sidebarSelectionName) + clashApiDatasStorage.resetStreamApi() connsQueue.sync { clashApiDatasStorage.connsStorage.conns @@ -35,9 +37,13 @@ struct SidebarView: View { updateConnections() } + .onChange(of: sidebarSelectionName) { newValue in + sidebarItemChanged(newValue) + } .onReceive(timer, perform: { _ in updateConnections() }) + } func updateConnections() { @@ -50,6 +56,13 @@ struct SidebarView: View { } } } + + func sidebarItemChanged(_ name: String?) { + guard let str = name, + let item = SidebarItem(rawValue: str) else { return } + + NotificationCenter.default.post(name: .sidebarItemChanged, object: nil, userInfo: ["item": item]) + } } //struct SidebarView_Previews: PreviewProvider { diff --git a/ClashX Dashboard/AppDelegate.swift b/ClashX Dashboard/AppDelegate.swift index 4b7a0f8..8b3cd0e 100644 --- a/ClashX Dashboard/AppDelegate.swift +++ b/ClashX Dashboard/AppDelegate.swift @@ -11,8 +11,21 @@ import ClashX_Dashboard_Kit @main class AppDelegate: NSObject, NSApplicationDelegate { + var dashboardWindowController: DashboardWindowController? + func applicationDidFinishLaunching(_ notification: Notification) { + if dashboardWindowController == nil { + dashboardWindowController = DashboardWindowController.create() + dashboardWindowController?.onWindowClose = { + [weak self] in + self?.dashboardWindowController = nil + } + } + dashboardWindowController?.showWindow(nil) + + + }