Fix groups

This commit is contained in:
世界
2025-12-01 16:27:24 +08:00
parent 22de256d13
commit 30492c1191
10 changed files with 199 additions and 161 deletions
@@ -64,7 +64,11 @@ import SwiftUI
}.sheet(isPresented: $showCardManagement, onDismiss: { }.sheet(isPresented: $showCardManagement, onDismiss: {
cardConfigurationVersion += 1 cardConfigurationVersion += 1
}, content: { }, content: {
CardManagementSheet().presentationDetents([.large]).presentationDragIndicator(.visible) if #available(iOS 16.0, tvOS 17.0, *) {
CardManagementSheet().presentationDetents([.large]).presentationDragIndicator(.visible)
} else {
CardManagementSheet()
}
}) })
#endif #endif
.onAppear { .onAppear {
@@ -1,28 +1,25 @@
import Libbox
import Library import Library
import SwiftUI import SwiftUI
@MainActor @MainActor
public struct GroupItemView: View { public struct GroupItemView: View {
private let _group: Binding<OutboundGroup> @EnvironmentObject private var listViewModel: GroupListViewModel
private var group: OutboundGroup { @Binding private var group: OutboundGroup
_group.wrappedValue
private let itemTag: String
private var item: OutboundGroupItem {
group.items.first { $0.tag == itemTag }!
} }
private let item: OutboundGroupItem
public init(_ group: Binding<OutboundGroup>, _ item: OutboundGroupItem) { public init(_ group: Binding<OutboundGroup>, _ item: OutboundGroupItem) {
_group = group _group = group
self.item = item itemTag = item.tag
} }
@State private var alert: Alert?
public var body: some View { public var body: some View {
Button { Button {
if group.selectable, group.selected != item.tag { if group.selectable, group.selected != item.tag {
Task { listViewModel.selectOutbound(groupTag: group.tag, outboundTag: item.tag)
await selectOutbound()
}
} }
} label: { } label: {
HStack { HStack {
@@ -64,22 +61,6 @@ public struct GroupItemView: View {
.background(backgroundColor) .background(backgroundColor)
.cornerRadius(10) .cornerRadius(10)
#endif #endif
.alertBinding($alert)
}
private nonisolated func selectOutbound() async {
do {
try await LibboxNewStandaloneCommandClient()!.selectOutbound(group.tag, outboundTag: item.tag)
var newGroup = await group
newGroup.selected = await item.tag
await MainActor.run { [newGroup] in
_group.wrappedValue = newGroup
}
} catch {
await MainActor.run {
alert = Alert(error)
}
}
} }
private var backgroundColor: Color { private var backgroundColor: Color {
@@ -13,8 +13,8 @@ public struct GroupListView: View {
} else if !viewModel.groups.isEmpty { } else if !viewModel.groups.isEmpty {
ScrollView { ScrollView {
VStack { VStack {
ForEach(viewModel.groups, id: \.hashValue) { it in ForEach($viewModel.groups, id: \.tag) { $group in
GroupView(it) GroupView($group)
} }
}.padding() }.padding()
} }
@@ -22,6 +22,8 @@ public struct GroupListView: View {
Text("Empty groups") Text("Empty groups")
} }
} }
.environmentObject(viewModel)
.alertBinding($viewModel.alert)
.onAppear { .onAppear {
viewModel.connect() viewModel.connect()
} }
@@ -6,6 +6,9 @@ import SwiftUI
public class GroupListViewModel: ObservableObject { public class GroupListViewModel: ObservableObject {
@Published public var isLoading = true @Published public var isLoading = true
@Published public var groups: [OutboundGroup] = [] @Published public var groups: [OutboundGroup] = []
@Published public var alert: Alert?
private var pendingSelections: [String: String] = [:]
public init() {} public init() {}
@@ -28,17 +31,100 @@ public class GroupListViewModel: ObservableObject {
public func setGroups(_ goGroups: [LibboxOutboundGroup]?) { public func setGroups(_ goGroups: [LibboxOutboundGroup]?) {
guard let goGroups else { return } guard let goGroups else { return }
var groups = [OutboundGroup]()
let existingGroups = Dictionary(uniqueKeysWithValues: groups.map { ($0.tag, $0) })
var newGroups = [OutboundGroup]()
for goGroup in goGroups { for goGroup in goGroups {
var items = [OutboundGroupItem]() var items = [OutboundGroupItem]()
let itemIterator = goGroup.getItems()! let itemIterator = goGroup.getItems()!
while itemIterator.hasNext() { while itemIterator.hasNext() {
let goItem = itemIterator.next()! let goItem = itemIterator.next()!
items.append(OutboundGroupItem(tag: goItem.tag, type: goItem.type, urlTestTime: Date(timeIntervalSince1970: Double(goItem.urlTestTime)), urlTestDelay: UInt16(goItem.urlTestDelay))) 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))
var selected = goGroup.selected
if let pending = pendingSelections[goGroup.tag] {
if goGroup.selected == pending {
pendingSelections.removeValue(forKey: goGroup.tag)
} else {
selected = pending
}
}
let isExpand = existingGroups[goGroup.tag]?.isExpand ?? goGroup.isExpand
newGroups.append(OutboundGroup(
tag: goGroup.tag,
type: goGroup.type,
selected: selected,
selectable: goGroup.selectable,
isExpand: isExpand,
items: items
))
} }
self.groups = groups groups = newGroups
isLoading = false isLoading = false
} }
public func selectOutbound(groupTag: String, outboundTag: String) {
if let index = groups.firstIndex(where: { $0.tag == groupTag }) {
groups[index].selected = outboundTag
}
pendingSelections[groupTag] = outboundTag
Task {
await doSelectOutbound(groupTag: groupTag, outboundTag: outboundTag)
}
}
private nonisolated func doSelectOutbound(groupTag: String, outboundTag: String) async {
do {
try await LibboxNewStandaloneCommandClient()!.selectOutbound(groupTag, outboundTag: outboundTag)
} catch {
await MainActor.run {
alert = Alert(error)
}
}
}
public func toggleExpand(groupTag: String) {
guard let index = groups.firstIndex(where: { $0.tag == groupTag }) else { return }
groups[index].isExpand.toggle()
let isExpand = groups[index].isExpand
Task {
await setGroupExpand(tag: groupTag, isExpand: isExpand)
}
}
private nonisolated func setGroupExpand(tag: String, isExpand: Bool) async {
do {
try await LibboxNewStandaloneCommandClient()!.setGroupExpand(tag, isExpand: isExpand)
} catch {
await MainActor.run {
alert = Alert(error)
}
}
}
public func performURLTest(_ tag: String) {
Task {
await doURLTest(tag: tag)
}
}
private nonisolated func doURLTest(tag: String) async {
do {
try await LibboxNewStandaloneCommandClient()!.urlTest(tag)
} catch {
await MainActor.run {
alert = Alert(error)
}
}
}
} }
+17 -17
View File
@@ -3,29 +3,30 @@ import SwiftUI
@MainActor @MainActor
public struct GroupView: View { public struct GroupView: View {
@StateObject private var viewModel: GroupViewModel @EnvironmentObject private var listViewModel: GroupListViewModel
@Binding private var group: OutboundGroup
@State private var geometryWidth: CGFloat = 300 @State private var geometryWidth: CGFloat = 300
public init(_ group: OutboundGroup) { public init(_ group: Binding<OutboundGroup>) {
_viewModel = StateObject(wrappedValue: GroupViewModel(group: group)) _group = group
} }
private var title: some View { private var title: some View {
HStack { HStack {
Text(viewModel.group.tag) Text(group.tag)
.font(.headline) .font(.headline)
Text(viewModel.group.displayType) Text(group.displayType)
.font(.subheadline) .font(.subheadline)
.foregroundColor(.secondary) .foregroundColor(.secondary)
Text("\(viewModel.group.items.count)") Text("\(group.items.count)")
.font(.subheadline) .font(.subheadline)
.padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4)) .padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4))
.background(Color.gray.opacity(0.5)) .background(Color.gray.opacity(0.5))
.cornerRadius(4) .cornerRadius(4)
Button { Button {
viewModel.toggleExpand() listViewModel.toggleExpand(groupTag: group.tag)
} label: { } label: {
if viewModel.group.isExpand { if group.isExpand {
Image(systemName: "arrow.down.to.line") Image(systemName: "arrow.down.to.line")
} else { } else {
Image(systemName: "arrow.up.to.line") Image(systemName: "arrow.up.to.line")
@@ -35,7 +36,7 @@ public struct GroupView: View {
.buttonStyle(.plain) .buttonStyle(.plain)
#endif #endif
Button { Button {
viewModel.performURLTest() listViewModel.performURLTest(group.tag)
} label: { } label: {
Image(systemName: "bolt.fill") Image(systemName: "bolt.fill")
} }
@@ -43,19 +44,18 @@ public struct GroupView: View {
.buttonStyle(.plain) .buttonStyle(.plain)
#endif #endif
} }
.alertBinding($viewModel.alert)
.padding([.top, .bottom], 8) .padding([.top, .bottom], 8)
.animation(.easeInOut, value: viewModel.group.isExpand) .animation(.easeInOut, value: group.isExpand)
} }
public var body: some View { public var body: some View {
Section { Section {
if viewModel.group.isExpand { if group.isExpand {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), LazyVGrid(columns: Array(repeating: GridItem(.flexible()),
count: explandColumnCount())) count: explandColumnCount()))
{ {
ForEach(viewModel.group.items, id: \.tag) { it in ForEach(group.items, id: \.tag) { it in
GroupItemView($viewModel.group, it) GroupItemView($group, it)
} }
} }
} else { } else {
@@ -66,7 +66,7 @@ public struct GroupView: View {
ZStack { ZStack {
Rectangle() Rectangle()
.fill(it.delayColor) .fill(it.delayColor)
if it.tag == viewModel.group.selected { if it.tag == group.selected {
Rectangle() Rectangle()
.fill(Color.white) .fill(Color.white)
#if !os(tvOS) #if !os(tvOS)
@@ -113,9 +113,9 @@ public struct GroupView: View {
count = Int(Int(geometryWidth) / 20) count = Int(Int(geometryWidth) / 20)
#endif #endif
if count == 0 { if count == 0 {
return [viewModel.group.items] return [group.items]
} else { } else {
return viewModel.group.items.chunked( return group.items.chunked(
into: count into: count
) )
} }
@@ -1,46 +0,0 @@
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)
}
}
}
}
@@ -1,30 +0,0 @@
import Foundation
import Libbox
import SwiftUI
public struct OutboundGroup: Codable, Hashable {
let tag: String
let type: String
var selected: String
let selectable: Bool
var isExpand: Bool
let items: [OutboundGroupItem]
public func hash(into hasher: inout Hasher) {
hasher.combine(tag)
hasher.combine(selected)
for item in items {
hasher.combine(item.urlTestTime)
}
}
public static func == (lhs: OutboundGroup, rhs: OutboundGroup) -> Bool {
lhs.hashValue == rhs.hashValue
}
}
public extension OutboundGroup {
var displayType: String {
LibboxProxyDisplayType(type)
}
}
@@ -0,0 +1,17 @@
import Library
import SwiftUI
public extension OutboundGroupItem {
var delayColor: Color {
switch urlTestDelay {
case 0:
return .gray
case ..<800:
return .green
case 800 ..< 1500:
return .yellow
default:
return .orange
}
}
}
@@ -1,34 +0,0 @@
import Foundation
import Libbox
import SwiftUI
public struct OutboundGroupItem: Codable {
public let tag: String
public let type: String
public let urlTestTime: Date
public let urlTestDelay: UInt16
}
public extension OutboundGroupItem {
var displayType: String {
LibboxProxyDisplayType(type)
}
var delayString: String {
"\(urlTestDelay)ms"
}
var delayColor: Color {
switch urlTestDelay {
case 0:
return .gray
case ..<800:
return .green
case 800 ..< 1500:
return .yellow
default:
return .orange
}
}
}
+58
View File
@@ -0,0 +1,58 @@
import Foundation
import Libbox
public struct OutboundGroup: Codable, Hashable {
public let tag: String
public let type: String
public var selected: String
public let selectable: Bool
public var isExpand: Bool
public var items: [OutboundGroupItem]
public init(tag: String, type: String, selected: String, selectable: Bool, isExpand: Bool, items: [OutboundGroupItem]) {
self.tag = tag
self.type = type
self.selected = selected
self.selectable = selectable
self.isExpand = isExpand
self.items = items
}
public func hash(into hasher: inout Hasher) {
hasher.combine(tag)
hasher.combine(selected)
for item in items {
hasher.combine(item.urlTestTime)
}
}
public static func == (lhs: OutboundGroup, rhs: OutboundGroup) -> Bool {
lhs.hashValue == rhs.hashValue
}
public var displayType: String {
LibboxProxyDisplayType(type)
}
}
public struct OutboundGroupItem: Codable, Hashable {
public let tag: String
public let type: String
public let urlTestTime: Date
public let urlTestDelay: UInt16
public init(tag: String, type: String, urlTestTime: Date, urlTestDelay: UInt16) {
self.tag = tag
self.type = type
self.urlTestTime = urlTestTime
self.urlTestDelay = urlTestDelay
}
public var displayType: String {
LibboxProxyDisplayType(type)
}
public var delayString: String {
"\(urlTestDelay)ms"
}
}