Refactor to use consolidated CommandClient

This commit is contained in:
世界
2025-11-26 22:25:48 +08:00
parent b71cbae382
commit 39b10d707e
11 changed files with 79 additions and 99 deletions
@@ -3,7 +3,7 @@ import SwiftUI
@MainActor @MainActor
public struct ConnectionListView: View { public struct ConnectionListView: View {
@Environment(\.scenePhase) private var scenePhase @EnvironmentObject private var commandClient: CommandClient
@StateObject private var viewModel = ConnectionListViewModel() @StateObject private var viewModel = ConnectionListViewModel()
public init() {} public init() {}
@@ -56,18 +56,12 @@ public struct ConnectionListView: View {
#endif #endif
.alertBinding($viewModel.alert) .alertBinding($viewModel.alert)
.onAppear { .onAppear {
viewModel.setCommandClient(commandClient)
viewModel.connect() viewModel.connect()
} }
.onDisappear { .onDisappear {
viewModel.disconnect() viewModel.disconnect()
} }
.onChangeCompat(of: scenePhase) { newValue in
if newValue == .active {
viewModel.connect()
} else {
viewModel.disconnect()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
#if os(iOS) #if os(iOS)
.background(Color(uiColor: .systemGroupedBackground)) .background(Color(uiColor: .systemGroupedBackground))
@@ -11,8 +11,8 @@ public class ConnectionListViewModel: ObservableObject {
@Published public var alert: Alert? @Published public var alert: Alert?
@Published public var connectionStateFilter: ConnectionStateFilter { @Published public var connectionStateFilter: ConnectionStateFilter {
didSet { didSet {
commandClient.connectionStateFilter = connectionStateFilter commandClient?.connectionStateFilter = connectionStateFilter
commandClient.filterConnectionsNow() commandClient?.filterConnectionsNow()
saveStateFilterTask?.cancel() saveStateFilterTask?.cancel()
saveStateFilterTask = Task { saveStateFilterTask = Task {
await SharedPreferences.connectionStateFilter.set(connectionStateFilter.rawValue) await SharedPreferences.connectionStateFilter.set(connectionStateFilter.rawValue)
@@ -22,8 +22,8 @@ public class ConnectionListViewModel: ObservableObject {
@Published public var connectionSort: ConnectionSort { @Published public var connectionSort: ConnectionSort {
didSet { didSet {
commandClient.connectionSort = connectionSort commandClient?.connectionSort = connectionSort
commandClient.filterConnectionsNow() commandClient?.filterConnectionsNow()
saveSortTask?.cancel() saveSortTask?.cancel()
saveSortTask = Task { saveSortTask = Task {
await SharedPreferences.connectionSort.set(connectionSort.rawValue) 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<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
private var connectTask: Task<Void, Never>? private var connectTask: Task<Void, Never>?
private var saveStateFilterTask: Task<Void, Never>? private var saveStateFilterTask: Task<Void, Never>?
@@ -40,8 +40,11 @@ public class ConnectionListViewModel: ObservableObject {
public init() { public init() {
connectionStateFilter = .active connectionStateFilter = .active
connectionSort = .byDate connectionSort = .byDate
}
commandClient.$connections public func setCommandClient(_ client: CommandClient) {
commandClient = client
client.$connections
.compactMap { $0 } .compactMap { $0 }
.sink { [weak self] goConnections in .sink { [weak self] goConnections in
self?.setConnections(goConnections) self?.setConnections(goConnections)
@@ -60,7 +63,6 @@ public class ConnectionListViewModel: ObservableObject {
guard let self else { return } guard let self else { return }
await self.loadPreferences() await self.loadPreferences()
if Task.isCancelled { return } if Task.isCancelled { return }
self.commandClient.connect()
self.connectTask = nil self.connectTask = nil
} }
} }
@@ -79,7 +81,6 @@ public class ConnectionListViewModel: ObservableObject {
saveStateFilterTask = nil saveStateFilterTask = nil
saveSortTask?.cancel() saveSortTask?.cancel()
saveSortTask = nil saveSortTask = nil
commandClient.disconnect()
} }
public func closeAllConnections() { public func closeAllConnections() {
@@ -74,6 +74,31 @@ public struct ActiveDashboardView: View {
OverviewView($viewModel.profileList, $viewModel.selectedProfileID, $viewModel.systemProxyAvailable, $viewModel.systemProxyEnabled) OverviewView($viewModel.profileList, $viewModel.selectedProfileID, $viewModel.systemProxyAvailable, $viewModel.systemProxyEnabled)
#endif #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 .onReceive(environments.profileUpdate) { _ in
Task { Task {
await viewModel.reload() await viewModel.reload()
@@ -12,6 +12,7 @@ final class ActiveDashboardViewModel: ObservableObject {
@Published var selection = DashboardPage.overview @Published var selection = DashboardPage.overview
@Published var systemProxyAvailable = false @Published var systemProxyAvailable = false
@Published var systemProxyEnabled = false @Published var systemProxyEnabled = false
@Published var dashboardClient = CommandClient([.status, .groups, .clashMode, .connections])
var onEmptyProfilesChange: ((Bool) -> Void)? var onEmptyProfilesChange: ((Bool) -> Void)?
@@ -4,7 +4,7 @@ import SwiftUI
@MainActor @MainActor
public struct ClashModeView: View { public struct ClashModeView: View {
@Environment(\.scenePhase) private var scenePhase @EnvironmentObject private var commandClient: CommandClient
@StateObject private var viewModel = ClashModeViewModel() @StateObject private var viewModel = ClashModeViewModel()
public init() {} public init() {}
@@ -29,13 +29,7 @@ public struct ClashModeView: View {
} }
.padding([.leading, .trailing]) .padding([.leading, .trailing])
.onAppear { .onAppear {
viewModel.connect() viewModel.setCommandClient(commandClient)
}
.onDisappear {
viewModel.disconnect()
}
.onChangeCompat(of: scenePhase) { newValue in
viewModel.handleScenePhase(newValue)
} }
.alertBinding($viewModel.alert) .alertBinding($viewModel.alert)
} }
@@ -7,37 +7,22 @@ final class ClashModeViewModel: ObservableObject {
@Published var clashMode = "" @Published var clashMode = ""
@Published var alert: Alert? @Published var alert: Alert?
private let commandClient = CommandClient(.clashMode) var commandClient: CommandClient?
var clashModeList: [String] { var clashModeList: [String] {
commandClient.clashModeList commandClient?.clashModeList ?? []
} }
var shouldShowPicker: Bool { var shouldShowPicker: Bool {
commandClient.clashModeList.count > 1 (commandClient?.clashModeList.count ?? 0) > 1
} }
init() { func setCommandClient(_ client: CommandClient) {
commandClient.$clashMode commandClient = client
client.$clashMode
.assign(to: &$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 { nonisolated func setClashMode(_ newMode: String) async {
do { do {
try LibboxNewStandaloneCommandClient()!.setClashMode(newMode) try LibboxNewStandaloneCommandClient()!.setClashMode(newMode)
@@ -6,8 +6,7 @@ import SwiftUI
#endif #endif
public struct ExtensionStatusView: View { public struct ExtensionStatusView: View {
@Environment(\.scenePhase) private var scenePhase @EnvironmentObject private var commandClient: CommandClient
@StateObject private var commandClient = CommandClient(.status)
@State private var columnCount: Int = 4 @State private var columnCount: Int = 4
@State private var alert: Alert? @State private var alert: Alert?
@@ -92,19 +91,6 @@ public struct ExtensionStatusView: View {
.frame(alignment: .topLeading) .frame(alignment: .topLeading)
.padding([.top, .leading, .trailing]) .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) .alertBinding($alert)
} }
@@ -2,7 +2,7 @@ import Library
import SwiftUI import SwiftUI
public struct GroupListView: View { public struct GroupListView: View {
@Environment(\.scenePhase) private var scenePhase @EnvironmentObject private var commandClient: CommandClient
@StateObject private var viewModel = GroupListViewModel() @StateObject private var viewModel = GroupListViewModel()
public init() {} public init() {}
@@ -23,17 +23,8 @@ public struct GroupListView: View {
} }
} }
.onAppear { .onAppear {
viewModel.setCommandClient(commandClient)
viewModel.connect() viewModel.connect()
} }
.onDisappear {
viewModel.disconnect()
}
.onChangeCompat(of: scenePhase) { newValue in
if newValue == .active {
viewModel.connect()
} else {
viewModel.disconnect()
}
}
} }
} }
@@ -8,11 +8,14 @@ public class GroupListViewModel: ObservableObject {
@Published public var isLoading = true @Published public var isLoading = true
@Published public var groups: [OutboundGroup] = [] @Published public var groups: [OutboundGroup] = []
private let commandClient = CommandClient(.groups) private var commandClient: CommandClient?
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
public init() { public init() {}
commandClient.$groups
public func setCommandClient(_ client: CommandClient) {
commandClient = client
client.$groups
.compactMap { $0 } .compactMap { $0 }
.sink { [weak self] goGroups in .sink { [weak self] goGroups in
self?.setGroups(goGroups) self?.setGroups(goGroups)
@@ -34,15 +37,9 @@ public class GroupListViewModel: ObservableObject {
}), }),
] ]
isLoading = false isLoading = false
} else {
commandClient.connect()
} }
} }
public func disconnect() {
commandClient.disconnect()
}
private func setGroups(_ goGroups: [LibboxOutboundGroup]) { private func setGroups(_ goGroups: [LibboxOutboundGroup]) {
var groups = [OutboundGroup]() var groups = [OutboundGroup]()
for goGroup in goGroups { for goGroup in goGroups {
+24 -19
View File
@@ -10,7 +10,7 @@ public class CommandClient: ObservableObject {
case connections case connections
} }
private let connectionType: ConnectionType private let connectionTypes: [ConnectionType]
private let logMaxLines: Int private let logMaxLines: Int
private var commandClient: LibboxCommandClient? private var commandClient: LibboxCommandClient?
private var connectTask: Task<Void, Error>? private var connectTask: Task<Void, Error>?
@@ -27,8 +27,8 @@ public class CommandClient: ObservableObject {
@Published public var connections: [LibboxConnection]? @Published public var connections: [LibboxConnection]?
public var rawConnections: LibboxConnections? public var rawConnections: LibboxConnections?
public init(_ connectionType: ConnectionType, logMaxLines: Int = 300) { public init(_ connectionTypes: [ConnectionType], logMaxLines: Int = 3000) {
self.connectionType = connectionType self.connectionTypes = connectionTypes
self.logMaxLines = logMaxLines self.logMaxLines = logMaxLines
logList = [] logList = []
clashModeList = [] clashModeList = []
@@ -36,6 +36,10 @@ public class CommandClient: ObservableObject {
isConnected = false isConnected = false
} }
public convenience init(_ connectionType: ConnectionType, logMaxLines: Int = 300) {
self.init([connectionType], logMaxLines: logMaxLines)
}
public func connect() { public func connect() {
if isConnected { if isConnected {
return return
@@ -94,27 +98,28 @@ public class CommandClient: ObservableObject {
} }
private nonisolated func connect0() async { private nonisolated func connect0() async {
if connectionType == .connections { if connectionTypes.contains(.connections) {
await initializeConnectionFilterState() await initializeConnectionFilterState()
} }
let clientOptions = LibboxCommandClientOptions() let clientOptions = LibboxCommandClientOptions()
switch connectionType { for connectionType in connectionTypes {
case .status: switch connectionType {
clientOptions.command = LibboxCommandStatus case .status:
case .groups: clientOptions.addCommand(LibboxCommandStatus)
clientOptions.command = LibboxCommandGroup case .groups:
case .log: clientOptions.addCommand(LibboxCommandGroup)
clientOptions.command = LibboxCommandLog case .log:
case .clashMode: clientOptions.addCommand(LibboxCommandLog)
clientOptions.command = LibboxCommandClashMode case .clashMode:
case .connections: clientOptions.addCommand(LibboxCommandClashMode)
clientOptions.command = LibboxCommandConnections case .connections:
clientOptions.addCommand(LibboxCommandConnections)
}
} }
switch connectionType { if connectionTypes.contains(.log) {
case .log:
clientOptions.statusInterval = Int64(500 * NSEC_PER_MSEC) clientOptions.statusInterval = Int64(500 * NSEC_PER_MSEC)
default: } else {
clientOptions.statusInterval = Int64(NSEC_PER_SEC) clientOptions.statusInterval = Int64(NSEC_PER_SEC)
} }
let client = LibboxNewCommandClient(clientHandler(self), clientOptions)! let client = LibboxNewCommandClient(clientHandler(self), clientOptions)!
@@ -145,7 +150,7 @@ public class CommandClient: ObservableObject {
func connected() { func connected() {
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
if commandClient.connectionType == .log { if commandClient.connectionTypes.contains(.log) {
commandClient.logList = [] commandClient.logList = []
} }
commandClient.isConnected = true commandClient.isConnected = true
+1
View File
@@ -20,6 +20,7 @@ open class ExtensionProvider: NEPacketTunnelProvider {
options.basePath = FilePath.sharedDirectory.relativePath options.basePath = FilePath.sharedDirectory.relativePath
options.workingPath = FilePath.workingDirectory.relativePath options.workingPath = FilePath.workingDirectory.relativePath
options.tempPath = FilePath.cacheDirectory.relativePath options.tempPath = FilePath.cacheDirectory.relativePath
options.logMaxLines = 3000
var setupError: NSError? var setupError: NSError?
LibboxSetup(options, &setupError) LibboxSetup(options, &setupError)
if let setupError { if let setupError {