Refactor to MVVM architecture
This commit is contained in:
@@ -1,22 +1,19 @@
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
public struct GroupListView: View {
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
@State private var isLoading = true
|
||||
@StateObject private var commandClient = CommandClient(.groups)
|
||||
@State private var groups: [OutboundGroup] = []
|
||||
@StateObject private var viewModel = GroupListViewModel()
|
||||
|
||||
public init() {}
|
||||
public var body: some View {
|
||||
VStack {
|
||||
if isLoading {
|
||||
if viewModel.isLoading {
|
||||
Text("Loading...")
|
||||
} else if !groups.isEmpty {
|
||||
} else if !viewModel.groups.isEmpty {
|
||||
ScrollView {
|
||||
VStack {
|
||||
ForEach(groups, id: \.hashValue) { it in
|
||||
ForEach(viewModel.groups, id: \.hashValue) { it in
|
||||
GroupView(it)
|
||||
}
|
||||
}.padding()
|
||||
@@ -26,56 +23,17 @@ public struct GroupListView: View {
|
||||
}
|
||||
}
|
||||
.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()
|
||||
}
|
||||
}
|
||||
.onReceive(commandClient.$groups, perform: { groups in
|
||||
if let groups {
|
||||
setGroups(groups)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func connect() {
|
||||
if ApplicationLibrary.inPreview {
|
||||
groups = [
|
||||
OutboundGroup(tag: "my_group", type: "selector", selected: "server", selectable: true, isExpand: true, items: [
|
||||
OutboundGroupItem(tag: "server", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: 12),
|
||||
OutboundGroupItem(tag: "server2", type: "WireGuard", urlTestTime: .now, urlTestDelay: 34),
|
||||
OutboundGroupItem(tag: "auto", type: "URLTest", urlTestTime: .now, urlTestDelay: 100),
|
||||
]),
|
||||
OutboundGroup(tag: "group2", type: "urltest", selected: "client", selectable: true, isExpand: false, items:
|
||||
(0 ..< 234).map { index in
|
||||
OutboundGroupItem(tag: "client\(index)", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: UInt16(100 + index * 10))
|
||||
}),
|
||||
]
|
||||
isLoading = false
|
||||
} else {
|
||||
commandClient.connect()
|
||||
}
|
||||
}
|
||||
|
||||
private func setGroups(_ goGroups: [LibboxOutboundGroup]) {
|
||||
var groups = [OutboundGroup]()
|
||||
for goGroup in goGroups {
|
||||
var items = [OutboundGroupItem]()
|
||||
let itemIterator = goGroup.getItems()!
|
||||
while itemIterator.hasNext() {
|
||||
let goItem = itemIterator.next()!
|
||||
items.append(OutboundGroupItem(tag: goItem.tag, type: goItem.type, urlTestTime: Date(timeIntervalSince1970: Double(goItem.urlTestTime)), urlTestDelay: UInt16(goItem.urlTestDelay)))
|
||||
}
|
||||
groups.append(OutboundGroup(tag: goGroup.tag, type: goGroup.type, selected: goGroup.selected, selectable: goGroup.selectable, isExpand: goGroup.isExpand, items: items))
|
||||
}
|
||||
self.groups = groups
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import Combine
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public class GroupListViewModel: ObservableObject {
|
||||
@Published public var isLoading = true
|
||||
@Published public var groups: [OutboundGroup] = []
|
||||
|
||||
private let commandClient = CommandClient(.groups)
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
public init() {
|
||||
commandClient.$groups
|
||||
.compactMap { $0 }
|
||||
.sink { [weak self] goGroups in
|
||||
self?.setGroups(goGroups)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
public func connect() {
|
||||
if ApplicationLibrary.inPreview {
|
||||
groups = [
|
||||
OutboundGroup(tag: "my_group", type: "selector", selected: "server", selectable: true, isExpand: true, items: [
|
||||
OutboundGroupItem(tag: "server", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: 12),
|
||||
OutboundGroupItem(tag: "server2", type: "WireGuard", urlTestTime: .now, urlTestDelay: 34),
|
||||
OutboundGroupItem(tag: "auto", type: "URLTest", urlTestTime: .now, urlTestDelay: 100),
|
||||
]),
|
||||
OutboundGroup(tag: "group2", type: "urltest", selected: "client", selectable: true, isExpand: false, items:
|
||||
(0 ..< 234).map { index in
|
||||
OutboundGroupItem(tag: "client\(index)", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: UInt16(100 + index * 10))
|
||||
}),
|
||||
]
|
||||
isLoading = false
|
||||
} else {
|
||||
commandClient.connect()
|
||||
}
|
||||
}
|
||||
|
||||
public func disconnect() {
|
||||
commandClient.disconnect()
|
||||
}
|
||||
|
||||
private func setGroups(_ goGroups: [LibboxOutboundGroup]) {
|
||||
var groups = [OutboundGroup]()
|
||||
for goGroup in goGroups {
|
||||
var items = [OutboundGroupItem]()
|
||||
let itemIterator = goGroup.getItems()!
|
||||
while itemIterator.hasNext() {
|
||||
let goItem = itemIterator.next()!
|
||||
items.append(OutboundGroupItem(tag: goItem.tag, type: goItem.type, urlTestTime: Date(timeIntervalSince1970: Double(goItem.urlTestTime)), urlTestDelay: UInt16(goItem.urlTestDelay)))
|
||||
}
|
||||
groups.append(OutboundGroup(tag: goGroup.tag, type: goGroup.type, selected: goGroup.selected, selectable: goGroup.selectable, isExpand: goGroup.isExpand, items: items))
|
||||
}
|
||||
self.groups = groups
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,31 @@
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public struct GroupView: View {
|
||||
@State private var group: OutboundGroup
|
||||
@StateObject private var viewModel: GroupViewModel
|
||||
@State private var geometryWidth: CGFloat = 300
|
||||
@State private var alert: Alert?
|
||||
|
||||
public init(_ group: OutboundGroup) {
|
||||
_group = State(initialValue: group)
|
||||
_viewModel = StateObject(wrappedValue: GroupViewModel(group: group))
|
||||
}
|
||||
|
||||
private var title: some View {
|
||||
HStack {
|
||||
Text(group.tag)
|
||||
Text(viewModel.group.tag)
|
||||
.font(.headline)
|
||||
Text(group.displayType)
|
||||
Text(viewModel.group.displayType)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
Text("\(group.items.count)")
|
||||
Text("\(viewModel.group.items.count)")
|
||||
.font(.subheadline)
|
||||
.padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4))
|
||||
.background(Color.gray.opacity(0.5))
|
||||
.cornerRadius(4)
|
||||
Button {
|
||||
group.isExpand = !group.isExpand
|
||||
Task {
|
||||
await setGroupExpand()
|
||||
}
|
||||
viewModel.toggleExpand()
|
||||
} label: {
|
||||
if group.isExpand {
|
||||
if viewModel.group.isExpand {
|
||||
Image(systemName: "arrow.down.to.line")
|
||||
} else {
|
||||
Image(systemName: "arrow.up.to.line")
|
||||
@@ -40,9 +35,7 @@ public struct GroupView: View {
|
||||
.buttonStyle(.plain)
|
||||
#endif
|
||||
Button {
|
||||
Task {
|
||||
await doURLTest()
|
||||
}
|
||||
viewModel.performURLTest()
|
||||
} label: {
|
||||
Image(systemName: "bolt.fill")
|
||||
}
|
||||
@@ -50,19 +43,19 @@ public struct GroupView: View {
|
||||
.buttonStyle(.plain)
|
||||
#endif
|
||||
}
|
||||
.alertBinding($alert)
|
||||
.alertBinding($viewModel.alert)
|
||||
.padding([.top, .bottom], 8)
|
||||
.animation(.easeInOut, value: group.isExpand)
|
||||
.animation(.easeInOut, value: viewModel.group.isExpand)
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
Section {
|
||||
if group.isExpand {
|
||||
if viewModel.group.isExpand {
|
||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()),
|
||||
count: explandColumnCount()))
|
||||
{
|
||||
ForEach(group.items, id: \.tag) { it in
|
||||
GroupItemView($group, it)
|
||||
ForEach(viewModel.group.items, id: \.tag) { it in
|
||||
GroupItemView($viewModel.group, it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -73,7 +66,7 @@ public struct GroupView: View {
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.fill(it.delayColor)
|
||||
if it.tag == group.selected {
|
||||
if it.tag == viewModel.group.selected {
|
||||
Rectangle()
|
||||
.fill(Color.white)
|
||||
#if !os(tvOS)
|
||||
@@ -120,9 +113,9 @@ public struct GroupView: View {
|
||||
count = Int(Int(geometryWidth) / 20)
|
||||
#endif
|
||||
if count == 0 {
|
||||
return [group.items]
|
||||
return [viewModel.group.items]
|
||||
} else {
|
||||
return group.items.chunked(
|
||||
return viewModel.group.items.chunked(
|
||||
into: count
|
||||
)
|
||||
}
|
||||
@@ -138,26 +131,6 @@ public struct GroupView: View {
|
||||
return standardCount < 1 ? 1 : standardCount
|
||||
#endif
|
||||
}
|
||||
|
||||
private nonisolated func doURLTest() async {
|
||||
do {
|
||||
try await LibboxNewStandaloneCommandClient()!.urlTest(group.tag)
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
alert = Alert(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated func setGroupExpand() async {
|
||||
do {
|
||||
try await LibboxNewStandaloneCommandClient()!.setGroupExpand(group.tag, isExpand: group.isExpand)
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
alert = Alert(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Array {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public class GroupViewModel: ObservableObject {
|
||||
@Published public var group: OutboundGroup
|
||||
@Published public var alert: Alert?
|
||||
|
||||
public init(group: OutboundGroup) {
|
||||
self.group = group
|
||||
}
|
||||
|
||||
public func toggleExpand() {
|
||||
group.isExpand = !group.isExpand
|
||||
Task {
|
||||
await setGroupExpand()
|
||||
}
|
||||
}
|
||||
|
||||
public func performURLTest() {
|
||||
Task {
|
||||
await doURLTest()
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated func doURLTest() async {
|
||||
do {
|
||||
try await LibboxNewStandaloneCommandClient()!.urlTest(group.tag)
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
alert = Alert(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated func setGroupExpand() async {
|
||||
do {
|
||||
try await LibboxNewStandaloneCommandClient()!.setGroupExpand(group.tag, isExpand: group.isExpand)
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
alert = Alert(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user