import Libbox import Library import SwiftUI @MainActor public struct ConnectionListView: View { @Environment(\.scenePhase) private var scenePhase @State private var isLoading = true @StateObject private var commandClient = CommandClient(.connections) @State private var connections: [Connection] = [] @State private var searchText = "" @State private var alert: Alert? public init() {} public var body: some View { VStack { if isLoading { Text("Loading...") } else { if connections.isEmpty { Text("Empty connections") } else { ScrollView { LazyVGrid(columns: [GridItem(.flexible())], alignment: .leading) { ForEach(connections.filter { it in searchText == "" || it.performSearch(searchText) }, id: \.hashValue) { it in ConnectionView(it) } } } .padding() } } } .toolbar { ToolbarItem { Menu { Picker("State", selection: $commandClient.connectionStateFilter) { ForEach(ConnectionStateFilter.allCases) { state in Text(state.name) } } Picker("Sort By", selection: $commandClient.connectionSort) { ForEach(ConnectionSort.allCases, id: \.self) { sortBy in Text(sortBy.name) } } Button("Close All Connections", role: .destructive) { do { try LibboxNewStandaloneCommandClient()!.closeConnections() } catch { alert = Alert(error) } } } label: { Label("Filter", systemImage: "line.3.horizontal.circle") } } } #if os(macOS) .searchable(text: $searchText) #endif .alertBinding($alert) .onAppear { connect() } .onDisappear { commandClient.disconnect() } .onChangeCompat(of: scenePhase) { newValue in if newValue == .active { commandClient.connect() } else { commandClient.disconnect() } } .onChangeCompat(of: commandClient.connectionStateFilter) { it in commandClient.filterConnectionsNow() Task { await SharedPreferences.connectionStateFilter.set(it.rawValue) } } .onChangeCompat(of: commandClient.connectionSort) { it in commandClient.filterConnectionsNow() Task { await SharedPreferences.connectionSort.set(it.rawValue) } } .onReceive(commandClient.$connections, perform: { connections in if let connections { self.connections = convertConnections(connections) isLoading = false } }) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) #if os(iOS) .background(Color(uiColor: .systemGroupedBackground)) #endif } private var backgroundColor: Color { #if os(iOS) return Color(uiColor: .secondarySystemGroupedBackground) #elseif os(macOS) return Color(nsColor: .textBackgroundColor) #elseif os(tvOS) return Color(uiColor: .black) #endif } private func connect() { if ApplicationLibrary.inPreview { isLoading = false } else { commandClient.connect() } } private func convertConnections(_ goConnections: [LibboxConnection]) -> [Connection] { var connections = [Connection]() for goConnection in goConnections { if goConnection.outboundType == "dns" { continue } var closedAt: Date? if goConnection.closedAt > 0 { closedAt = Date(timeIntervalSince1970: Double(goConnection.closedAt) / 1000) } connections.append(Connection( id: goConnection.id_, inbound: goConnection.inbound, inboundType: goConnection.inboundType, ipVersion: goConnection.ipVersion, network: goConnection.network, source: goConnection.source, destination: goConnection.destination, domain: goConnection.domain, displayDestination: goConnection.displayDestination(), protocolName: goConnection.protocol, user: goConnection.user, fromOutbound: goConnection.fromOutbound, createdAt: Date(timeIntervalSince1970: Double(goConnection.createdAt) / 1000), closedAt: closedAt, upload: goConnection.uplink, download: goConnection.downlink, uploadTotal: goConnection.uplinkTotal, downloadTotal: goConnection.downlinkTotal, rule: goConnection.rule, outbound: goConnection.outbound, outboundType: goConnection.outboundType, chain: goConnection.chain()!.toArray() )) } return connections } }