From 39b10d707e99f73bb6f7370d52a9cee12cbfaabc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 12 Oct 2025 12:53:07 +0800 Subject: [PATCH] Refactor to use consolidated CommandClient --- .../Connections/ConnectionListView.swift | 10 +---- .../Connections/ConnectionListViewModel.swift | 17 ++++---- .../Views/Dashboard/ActiveDashboardView.swift | 25 +++++++++++ .../Dashboard/ActiveDashboardViewModel.swift | 1 + .../Views/Dashboard/ClashModeView.swift | 10 +---- .../Views/Dashboard/ClashModeViewModel.swift | 27 +++--------- .../Views/Dashboard/ExtensionStatusView.swift | 16 +------ .../Views/Groups/GroupListView.swift | 13 +----- .../Views/Groups/GroupListViewModel.swift | 15 +++---- Library/Network/CommandClient.swift | 43 +++++++++++-------- Library/Network/ExtensionProvider.swift | 1 + 11 files changed, 79 insertions(+), 99 deletions(-) diff --git a/ApplicationLibrary/Views/Connections/ConnectionListView.swift b/ApplicationLibrary/Views/Connections/ConnectionListView.swift index 9c79724..bb82232 100644 --- a/ApplicationLibrary/Views/Connections/ConnectionListView.swift +++ b/ApplicationLibrary/Views/Connections/ConnectionListView.swift @@ -3,7 +3,7 @@ import SwiftUI @MainActor public struct ConnectionListView: View { - @Environment(\.scenePhase) private var scenePhase + @EnvironmentObject private var commandClient: CommandClient @StateObject private var viewModel = ConnectionListViewModel() public init() {} @@ -56,18 +56,12 @@ public struct ConnectionListView: View { #endif .alertBinding($viewModel.alert) .onAppear { + viewModel.setCommandClient(commandClient) viewModel.connect() } .onDisappear { viewModel.disconnect() } - .onChangeCompat(of: scenePhase) { newValue in - if newValue == .active { - viewModel.connect() - } else { - viewModel.disconnect() - } - } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) #if os(iOS) .background(Color(uiColor: .systemGroupedBackground)) diff --git a/ApplicationLibrary/Views/Connections/ConnectionListViewModel.swift b/ApplicationLibrary/Views/Connections/ConnectionListViewModel.swift index 2a9c8e8..8fe043d 100644 --- a/ApplicationLibrary/Views/Connections/ConnectionListViewModel.swift +++ b/ApplicationLibrary/Views/Connections/ConnectionListViewModel.swift @@ -11,8 +11,8 @@ public class ConnectionListViewModel: ObservableObject { @Published public var alert: Alert? @Published public var connectionStateFilter: ConnectionStateFilter { didSet { - commandClient.connectionStateFilter = connectionStateFilter - commandClient.filterConnectionsNow() + commandClient?.connectionStateFilter = connectionStateFilter + commandClient?.filterConnectionsNow() saveStateFilterTask?.cancel() saveStateFilterTask = Task { await SharedPreferences.connectionStateFilter.set(connectionStateFilter.rawValue) @@ -22,8 +22,8 @@ public class ConnectionListViewModel: ObservableObject { @Published public var connectionSort: ConnectionSort { didSet { - commandClient.connectionSort = connectionSort - commandClient.filterConnectionsNow() + commandClient?.connectionSort = connectionSort + commandClient?.filterConnectionsNow() saveSortTask?.cancel() saveSortTask = Task { await SharedPreferences.connectionSort.set(connectionSort.rawValue) @@ -31,7 +31,7 @@ public class ConnectionListViewModel: ObservableObject { } } - private let commandClient = CommandClient(.connections) + private var commandClient: CommandClient? private var cancellables = Set() private var connectTask: Task? private var saveStateFilterTask: Task? @@ -40,8 +40,11 @@ public class ConnectionListViewModel: ObservableObject { public init() { connectionStateFilter = .active connectionSort = .byDate + } - commandClient.$connections + public func setCommandClient(_ client: CommandClient) { + commandClient = client + client.$connections .compactMap { $0 } .sink { [weak self] goConnections in self?.setConnections(goConnections) @@ -60,7 +63,6 @@ public class ConnectionListViewModel: ObservableObject { guard let self else { return } await self.loadPreferences() if Task.isCancelled { return } - self.commandClient.connect() self.connectTask = nil } } @@ -79,7 +81,6 @@ public class ConnectionListViewModel: ObservableObject { saveStateFilterTask = nil saveSortTask?.cancel() saveSortTask = nil - commandClient.disconnect() } public func closeAllConnections() { diff --git a/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift b/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift index f63a83c..43aacdf 100644 --- a/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift +++ b/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift @@ -74,6 +74,31 @@ public struct ActiveDashboardView: View { OverviewView($viewModel.profileList, $viewModel.selectedProfileID, $viewModel.systemProxyAvailable, $viewModel.systemProxyEnabled) #endif } + .environmentObject(viewModel.dashboardClient) + .onAppear { + if ApplicationLibrary.inPreview || profile.status.isConnected { + viewModel.dashboardClient.connect() + } + } + .onDisappear { + viewModel.dashboardClient.disconnect() + } + .onChangeCompat(of: scenePhase) { newPhase in + if newPhase == .active { + if profile.status.isConnected { + viewModel.dashboardClient.connect() + } + } else { + viewModel.dashboardClient.disconnect() + } + } + .onChangeCompat(of: profile.status) { newStatus in + if newStatus.isConnected { + viewModel.dashboardClient.connect() + } else { + viewModel.dashboardClient.disconnect() + } + } .onReceive(environments.profileUpdate) { _ in Task { await viewModel.reload() diff --git a/ApplicationLibrary/Views/Dashboard/ActiveDashboardViewModel.swift b/ApplicationLibrary/Views/Dashboard/ActiveDashboardViewModel.swift index bfd7cef..5e77ee5 100644 --- a/ApplicationLibrary/Views/Dashboard/ActiveDashboardViewModel.swift +++ b/ApplicationLibrary/Views/Dashboard/ActiveDashboardViewModel.swift @@ -12,6 +12,7 @@ final class ActiveDashboardViewModel: ObservableObject { @Published var selection = DashboardPage.overview @Published var systemProxyAvailable = false @Published var systemProxyEnabled = false + @Published var dashboardClient = CommandClient([.status, .groups, .clashMode, .connections]) var onEmptyProfilesChange: ((Bool) -> Void)? diff --git a/ApplicationLibrary/Views/Dashboard/ClashModeView.swift b/ApplicationLibrary/Views/Dashboard/ClashModeView.swift index fe13718..593c12c 100644 --- a/ApplicationLibrary/Views/Dashboard/ClashModeView.swift +++ b/ApplicationLibrary/Views/Dashboard/ClashModeView.swift @@ -4,7 +4,7 @@ import SwiftUI @MainActor public struct ClashModeView: View { - @Environment(\.scenePhase) private var scenePhase + @EnvironmentObject private var commandClient: CommandClient @StateObject private var viewModel = ClashModeViewModel() public init() {} @@ -29,13 +29,7 @@ public struct ClashModeView: View { } .padding([.leading, .trailing]) .onAppear { - viewModel.connect() - } - .onDisappear { - viewModel.disconnect() - } - .onChangeCompat(of: scenePhase) { newValue in - viewModel.handleScenePhase(newValue) + viewModel.setCommandClient(commandClient) } .alertBinding($viewModel.alert) } diff --git a/ApplicationLibrary/Views/Dashboard/ClashModeViewModel.swift b/ApplicationLibrary/Views/Dashboard/ClashModeViewModel.swift index 942ae94..27e1d49 100644 --- a/ApplicationLibrary/Views/Dashboard/ClashModeViewModel.swift +++ b/ApplicationLibrary/Views/Dashboard/ClashModeViewModel.swift @@ -7,37 +7,22 @@ final class ClashModeViewModel: ObservableObject { @Published var clashMode = "" @Published var alert: Alert? - private let commandClient = CommandClient(.clashMode) + var commandClient: CommandClient? var clashModeList: [String] { - commandClient.clashModeList + commandClient?.clashModeList ?? [] } var shouldShowPicker: Bool { - commandClient.clashModeList.count > 1 + (commandClient?.clashModeList.count ?? 0) > 1 } - init() { - commandClient.$clashMode + func setCommandClient(_ client: CommandClient) { + commandClient = client + client.$clashMode .assign(to: &$clashMode) } - func connect() { - commandClient.connect() - } - - func disconnect() { - commandClient.disconnect() - } - - func handleScenePhase(_ phase: ScenePhase) { - if phase == .active { - connect() - } else { - disconnect() - } - } - nonisolated func setClashMode(_ newMode: String) async { do { try LibboxNewStandaloneCommandClient()!.setClashMode(newMode) diff --git a/ApplicationLibrary/Views/Dashboard/ExtensionStatusView.swift b/ApplicationLibrary/Views/Dashboard/ExtensionStatusView.swift index 9f13fc8..368c2cf 100644 --- a/ApplicationLibrary/Views/Dashboard/ExtensionStatusView.swift +++ b/ApplicationLibrary/Views/Dashboard/ExtensionStatusView.swift @@ -6,8 +6,7 @@ import SwiftUI #endif public struct ExtensionStatusView: View { - @Environment(\.scenePhase) private var scenePhase - @StateObject private var commandClient = CommandClient(.status) + @EnvironmentObject private var commandClient: CommandClient @State private var columnCount: Int = 4 @State private var alert: Alert? @@ -92,19 +91,6 @@ public struct ExtensionStatusView: View { .frame(alignment: .topLeading) .padding([.top, .leading, .trailing]) } - .onAppear { - commandClient.connect() - } - .onDisappear { - commandClient.disconnect() - } - .onChangeCompat(of: scenePhase) { newValue in - if newValue == .active { - commandClient.connect() - } else { - commandClient.disconnect() - } - } .alertBinding($alert) } diff --git a/ApplicationLibrary/Views/Groups/GroupListView.swift b/ApplicationLibrary/Views/Groups/GroupListView.swift index 4bf0697..737c075 100644 --- a/ApplicationLibrary/Views/Groups/GroupListView.swift +++ b/ApplicationLibrary/Views/Groups/GroupListView.swift @@ -2,7 +2,7 @@ import Library import SwiftUI public struct GroupListView: View { - @Environment(\.scenePhase) private var scenePhase + @EnvironmentObject private var commandClient: CommandClient @StateObject private var viewModel = GroupListViewModel() public init() {} @@ -23,17 +23,8 @@ public struct GroupListView: View { } } .onAppear { + viewModel.setCommandClient(commandClient) viewModel.connect() } - .onDisappear { - viewModel.disconnect() - } - .onChangeCompat(of: scenePhase) { newValue in - if newValue == .active { - viewModel.connect() - } else { - viewModel.disconnect() - } - } } } diff --git a/ApplicationLibrary/Views/Groups/GroupListViewModel.swift b/ApplicationLibrary/Views/Groups/GroupListViewModel.swift index fc4fa6f..3a962fe 100644 --- a/ApplicationLibrary/Views/Groups/GroupListViewModel.swift +++ b/ApplicationLibrary/Views/Groups/GroupListViewModel.swift @@ -8,11 +8,14 @@ public class GroupListViewModel: ObservableObject { @Published public var isLoading = true @Published public var groups: [OutboundGroup] = [] - private let commandClient = CommandClient(.groups) + private var commandClient: CommandClient? private var cancellables = Set() - public init() { - commandClient.$groups + public init() {} + + public func setCommandClient(_ client: CommandClient) { + commandClient = client + client.$groups .compactMap { $0 } .sink { [weak self] goGroups in self?.setGroups(goGroups) @@ -34,15 +37,9 @@ public class GroupListViewModel: ObservableObject { }), ] isLoading = false - } else { - commandClient.connect() } } - public func disconnect() { - commandClient.disconnect() - } - private func setGroups(_ goGroups: [LibboxOutboundGroup]) { var groups = [OutboundGroup]() for goGroup in goGroups { diff --git a/Library/Network/CommandClient.swift b/Library/Network/CommandClient.swift index ac14f19..9528ed1 100644 --- a/Library/Network/CommandClient.swift +++ b/Library/Network/CommandClient.swift @@ -10,7 +10,7 @@ public class CommandClient: ObservableObject { case connections } - private let connectionType: ConnectionType + private let connectionTypes: [ConnectionType] private let logMaxLines: Int private var commandClient: LibboxCommandClient? private var connectTask: Task? @@ -27,8 +27,8 @@ public class CommandClient: ObservableObject { @Published public var connections: [LibboxConnection]? public var rawConnections: LibboxConnections? - public init(_ connectionType: ConnectionType, logMaxLines: Int = 300) { - self.connectionType = connectionType + public init(_ connectionTypes: [ConnectionType], logMaxLines: Int = 3000) { + self.connectionTypes = connectionTypes self.logMaxLines = logMaxLines logList = [] clashModeList = [] @@ -36,6 +36,10 @@ public class CommandClient: ObservableObject { isConnected = false } + public convenience init(_ connectionType: ConnectionType, logMaxLines: Int = 300) { + self.init([connectionType], logMaxLines: logMaxLines) + } + public func connect() { if isConnected { return @@ -94,27 +98,28 @@ public class CommandClient: ObservableObject { } private nonisolated func connect0() async { - if connectionType == .connections { + if connectionTypes.contains(.connections) { await initializeConnectionFilterState() } let clientOptions = LibboxCommandClientOptions() - switch connectionType { - case .status: - clientOptions.command = LibboxCommandStatus - case .groups: - clientOptions.command = LibboxCommandGroup - case .log: - clientOptions.command = LibboxCommandLog - case .clashMode: - clientOptions.command = LibboxCommandClashMode - case .connections: - clientOptions.command = LibboxCommandConnections + for connectionType in connectionTypes { + switch connectionType { + case .status: + clientOptions.addCommand(LibboxCommandStatus) + case .groups: + clientOptions.addCommand(LibboxCommandGroup) + case .log: + clientOptions.addCommand(LibboxCommandLog) + case .clashMode: + clientOptions.addCommand(LibboxCommandClashMode) + case .connections: + clientOptions.addCommand(LibboxCommandConnections) + } } - switch connectionType { - case .log: + if connectionTypes.contains(.log) { clientOptions.statusInterval = Int64(500 * NSEC_PER_MSEC) - default: + } else { clientOptions.statusInterval = Int64(NSEC_PER_SEC) } let client = LibboxNewCommandClient(clientHandler(self), clientOptions)! @@ -145,7 +150,7 @@ public class CommandClient: ObservableObject { func connected() { DispatchQueue.main.async { [self] in - if commandClient.connectionType == .log { + if commandClient.connectionTypes.contains(.log) { commandClient.logList = [] } commandClient.isConnected = true diff --git a/Library/Network/ExtensionProvider.swift b/Library/Network/ExtensionProvider.swift index 3a5fcd5..de2e76a 100644 --- a/Library/Network/ExtensionProvider.swift +++ b/Library/Network/ExtensionProvider.swift @@ -20,6 +20,7 @@ open class ExtensionProvider: NEPacketTunnelProvider { options.basePath = FilePath.sharedDirectory.relativePath options.workingPath = FilePath.workingDirectory.relativePath options.tempPath = FilePath.cacheDirectory.relativePath + options.logMaxLines = 3000 var setupError: NSError? LibboxSetup(options, &setupError) if let setupError {