Refactor to MVVM architecture
This commit is contained in:
@@ -1,30 +1,23 @@
|
||||
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?
|
||||
@StateObject private var viewModel = ConnectionListViewModel()
|
||||
|
||||
public init() {}
|
||||
public var body: some View {
|
||||
VStack {
|
||||
if isLoading {
|
||||
if viewModel.isLoading {
|
||||
Text("Loading...")
|
||||
} else {
|
||||
if connections.isEmpty {
|
||||
if viewModel.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
|
||||
ForEach(viewModel.filteredConnections(), id: \.hashValue) { it in
|
||||
ConnectionView(it)
|
||||
}
|
||||
}
|
||||
@@ -37,24 +30,20 @@ public struct ConnectionListView: View {
|
||||
.toolbar {
|
||||
ToolbarItem {
|
||||
Menu {
|
||||
Picker("State", selection: $commandClient.connectionStateFilter) {
|
||||
Picker("State", selection: $viewModel.connectionStateFilter) {
|
||||
ForEach(ConnectionStateFilter.allCases) { state in
|
||||
Text(state.name)
|
||||
}
|
||||
}
|
||||
|
||||
Picker("Sort By", selection: $commandClient.connectionSort) {
|
||||
Picker("Sort By", selection: $viewModel.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)
|
||||
}
|
||||
viewModel.closeAllConnections()
|
||||
}
|
||||
} label: {
|
||||
Label("Filter", systemImage: "line.3.horizontal.circle")
|
||||
@@ -63,40 +52,22 @@ public struct ConnectionListView: View {
|
||||
}
|
||||
#endif
|
||||
#if os(macOS)
|
||||
.searchable(text: $searchText)
|
||||
.searchable(text: $viewModel.searchText)
|
||||
#endif
|
||||
.alertBinding($alert)
|
||||
.alertBinding($viewModel.alert)
|
||||
.onAppear {
|
||||
connect()
|
||||
viewModel.connect()
|
||||
}
|
||||
.onDisappear {
|
||||
commandClient.disconnect()
|
||||
viewModel.disconnect()
|
||||
}
|
||||
.onChangeCompat(of: scenePhase) { newValue in
|
||||
if newValue == .active {
|
||||
commandClient.connect()
|
||||
viewModel.connect()
|
||||
} else {
|
||||
commandClient.disconnect()
|
||||
viewModel.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))
|
||||
@@ -112,50 +83,4 @@ public struct ConnectionListView: View {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import Combine
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public class ConnectionListViewModel: ObservableObject {
|
||||
@Published public var isLoading = true
|
||||
@Published public var connections: [Connection] = []
|
||||
@Published public var searchText = ""
|
||||
@Published public var alert: Alert?
|
||||
@Published public var connectionStateFilter: ConnectionStateFilter {
|
||||
didSet {
|
||||
commandClient.connectionStateFilter = connectionStateFilter
|
||||
commandClient.filterConnectionsNow()
|
||||
saveStateFilterTask?.cancel()
|
||||
saveStateFilterTask = Task {
|
||||
await SharedPreferences.connectionStateFilter.set(connectionStateFilter.rawValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published public var connectionSort: ConnectionSort {
|
||||
didSet {
|
||||
commandClient.connectionSort = connectionSort
|
||||
commandClient.filterConnectionsNow()
|
||||
saveSortTask?.cancel()
|
||||
saveSortTask = Task {
|
||||
await SharedPreferences.connectionSort.set(connectionSort.rawValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let commandClient = CommandClient(.connections)
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var connectTask: Task<Void, Never>?
|
||||
private var saveStateFilterTask: Task<Void, Never>?
|
||||
private var saveSortTask: Task<Void, Never>?
|
||||
|
||||
public init() {
|
||||
connectionStateFilter = .active
|
||||
connectionSort = .byDate
|
||||
|
||||
commandClient.$connections
|
||||
.compactMap { $0 }
|
||||
.sink { [weak self] goConnections in
|
||||
self?.setConnections(goConnections)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
public func connect() {
|
||||
if ApplicationLibrary.inPreview {
|
||||
isLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
connectTask?.cancel()
|
||||
connectTask = Task { @MainActor [weak self] in
|
||||
guard let self else { return }
|
||||
await self.loadPreferences()
|
||||
if Task.isCancelled { return }
|
||||
self.commandClient.connect()
|
||||
self.connectTask = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func loadPreferences() async {
|
||||
let filter = await ConnectionStateFilter(rawValue: SharedPreferences.connectionStateFilter.get()) ?? .active
|
||||
let sort = await ConnectionSort(rawValue: SharedPreferences.connectionSort.get()) ?? .byDate
|
||||
connectionStateFilter = filter
|
||||
connectionSort = sort
|
||||
}
|
||||
|
||||
public func disconnect() {
|
||||
connectTask?.cancel()
|
||||
connectTask = nil
|
||||
saveStateFilterTask?.cancel()
|
||||
saveStateFilterTask = nil
|
||||
saveSortTask?.cancel()
|
||||
saveSortTask = nil
|
||||
commandClient.disconnect()
|
||||
}
|
||||
|
||||
public func closeAllConnections() {
|
||||
do {
|
||||
try LibboxNewStandaloneCommandClient()!.closeConnections()
|
||||
} catch {
|
||||
alert = Alert(error)
|
||||
}
|
||||
}
|
||||
|
||||
public func filteredConnections() -> [Connection] {
|
||||
connections.filter { connection in
|
||||
searchText == "" || connection.performSearch(searchText)
|
||||
}
|
||||
}
|
||||
|
||||
private func setConnections(_ goConnections: [LibboxConnection]) {
|
||||
connections = convertConnections(goConnections)
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user