From dbd8700145170e64131a006787749a826d43ee95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 15 Aug 2023 17:20:44 +0800 Subject: [PATCH] Refactor command client --- .../Views/Dashboard/DashboardView.swift | 9 +- .../Views/Dashboard/ExtensionStatusView.swift | 73 +--------- .../Dashboard/InstallProfileButton.swift | 2 - .../Views/Dashboard/StartStopButton.swift | 16 +- .../Views/EnvironmentValues.swift | 26 ---- .../Views/Groups/GroupListView.swift | 83 ++--------- ApplicationLibrary/Views/Log/LogClient.swift | 120 --------------- ApplicationLibrary/Views/Log/LogView.swift | 103 +++++-------- .../Views/Setting/ServiceLogView.swift | 5 +- Library/Network/CommandClient.swift | 137 ++++++++++++++++++ Library/Network/ExtensionEnvironments.swift | 45 ++++++ MacLibrary/MacApplication.swift | 4 +- MacLibrary/MainView.swift | 94 ++++-------- MacLibrary/MenuLabel.swift | 40 +++++ MacLibrary/SidebarView.swift | 6 +- SFI/Application.swift | 4 +- SFI/MainView.swift | 74 +++------- SFT/Application.swift | 4 + SFT/MainView.swift | 93 +++--------- sing-box.xcodeproj/project.pbxproj | 12 +- 20 files changed, 378 insertions(+), 572 deletions(-) delete mode 100644 ApplicationLibrary/Views/Log/LogClient.swift create mode 100644 Library/Network/CommandClient.swift create mode 100644 Library/Network/ExtensionEnvironments.swift create mode 100644 MacLibrary/MenuLabel.swift diff --git a/ApplicationLibrary/Views/Dashboard/DashboardView.swift b/ApplicationLibrary/Views/Dashboard/DashboardView.swift index d25ed2f..9a667d7 100644 --- a/ApplicationLibrary/Views/Dashboard/DashboardView.swift +++ b/ApplicationLibrary/Views/Dashboard/DashboardView.swift @@ -52,11 +52,14 @@ public struct DashboardView: View { #endif struct DashboardView0: View { - @Environment(\.extensionProfile) private var extensionProfile + @EnvironmentObject private var environments: ExtensionEnvironments + var body: some View { if ApplicationLibrary.inPreview { ActiveDashboardView() - } else if let profile = extensionProfile.wrappedValue { + } else if environments.extensionProfileLoading { + ProgressView() + } else if let profile = environments.extensionProfile { DashboardView1().environmentObject(profile) } else { FormView { @@ -67,13 +70,13 @@ public struct DashboardView: View { } struct DashboardView1: View { + @EnvironmentObject private var environments: ExtensionEnvironments @EnvironmentObject private var profile: ExtensionProfile @State private var alert: Alert? var body: some View { VStack { ActiveDashboardView() - .environmentObject(profile) } .alertBinding($alert) .onChangeCompat(of: profile.status) { newValue in diff --git a/ApplicationLibrary/Views/Dashboard/ExtensionStatusView.swift b/ApplicationLibrary/Views/Dashboard/ExtensionStatusView.swift index da22dc2..a1f2345 100644 --- a/ApplicationLibrary/Views/Dashboard/ExtensionStatusView.swift +++ b/ApplicationLibrary/Views/Dashboard/ExtensionStatusView.swift @@ -3,16 +3,13 @@ import Library import SwiftUI public struct ExtensionStatusView: View { - @State private var commandClient: LibboxCommandClient? - @State private var message: LibboxStatusMessage? - @State private var connectTask: Task? + @StateObject private var commandClient = CommandClient(.status) @State private var columnCount: Int = 4 @State private var alert: Alert? private let infoFont = Font.system(.caption, design: .monospaced) public init() {} - public var body: some View { viewBuilder { VStack { @@ -34,7 +31,7 @@ public struct ExtensionStatusView: View { StatusLine("Uplink", "52 MiB") StatusLine("Downlink", "5.6 GiB") } - } else if let message { + } else if let message = commandClient.status { StatusItem("Status") { StatusLine("Memory", LibboxFormatBytes(message.memory)) StatusLine("Goroutines", "\(message.goroutines)") @@ -80,53 +77,15 @@ public struct ExtensionStatusView: View { .frame(alignment: .topLeading) .padding([.top, .leading, .trailing]) } - .onAppear(perform: doReload) + .onAppear { + commandClient.connect() + } .onDisappear { - connectTask?.cancel() - if let commandClient { - try? commandClient.disconnect() - } - commandClient = nil + commandClient.disconnect() } .alertBinding($alert) } - private func doReload() { - connectTask?.cancel() - connectTask = Task.detached { - await connect() - } - } - - private func connect() async { - let clientOptions = LibboxCommandClientOptions() - clientOptions.command = LibboxCommandStatus - clientOptions.statusInterval = Int64(2 * NSEC_PER_SEC) - let client = LibboxNewCommandClient(FilePath.sharedDirectory.relativePath, statusHandler(self), clientOptions)! - - do { - for i in 0 ..< 10 { - try await Task.sleep(nanoseconds: UInt64(Double(100 + (i * 50)) * Double(NSEC_PER_MSEC))) - try Task.checkCancellation() - let isConnected: Bool - do { - try client.connect() - isConnected = true - } catch { - isConnected = false - } - try Task.checkCancellation() - if isConnected { - commandClient = client - return - } - } - } catch { - NSLog("failed to connect status: \(error.localizedDescription)") - try? client.disconnect() - } - } - private func updateColumnCount(_ width: Double) { let v = Int(Int(width) / 155) let new = v < 1 ? 1 : (v > 4 ? 4 : (v % 2 == 0 ? v : v - 1)) @@ -144,26 +103,6 @@ public struct ExtensionStatusView: View { } } - private class statusHandler: NSObject, LibboxCommandClientHandlerProtocol { - private let statusView: ExtensionStatusView - - init(_ statusView: ExtensionStatusView) { - self.statusView = statusView - } - - func connected() {} - - func disconnected(_: String?) {} - - func writeLog(_: String?) {} - - func writeStatus(_ message: LibboxStatusMessage?) { - statusView.message = message - } - - func writeGroups(_: LibboxOutboundGroupIteratorProtocol?) {} - } - private struct StatusItem: View where T: View { private let title: String @ViewBuilder private let content: () -> T diff --git a/ApplicationLibrary/Views/Dashboard/InstallProfileButton.swift b/ApplicationLibrary/Views/Dashboard/InstallProfileButton.swift index e32952e..67fff8a 100644 --- a/ApplicationLibrary/Views/Dashboard/InstallProfileButton.swift +++ b/ApplicationLibrary/Views/Dashboard/InstallProfileButton.swift @@ -2,8 +2,6 @@ import Library import SwiftUI public struct InstallProfileButton: View { - @Environment(\.extensionProfile) private var extensionProfile - @State private var alert: Alert? public init() {} diff --git a/ApplicationLibrary/Views/Dashboard/StartStopButton.swift b/ApplicationLibrary/Views/Dashboard/StartStopButton.swift index 036beb8..a30b57b 100644 --- a/ApplicationLibrary/Views/Dashboard/StartStopButton.swift +++ b/ApplicationLibrary/Views/Dashboard/StartStopButton.swift @@ -3,7 +3,7 @@ import NetworkExtension import SwiftUI public struct StartStopButton: View { - @Environment(\.extensionProfile) private var extensionProfile + @EnvironmentObject private var environments: ExtensionEnvironments public init() {} @@ -20,8 +20,8 @@ public struct StartStopButton: View { }) #endif - } else if let profile = extensionProfile.wrappedValue { - Button0(profile) + } else if let profile = environments.extensionProfile { + Button0().environmentObject(profile) } else { #if os(iOS) || os(tvOS) Toggle(isOn: .constant(false)) { @@ -39,14 +39,10 @@ public struct StartStopButton: View { } private struct Button0: View { - @Environment(\.logClient) private var logClient - @ObservedObject private var profile: ExtensionProfile + @EnvironmentObject private var environments: ExtensionEnvironments + @EnvironmentObject private var profile: ExtensionProfile @State private var alert: Alert? - init(_ profile: ExtensionProfile) { - self.profile = profile - } - var body: some View { viewBuilder { #if os(iOS) || os(tvOS) @@ -81,7 +77,7 @@ public struct StartStopButton: View { do { if isEnabled { try await profile.start() - logClient.wrappedValue?.reconnect() + environments.logClient.connect() } else { profile.stop() } diff --git a/ApplicationLibrary/Views/EnvironmentValues.swift b/ApplicationLibrary/Views/EnvironmentValues.swift index 345c65c..7daaa43 100644 --- a/ApplicationLibrary/Views/EnvironmentValues.swift +++ b/ApplicationLibrary/Views/EnvironmentValues.swift @@ -30,32 +30,6 @@ public extension EnvironmentValues { } } - private struct extensionProfileKey: EnvironmentKey { - static let defaultValue: Binding = .constant(nil) - } - - var extensionProfile: Binding { - get { - self[extensionProfileKey.self] - } - set { - self[extensionProfileKey.self] = newValue - } - } - - private struct logClientKey: EnvironmentKey { - static let defaultValue: Binding = .constant(nil) - } - - var logClient: Binding { - get { - self[logClientKey.self] - } - set { - self[logClientKey.self] = newValue - } - } - private struct importRemoteProfileKey: EnvironmentKey { static var defaultValue: Binding = .constant(nil) } diff --git a/ApplicationLibrary/Views/Groups/GroupListView.swift b/ApplicationLibrary/Views/Groups/GroupListView.swift index dcfa222..bf4dcb0 100644 --- a/ApplicationLibrary/Views/Groups/GroupListView.swift +++ b/ApplicationLibrary/Views/Groups/GroupListView.swift @@ -4,10 +4,8 @@ import SwiftUI public struct GroupListView: View { @State private var isLoading = true - @State private var connectTask: Task? - @State private var commandClient: LibboxCommandClient? + @StateObject private var commandClient = CommandClient(.groups) @State private var groups: [OutboundGroup] = [] - @State private var groupExpand: [String: Bool] = [:] public init() {} public var body: some View { @@ -27,17 +25,20 @@ public struct GroupListView: View { } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) - .onAppear(perform: doReload) - .onDisappear { - connectTask?.cancel() - if let commandClient { - try? commandClient.disconnect() - } - commandClient = nil + .onAppear { + connect() } + .onDisappear { + commandClient.disconnect() + } + .onReceive(commandClient.$groups, perform: { groups in + if let groups { + setGroups(groups) + } + }) } - private func doReload() { + private func connect() { if ApplicationLibrary.inPreview { groups = [ OutboundGroup(tag: "my_group", type: "selector", selected: "server", selectable: true, isExpand: true, items: [ @@ -52,47 +53,11 @@ public struct GroupListView: View { ] isLoading = false } else { - connectTask?.cancel() - connectTask = Task.detached { - await connect() - } + commandClient.connect() } } - private func connect() async { - let clientOptions = LibboxCommandClientOptions() - clientOptions.command = LibboxCommandGroup - clientOptions.statusInterval = Int64(2 * NSEC_PER_SEC) - let client = LibboxNewCommandClient(FilePath.sharedDirectory.relativePath, groupsHandler(self), clientOptions)! - - do { - for i in 0 ..< 10 { - try await Task.sleep(nanoseconds: UInt64(Double(100 + (i * 50)) * Double(NSEC_PER_MSEC))) - try Task.checkCancellation() - let isConnected: Bool - do { - try client.connect() - isConnected = true - } catch { - isConnected = false - } - try Task.checkCancellation() - if isConnected { - commandClient = client - return - } - } - } catch { - NSLog("failed to connect status: \(error.localizedDescription)") - try? client.disconnect() - } - } - - private func setGroups(_ groupIterator: LibboxOutboundGroupIteratorProtocol) { - var goGroups = [LibboxOutboundGroup]() - while groupIterator.hasNext() { - goGroups.append(groupIterator.next()!) - } + private func setGroups(_ goGroups: [LibboxOutboundGroup]) { var groups = [OutboundGroup]() for goGroup in goGroups { var items = [OutboundGroupItem]() @@ -106,24 +71,4 @@ public struct GroupListView: View { self.groups = groups isLoading = false } - - private class groupsHandler: NSObject, LibboxCommandClientHandlerProtocol { - private let groupListView: GroupListView - - init(_ statusView: GroupListView) { - groupListView = statusView - } - - func connected() {} - - func disconnected(_: String?) {} - - func writeLog(_: String?) {} - - func writeStatus(_: LibboxStatusMessage?) {} - - func writeGroups(_ groupIterator: LibboxOutboundGroupIteratorProtocol?) { - groupListView.setGroups(groupIterator!) - } - } } diff --git a/ApplicationLibrary/Views/Log/LogClient.swift b/ApplicationLibrary/Views/Log/LogClient.swift deleted file mode 100644 index 95d34a9..0000000 --- a/ApplicationLibrary/Views/Log/LogClient.swift +++ /dev/null @@ -1,120 +0,0 @@ -import Foundation -import Libbox -import Library -import SwiftUI - -public class LogClient: ObservableObject { - private var maxLines: Int - @Published public var isConnected: Bool - @Published public var logList: [String] - - private var commandClient: LibboxCommandClient! - private var connectTask: Task? - - public init(_ maxLines: Int) { - self.maxLines = maxLines - isConnected = false - logList = [] - } - - deinit { - if let connectTask { - connectTask.cancel() - } - if let commandClient { - try? commandClient.disconnect() - } - } - - public func reconnect() { - if ApplicationLibrary.inPreview { - logList = [ - "(packet-tunnel) log server started", - "INFO[0000] router: loaded geoip database: 250 codes", - "INFO[0000] router: loaded geosite database: 1400 codes", - "INFO[0000] router: updated default interface en0, index 11", - "inbound/tun[0]: started at utun3", - "sing-box started (1.666s)", - ] - isConnected = true - } else { - if isConnected { - return - } - if let connectTask { - connectTask.cancel() - } - connectTask = Task.detached { - await self.connect() - } - } - } - - private func connect() async { - let clientOptions = LibboxCommandClientOptions() - clientOptions.command = LibboxCommandLog - clientOptions.statusInterval = Int64(2 * NSEC_PER_SEC) - let client = LibboxNewCommandClient(FilePath.sharedDirectory.relativePath, logHandler(self), clientOptions)! - - do { - for i in 0 ..< 10 { - try await Task.sleep(nanoseconds: UInt64(Double(100 + (i * 50)) * Double(NSEC_PER_MSEC))) - try Task.checkCancellation() - let isConnected: Bool - do { - try client.connect() - isConnected = true - } catch { - isConnected = false - } - try Task.checkCancellation() - if isConnected { - commandClient = client - return - } - } - } catch { - try? client.disconnect() - } - } - - private class logHandler: NSObject, LibboxCommandClientHandlerProtocol { - private let logClient: LogClient - - init(_ logClient: LogClient) { - self.logClient = logClient - } - - @MainActor - func connected() { - logClient.logList.removeAll() - logClient.isConnected = true - } - - @MainActor - func disconnected(_ message: String?) { - if let message { - logClient.logList.append("(log client closed) \(message)") - } else { - logClient.logList.append("(log client closed)") - } - try? logClient.commandClient?.disconnect() - logClient.commandClient = nil - logClient.isConnected = false - } - - @MainActor - func writeLog(_ message: String?) { - guard let message else { - return - } - if logClient.logList.count > logClient.maxLines { - logClient.logList.removeFirst() - } - logClient.logList.append(message) - } - - func writeStatus(_: LibboxStatusMessage?) {} - func writeGroups(_: LibboxOutboundGroupIteratorProtocol?) {} - } -} diff --git a/ApplicationLibrary/Views/Log/LogView.swift b/ApplicationLibrary/Views/Log/LogView.swift index ffb99af..5bc0e0a 100644 --- a/ApplicationLibrary/Views/Log/LogView.swift +++ b/ApplicationLibrary/Views/Log/LogView.swift @@ -1,80 +1,53 @@ +import Library import SwiftUI public struct LogView: View { - @Environment(\.logClient) private var logClient + @Environment(\.selection) private var selection + @EnvironmentObject private var environments: ExtensionEnvironments + + private let logFont = Font.system(.caption2, design: .monospaced) public init() {} public var body: some View { - viewBuilder { - if let logClient = logClient.wrappedValue { - LogView0().environmentObject(logClient) - } else { - Text("Service not started") - } - } - } - - private struct LogView0: View { - @Environment(\.selection) private var selection - @Environment(\.extensionProfile) private var extensionProfile - @EnvironmentObject private var logClient: LogClient - - private let logFont = Font.system(.caption2, design: .monospaced) - - var body: some View { - viewBuilder { - if logClient.logList.isEmpty { - VStack { - if logClient.isConnected { - Text("Empty logs") - } else { - Text("Service not started").onAppear(perform: connectLog) - } - }.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + if environments.logClient.logList.isEmpty { + VStack { + if environments.logClient.isConnected { + Text("Empty logs") } else { - ScrollViewReader { reader in - ScrollView { - VStack(alignment: .leading, spacing: 0) { - ForEach(Array(logClient.logList.enumerated()), id: \.offset) { it in - Text(it.element) - .font(logFont) - #if os(tvOS) - .focusable() - #endif - Spacer(minLength: 8) - } - - .onChangeCompat(of: logClient.logList.count) { newCount in - withAnimation { - reader.scrollTo(newCount - 1) - } - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - .padding() - } - #if os(tvOS) - .focusEffectDisabled() - .focusSection() - #endif - .onAppear { - reader.scrollTo(logClient.logList.count - 1) - } + Text("Service not started").onAppear { + environments.connectLog() } } - } - } + }.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + } else { + ScrollViewReader { reader in + ScrollView { + VStack(alignment: .leading, spacing: 0) { + ForEach(Array(environments.logClient.logList.enumerated()), id: \.offset) { it in + Text(it.element) + .font(logFont) + #if os(tvOS) + .focusable() + #endif + Spacer(minLength: 8) + } - private func connectLog() { - if ApplicationLibrary.inPreview { - logClient.reconnect() - } else { - guard let profile = extensionProfile.wrappedValue else { - return + .onChangeCompat(of: environments.logClient.logList.count) { newCount in + withAnimation { + reader.scrollTo(newCount - 1) + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .padding() } - if profile.status.isConnected, !logClient.isConnected { - logClient.reconnect() + #if os(tvOS) + .focusEffectDisabled() + .focusSection() + #endif + .onAppear { + reader.scrollTo(environments.logClient.logList.count - 1) } } } diff --git a/ApplicationLibrary/Views/Setting/ServiceLogView.swift b/ApplicationLibrary/Views/Setting/ServiceLogView.swift index 4593f19..49ade65 100644 --- a/ApplicationLibrary/Views/Setting/ServiceLogView.swift +++ b/ApplicationLibrary/Views/Setting/ServiceLogView.swift @@ -73,10 +73,8 @@ public struct ServiceLogView: View { if content.isEmpty { do { content = try String(contentsOf: FilePath.cacheDirectory.appendingPathComponent("stderr.log.old")) - } catch { - } + } catch {} } - isLoading = false } @@ -85,6 +83,7 @@ public struct ServiceLogView: View { try? FileManager.default.removeItem(at: FilePath.cacheDirectory.appendingPathComponent("stderr.log.old")) DispatchQueue.main.async { dismiss() + isLoading = true } } diff --git a/Library/Network/CommandClient.swift b/Library/Network/CommandClient.swift new file mode 100644 index 0000000..0970996 --- /dev/null +++ b/Library/Network/CommandClient.swift @@ -0,0 +1,137 @@ +import Foundation +import Libbox + +public class CommandClient: ObservableObject { + public enum ConnectionType { + case status + case groups + case log + } + + private let connectionType: ConnectionType + private let logMaxLines: Int + private var commandClient: LibboxCommandClient? + private var connectTask: Task? + + @Published public var isConnected: Bool + @Published public var status: LibboxStatusMessage? + @Published public var groups: [LibboxOutboundGroup]? + @Published public var logList: [String] + + public init(_ connectionType: ConnectionType, logMaxLines: Int = 300) { + self.connectionType = connectionType + self.logMaxLines = logMaxLines + logList = [] + isConnected = false + } + + public func connect() { + if isConnected { + return + } + if let connectTask { + connectTask.cancel() + } + connectTask = Task.detached { + await self.connect0() + } + } + + public func disconnect() { + if let connectTask { + connectTask.cancel() + self.connectTask = nil + } + if let commandClient { + try? commandClient.disconnect() + self.commandClient = nil + } + } + + private func connect0() async { + let clientOptions = LibboxCommandClientOptions() + switch connectionType { + case .status: + clientOptions.command = LibboxCommandStatus + case .groups: + clientOptions.command = LibboxCommandGroup + case .log: + clientOptions.command = LibboxCommandLog + } + clientOptions.statusInterval = Int64(2 * NSEC_PER_SEC) + let client = LibboxNewCommandClient(FilePath.sharedDirectory.relativePath, clientHandler(self), clientOptions)! + do { + for i in 0 ..< 10 { + try await Task.sleep(nanoseconds: UInt64(Double(100 + (i * 50)) * Double(NSEC_PER_MSEC))) + try Task.checkCancellation() + let isConnected: Bool + do { + try client.connect() + isConnected = true + } catch { + isConnected = false + } + try Task.checkCancellation() + if isConnected { + commandClient = client + return + } + } + } catch { + try? client.disconnect() + } + } + + private class clientHandler: NSObject, LibboxCommandClientHandlerProtocol { + private let commandClient: CommandClient + + init(_ commandClient: CommandClient) { + self.commandClient = commandClient + } + + func connected() { + DispatchQueue.main.sync { + self.commandClient.isConnected = true + } + } + + func disconnected(_: String?) { + DispatchQueue.main.sync { + self.commandClient.isConnected = false + } + } + + func writeLog(_ message: String?) { + guard let message else { + return + } + var logList = commandClient.logList + if logList.count > commandClient.logMaxLines { + logList.removeFirst() + } + logList.append(message) + DispatchQueue.main.sync { + self.commandClient.logList = logList + } + } + + func writeStatus(_ message: LibboxStatusMessage?) { + DispatchQueue.main.sync { + self.commandClient.status = message + } + } + + func writeGroups(_ groups: LibboxOutboundGroupIteratorProtocol?) { + guard let groups else { + return + } + var newGroups: [LibboxOutboundGroup] = [] + while groups.hasNext() { + newGroups.append(groups.next()!) + } + DispatchQueue.main.sync { + self.commandClient.groups = newGroups + } + } + } +} diff --git a/Library/Network/ExtensionEnvironments.swift b/Library/Network/ExtensionEnvironments.swift new file mode 100644 index 0000000..a7a70fe --- /dev/null +++ b/Library/Network/ExtensionEnvironments.swift @@ -0,0 +1,45 @@ +import Foundation + +public class ExtensionEnvironments: ObservableObject { + @Published public var logClient = CommandClient(.log) + @Published public var extensionProfileLoading = true + @Published public var extensionProfile: ExtensionProfile? + + public init() {} + + deinit { + logClient.disconnect() + } + + public func postReload() { + Task.detached { + await self.reload() + } + } + + public func reload() async { + if let newProfile = try? await ExtensionProfile.load() { + if extensionProfile == nil || extensionProfile?.status == .invalid { + newProfile.register() + await MainActor.run { + extensionProfile = newProfile + extensionProfileLoading = false + } + } + } else { + await MainActor.run { + extensionProfile = nil + extensionProfileLoading = false + } + } + } + + public func connectLog() { + guard let profile = extensionProfile else { + return + } + if profile.status.isConnected, !logClient.isConnected { + logClient.connect() + } + } +} diff --git a/MacLibrary/MacApplication.swift b/MacLibrary/MacApplication.swift index 6de43e6..73e7d62 100644 --- a/MacLibrary/MacApplication.swift +++ b/MacLibrary/MacApplication.swift @@ -5,6 +5,7 @@ import SwiftUI public struct MacApplication: Scene { @State private var showMenuBarExtra = false @State private var isMenuPresented = false + @StateObject private var environments = ExtensionEnvironments() public init() {} public var body: some Scene { @@ -12,10 +13,11 @@ public struct MacApplication: Scene { MainView() .onAppear { Task.detached { - initialize() + await initialize() } } .environment(\.showMenuBarExtra, $showMenuBarExtra) + .environmentObject(environments) }) .commands { if showMenuBarExtra { diff --git a/MacLibrary/MainView.swift b/MacLibrary/MainView.swift index 0530178..0f61036 100644 --- a/MacLibrary/MainView.swift +++ b/MacLibrary/MainView.swift @@ -5,11 +5,9 @@ import SwiftUI public struct MainView: View { @Environment(\.controlActiveState) private var controlActiveState + @EnvironmentObject private var environments: ExtensionEnvironments @State private var selection = NavigationPage.dashboard - @State private var extensionProfile: ExtensionProfile? - @State private var profileLoading = true - @State private var logClient: LogClient! @State private var importProfile: LibboxProfileContent? @State private var importRemoteProfile: LibboxImportRemoteProfile? @State private var alert: Alert? @@ -19,55 +17,41 @@ public struct MainView: View { NavigationSplitView { SidebarView() } detail: { - if profileLoading { - ProgressView().onAppear { - Task { - logClient = LogClient(SharedPreferences.maxLogLines) - await loadProfile() - } - } - } else { - selection.contentView - .navigationTitle(selection.title) - } + selection.contentView + .navigationTitle(selection.title) } - #if !DEBUG .onAppear { + environments.postReload() + #if !DEBUG if Variant.useSystemExtension { Task.detached { checkApplicationPath() } } + #endif + } + .alertBinding($alert) + .toolbar { + ToolbarItem(placement: .navigation) { + StartStopButton() } - #endif - .alertBinding($alert) - .toolbar { - ToolbarItem(placement: .navigation) { - StartStopButton() - } + } + .onChangeCompat(of: controlActiveState) { newValue in + if newValue != .inactive { + environments.postReload() } - .onChangeCompat(of: controlActiveState) { newValue in - if newValue != .inactive { - Task { - await loadProfile() - connectLog() - } - } + } + .onChangeCompat(of: selection) { value in + if value == .logs { + environments.connectLog() } - .onChangeCompat(of: selection) { value in - if value == .logs { - connectLog() - } - } - .formStyle(.grouped) - .environment(\.selection, $selection) - .environment(\.extensionProfile, $extensionProfile) - .environment(\.logClient, $logClient) - - .environment(\.importProfile, $importProfile) - .environment(\.importRemoteProfile, $importRemoteProfile) - .handlesExternalEvents(preferring: [], allowing: ["*"]) - .onOpenURL(perform: openURL) + } + .formStyle(.grouped) + .environment(\.selection, $selection) + .environment(\.importProfile, $importProfile) + .environment(\.importRemoteProfile, $importRemoteProfile) + .handlesExternalEvents(preferring: [], allowing: ["*"]) + .onOpenURL(perform: openURL) } private func openURL(url: URL) { @@ -97,32 +81,6 @@ public struct MainView: View { } } - private func loadProfile() async { - defer { - profileLoading = false - } - if let newProfile = try? await ExtensionProfile.load() { - if extensionProfile == nil { - newProfile.register() - extensionProfile = newProfile - } - } else { - extensionProfile = nil - } - } - - private func connectLog() { - guard let profile = extensionProfile else { - return - } - guard let logClient else { - return - } - if profile.status.isConnected, !logClient.isConnected { - logClient.reconnect() - } - } - private func checkApplicationPath() { let directoryName = URL(filePath: Bundle.main.bundlePath).deletingLastPathComponent().pathComponents.last if directoryName != "Applications" { diff --git a/MacLibrary/MenuLabel.swift b/MacLibrary/MenuLabel.swift new file mode 100644 index 0000000..6275f5b --- /dev/null +++ b/MacLibrary/MenuLabel.swift @@ -0,0 +1,40 @@ +import Libbox +import Library +import SwiftUI + +public struct MenuLabel: View { + @EnvironmentObject private var environments: ExtensionEnvirnments + + public init() {} + public var body: some View { +// if let profile = environments.extensionProfile { +// MenuLabel0().environmentObject(profile) +// } else { +// } + } + + private struct MenuLabel0: View { + @EnvironmentObject private var environments: ExtensionEnvirnments + @EnvironmentObject private var extensionProfile: ExtensionProfile + @StateObject private var commandClient = CommandClient(.status) + + var body: some View { + HStack { + if extensionProfile.status.isConnectedStrict, let message = commandClient.status { + Image("MenuIcon") + Text(" ↑ \(LibboxFormatBytes(message.uplink))/s ↓ \(LibboxFormatBytes(message.downlink))/s") + } else { + Image("MenuIcon") + } + } + .onAppear { + commandClient.connect() + } + .onChangeCompat(of: extensionProfile.status) { newValue in + if newValue.isConnectedStrict { + commandClient.connect() + } + } + } + } +} diff --git a/MacLibrary/SidebarView.swift b/MacLibrary/SidebarView.swift index 262ca42..0aa1964 100644 --- a/MacLibrary/SidebarView.swift +++ b/MacLibrary/SidebarView.swift @@ -4,12 +4,14 @@ import SwiftUI public struct SidebarView: View { @Environment(\.selection) private var selection - @Environment(\.extensionProfile) private var extensionProfile + @EnvironmentObject private var environments: ExtensionEnvironments public init() {} public var body: some View { VStack { - if let profile = extensionProfile.wrappedValue { + if environments.extensionProfileLoading { + ProgressView() + } else if let profile = environments.extensionProfile { SidebarView0().environmentObject(profile) } else { SidebarView1() diff --git a/SFI/Application.swift b/SFI/Application.swift index 13f42e5..ec6c6ad 100644 --- a/SFI/Application.swift +++ b/SFI/Application.swift @@ -1,16 +1,16 @@ -import ApplicationLibrary import Foundation import Library import SwiftUI -import UIKit @main struct Application: App { @UIApplicationDelegateAdaptor private var appDelegate: ApplicationDelegate + @StateObject private var environments = ExtensionEnvironments() var body: some Scene { WindowGroup { MainView() + .environmentObject(environments) } } } diff --git a/SFI/MainView.swift b/SFI/MainView.swift index c844c0b..f4daa2b 100644 --- a/SFI/MainView.swift +++ b/SFI/MainView.swift @@ -4,49 +4,40 @@ import Library import SwiftUI struct MainView: View { - @Environment(\.scenePhase) var scenePhase + @Environment(\.scenePhase) private var scenePhase + @EnvironmentObject private var environments: ExtensionEnvironments @State private var selection = NavigationPage.dashboard - @State private var extensionProfile: ExtensionProfile? - @State private var profileLoading = true - @State private var logClient: LogClient! @State private var importProfile: LibboxProfileContent? @State private var importRemoteProfile: LibboxImportRemoteProfile? @State private var alert: Alert? var body: some View { - viewBuilder { - if profileLoading { - ProgressView().onAppear { - Task.detached { - logClient = LogClient(SharedPreferences.maxLogLines) - await loadProfile() - } - } - } else { - TabView(selection: $selection) { - ForEach(NavigationPage.allCases, id: \.self) { page in - NavigationStackCompat { - page.contentView - .navigationTitle(page.title) - } - .tag(page) - .tabItem { page.label } - } + TabView(selection: $selection) { + ForEach(NavigationPage.allCases, id: \.self) { page in + NavigationStackCompat { + page.contentView + .navigationTitle(page.title) } + .tag(page) + .tabItem { page.label } } } + .onAppear { + environments.postReload() + } .alertBinding($alert) .onChangeCompat(of: scenePhase) { newValue in if newValue == .active { - Task.detached { - await loadProfile() - } + environments.postReload() + } + } + .onChangeCompat(of: selection) { newValue in + if newValue == .logs { + environments.connectLog() } } .environment(\.selection, $selection) - .environment(\.extensionProfile, $extensionProfile) - .environment(\.logClient, $logClient) .environment(\.importProfile, $importProfile) .environment(\.importRemoteProfile, $importRemoteProfile) .handlesExternalEvents(preferring: [], allowing: ["*"]) @@ -80,33 +71,4 @@ struct MainView: View { alert = Alert(errorMessage: "Handled unknown URL \(url.absoluteString)") } } - - private func loadProfile() async { - defer { - profileLoading = false - } - if ApplicationLibrary.inPreview { - return - } - if let newProfile = try? await ExtensionProfile.load() { - if extensionProfile == nil || extensionProfile?.status == .invalid { - newProfile.register() - extensionProfile = newProfile - } - } else { - extensionProfile = nil - } - } - - private func connectLog() { - guard let profile = extensionProfile else { - return - } - guard let logClient else { - return - } - if profile.status.isConnected, !logClient.isConnected { - logClient.reconnect() - } - } } diff --git a/SFT/Application.swift b/SFT/Application.swift index 4d6348e..ec6c6ad 100644 --- a/SFT/Application.swift +++ b/SFT/Application.swift @@ -1,12 +1,16 @@ +import Foundation +import Library import SwiftUI @main struct Application: App { @UIApplicationDelegateAdaptor private var appDelegate: ApplicationDelegate + @StateObject private var environments = ExtensionEnvironments() var body: some Scene { WindowGroup { MainView() + .environmentObject(environments) } } } diff --git a/SFT/MainView.swift b/SFT/MainView.swift index 35bb820..88b4bf2 100644 --- a/SFT/MainView.swift +++ b/SFT/MainView.swift @@ -4,90 +4,35 @@ import Library import SwiftUI struct MainView: View { - @Environment(\.scenePhase) var scenePhase - + @Environment(\.scenePhase) private var scenePhase + @EnvironmentObject private var environments: ExtensionEnvironments @State private var selection = NavigationPage.dashboard - @State private var extensionProfile: ExtensionProfile? - @State private var profileLoading = true - @State private var logClient: LogClient! - @State private var importRemoteProfile: LibboxImportRemoteProfile? var body: some View { - viewBuilder { - if profileLoading { - ProgressView().onAppear { - Task.detached { - logClient = LogClient(SharedPreferences.maxLogLines) - await loadProfile() - } - } - } else { - TabView(selection: $selection) { - ForEach(NavigationPage.allCases, id: \.self) { page in - NavigationStackCompat { - page.contentView - .navigationTitle(page.title) - .focusSection() - } - .tag(page) - .tabItem { page.label } - } + TabView(selection: $selection) { + ForEach(NavigationPage.allCases, id: \.self) { page in + NavigationStackCompat { + page.contentView + .navigationTitle(page.title) + .focusSection() } + .tag(page) + .tabItem { page.label } } } + .onAppear { + environments.postReload() + } .onChangeCompat(of: scenePhase) { newValue in if newValue == .active { - Task.detached { - await loadProfile() - } + environments.postReload() + } + } + .onChangeCompat(of: selection) { newValue in + if newValue == .logs { + environments.connectLog() } } .environment(\.selection, $selection) - .environment(\.extensionProfile, $extensionProfile) - .environment(\.logClient, $logClient) - .environment(\.importRemoteProfile, $importRemoteProfile) - .onOpenURL(perform: openURL) - } - - private func openURL(url: URL) { - if url.host == "import-remote-profile" { - var error: NSError? - importRemoteProfile = LibboxParseRemoteProfileImportLink(url.absoluteString, &error) - if error != nil { - return - } - if selection != .profiles { - selection = .profiles - } - } - } - - private func loadProfile() async { - defer { - profileLoading = false - } - if ApplicationLibrary.inPreview { - return - } - if let newProfile = try? await ExtensionProfile.load() { - if extensionProfile == nil || extensionProfile?.status == .invalid { - newProfile.register() - extensionProfile = newProfile - } - } else { - extensionProfile = nil - } - } - - private func connectLog() { - guard let profile = extensionProfile else { - return - } - guard let logClient else { - return - } - if profile.status.isConnected, !logClient.isConnected { - logClient.reconnect() - } } } diff --git a/sing-box.xcodeproj/project.pbxproj b/sing-box.xcodeproj/project.pbxproj index 7197090..47a8ee9 100644 --- a/sing-box.xcodeproj/project.pbxproj +++ b/sing-box.xcodeproj/project.pbxproj @@ -20,6 +20,8 @@ 3A2223582A6E1CC700C50B23 /* MacApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2223572A6E1CC700C50B23 /* MacApplication.swift */; }; 3A22235A2A6E212A00C50B23 /* SystemExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2223592A6E212A00C50B23 /* SystemExtension.swift */; }; 3A251C122A52D09700651082 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3AAB5E7A2A4C1446009757F1 /* Assets.xcassets */; }; + 3A27D9002A89BE230031EBCC /* CommandClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A27D8FF2A89BE230031EBCC /* CommandClient.swift */; }; + 3A27D9022A89C6870031EBCC /* ExtensionEnvironments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A27D9012A89C6870031EBCC /* ExtensionEnvironments.swift */; }; 3A2EAEED2A6F4CBB00D00DE3 /* IndependentApplicationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2EAEEC2A6F4CBB00D00DE3 /* IndependentApplicationDelegate.swift */; }; 3A3AA7FC2A4EFDAE002F78AB /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; }; 3A3AA7FF2A4EFDB3002F78AB /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; }; @@ -44,7 +46,6 @@ 3A4EAD302A4FEB77005435B3 /* NewProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ADF8DF82A4AFCB400900CC8 /* NewProfileView.swift */; }; 3A4EAD312A4FEB7B005435B3 /* SettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAB5E712A4BF6F6009757F1 /* SettingView.swift */; }; 3A4EAD322A4FEB7B005435B3 /* ServiceLogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAB5E732A4BF90B009757F1 /* ServiceLogView.swift */; }; - 3A4EAD332A4FEB7F005435B3 /* LogClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA1ABBB2A4C4107000FD4BA /* LogClient.swift */; }; 3A4EAD342A4FEB7F005435B3 /* LogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA1ABB92A4C4054000FD4BA /* LogView.swift */; }; 3A4EAD352A4FEB9C005435B3 /* UIProfileUpdateTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A55F9572A4D137E003C4EF4 /* UIProfileUpdateTask.swift */; }; 3A4EAD362A4FEB9C005435B3 /* ProfileUpdateTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A55F9592A4D1554003C4EF4 /* ProfileUpdateTask.swift */; }; @@ -435,6 +436,8 @@ 3A2223552A6E1BDE00C50B23 /* Variant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Variant.swift; sourceTree = ""; }; 3A2223572A6E1CC700C50B23 /* MacApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacApplication.swift; sourceTree = ""; }; 3A2223592A6E212A00C50B23 /* SystemExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemExtension.swift; sourceTree = ""; }; + 3A27D8FF2A89BE230031EBCC /* CommandClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandClient.swift; sourceTree = ""; }; + 3A27D9012A89C6870031EBCC /* ExtensionEnvironments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionEnvironments.swift; sourceTree = ""; }; 3A2EAEEC2A6F4CBB00D00DE3 /* IndependentApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndependentApplicationDelegate.swift; sourceTree = ""; }; 3A3DEBE12A4FFA1A00373BF4 /* ExtensionFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ExtensionFoundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS17.0.sdk/System/Library/Frameworks/ExtensionFoundation.framework; sourceTree = DEVELOPER_DIR; }; 3A3DEBE62A4FFA6000373BF4 /* AppIntents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppIntents.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS17.0.sdk/System/Library/Frameworks/AppIntents.framework; sourceTree = DEVELOPER_DIR; }; @@ -467,7 +470,6 @@ 3A99B42B2A75288C0010D4B0 /* ViewCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewCompat.swift; sourceTree = ""; }; 3A99B42D2A752ABB0010D4B0 /* NavigationDestinationCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationDestinationCompat.swift; sourceTree = ""; }; 3AA1ABB92A4C4054000FD4BA /* LogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogView.swift; sourceTree = ""; }; - 3AA1ABBB2A4C4107000FD4BA /* LogClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogClient.swift; sourceTree = ""; }; 3AAB5E712A4BF6F6009757F1 /* SettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingView.swift; sourceTree = ""; }; 3AAB5E732A4BF90B009757F1 /* ServiceLogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceLogView.swift; sourceTree = ""; }; 3AAB5E752A4BFB0B009757F1 /* EditProfileContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfileContentView.swift; sourceTree = ""; }; @@ -711,7 +713,6 @@ isa = PBXGroup; children = ( 3AA1ABB92A4C4054000FD4BA /* LogView.swift */, - 3AA1ABBB2A4C4107000FD4BA /* LogClient.swift */, ); path = Log; sourceTree = ""; @@ -883,6 +884,8 @@ 3A4EAD3B2A4FECCE005435B3 /* NEVPNStatus+isConnected.swift */, 3A2223592A6E212A00C50B23 /* SystemExtension.swift */, 3AF342A62A4AA0FF002B34AC /* ExtensionPlatformInterface.swift */, + 3A27D8FF2A89BE230031EBCC /* CommandClient.swift */, + 3A27D9012A89C6870031EBCC /* ExtensionEnvironments.swift */, ); path = Network; sourceTree = ""; @@ -1446,7 +1449,6 @@ 3A4EAD2C2A4FEB77005435B3 /* EditProfileWindowView.swift in Sources */, 3A1CF2FA2A50F0BD000A8289 /* OutboundGroupItem.swift in Sources */, 3A99B42E2A752ABB0010D4B0 /* NavigationDestinationCompat.swift in Sources */, - 3A4EAD332A4FEB7F005435B3 /* LogClient.swift in Sources */, 3AC729F22A76088E00FE8EC1 /* ShareButton.swift in Sources */, 3A0C6D3C2A79D46500A4DF2B /* OverviewView.swift in Sources */, 3A4EAD262A4FEB65005435B3 /* ExtensionStatusView.swift in Sources */, @@ -1506,6 +1508,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3A27D9022A89C6870031EBCC /* ExtensionEnvironments.swift in Sources */, 3AE4D0B52A6E2BAC009FEA9E /* ExtensionProvider.swift in Sources */, 3A2223562A6E1BDE00C50B23 /* Variant.swift in Sources */, 3AEC214A2A45AA5600A63465 /* Profile+RW.swift in Sources */, @@ -1530,6 +1533,7 @@ 3AE4D0B22A6E2B6A009FEA9E /* ExtensionPlatformInterface.swift in Sources */, 3AEC21402A45A28F00A63465 /* ProfileManager.swift in Sources */, 3A9144D92A46AE370036E9AD /* ShadredPreferences+Database.swift in Sources */, + 3A27D9002A89BE230031EBCC /* CommandClient.swift in Sources */, 3AF342A02A4A9916002B34AC /* ExtensionProfile.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0;