From 30492c1191fe400186e4e2b9b3d32d6157bfad52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 1 Dec 2025 16:27:24 +0800 Subject: [PATCH] Fix groups --- .../Views/Dashboard/ActiveDashboardView.swift | 6 +- .../Views/Groups/GroupItemView.swift | 35 ++----- .../Views/Groups/GroupListView.swift | 6 +- .../Views/Groups/GroupListViewModel.swift | 94 ++++++++++++++++++- .../Views/Groups/GroupView.swift | 34 +++---- .../Views/Groups/GroupViewModel.swift | 46 --------- .../Views/Groups/OutboundGroup.swift | 30 ------ .../Groups/OutboundGroupItem+SwiftUI.swift | 17 ++++ .../Views/Groups/OutboundGroupItem.swift | 34 ------- Library/Network/OutboundGroup.swift | 58 ++++++++++++ 10 files changed, 199 insertions(+), 161 deletions(-) delete mode 100644 ApplicationLibrary/Views/Groups/GroupViewModel.swift delete mode 100644 ApplicationLibrary/Views/Groups/OutboundGroup.swift create mode 100644 ApplicationLibrary/Views/Groups/OutboundGroupItem+SwiftUI.swift delete mode 100644 ApplicationLibrary/Views/Groups/OutboundGroupItem.swift create mode 100644 Library/Network/OutboundGroup.swift diff --git a/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift b/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift index 0a54644..87e5b92 100644 --- a/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift +++ b/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift @@ -64,7 +64,11 @@ import SwiftUI }.sheet(isPresented: $showCardManagement, onDismiss: { cardConfigurationVersion += 1 }, content: { - CardManagementSheet().presentationDetents([.large]).presentationDragIndicator(.visible) + if #available(iOS 16.0, tvOS 17.0, *) { + CardManagementSheet().presentationDetents([.large]).presentationDragIndicator(.visible) + } else { + CardManagementSheet() + } }) #endif .onAppear { diff --git a/ApplicationLibrary/Views/Groups/GroupItemView.swift b/ApplicationLibrary/Views/Groups/GroupItemView.swift index 65f8f17..d55b540 100644 --- a/ApplicationLibrary/Views/Groups/GroupItemView.swift +++ b/ApplicationLibrary/Views/Groups/GroupItemView.swift @@ -1,28 +1,25 @@ -import Libbox import Library import SwiftUI @MainActor public struct GroupItemView: View { - private let _group: Binding - private var group: OutboundGroup { - _group.wrappedValue + @EnvironmentObject private var listViewModel: GroupListViewModel + @Binding private var group: OutboundGroup + + private let itemTag: String + private var item: OutboundGroupItem { + group.items.first { $0.tag == itemTag }! } - private let item: OutboundGroupItem public init(_ group: Binding, _ item: OutboundGroupItem) { _group = group - self.item = item + itemTag = item.tag } - @State private var alert: Alert? - public var body: some View { Button { if group.selectable, group.selected != item.tag { - Task { - await selectOutbound() - } + listViewModel.selectOutbound(groupTag: group.tag, outboundTag: item.tag) } } label: { HStack { @@ -64,22 +61,6 @@ public struct GroupItemView: View { .background(backgroundColor) .cornerRadius(10) #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 { diff --git a/ApplicationLibrary/Views/Groups/GroupListView.swift b/ApplicationLibrary/Views/Groups/GroupListView.swift index e066490..5a76aa8 100644 --- a/ApplicationLibrary/Views/Groups/GroupListView.swift +++ b/ApplicationLibrary/Views/Groups/GroupListView.swift @@ -13,8 +13,8 @@ public struct GroupListView: View { } else if !viewModel.groups.isEmpty { ScrollView { VStack { - ForEach(viewModel.groups, id: \.hashValue) { it in - GroupView(it) + ForEach($viewModel.groups, id: \.tag) { $group in + GroupView($group) } }.padding() } @@ -22,6 +22,8 @@ public struct GroupListView: View { Text("Empty groups") } } + .environmentObject(viewModel) + .alertBinding($viewModel.alert) .onAppear { viewModel.connect() } diff --git a/ApplicationLibrary/Views/Groups/GroupListViewModel.swift b/ApplicationLibrary/Views/Groups/GroupListViewModel.swift index 9a2f133..70b01b5 100644 --- a/ApplicationLibrary/Views/Groups/GroupListViewModel.swift +++ b/ApplicationLibrary/Views/Groups/GroupListViewModel.swift @@ -6,6 +6,9 @@ import SwiftUI public class GroupListViewModel: ObservableObject { @Published public var isLoading = true @Published public var groups: [OutboundGroup] = [] + @Published public var alert: Alert? + + private var pendingSelections: [String: String] = [:] public init() {} @@ -28,17 +31,100 @@ public class GroupListViewModel: ObservableObject { public func setGroups(_ goGroups: [LibboxOutboundGroup]?) { guard let goGroups else { return } - var groups = [OutboundGroup]() + + let existingGroups = Dictionary(uniqueKeysWithValues: groups.map { ($0.tag, $0) }) + + var newGroups = [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))) + 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 } + + 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) + } + } + } } diff --git a/ApplicationLibrary/Views/Groups/GroupView.swift b/ApplicationLibrary/Views/Groups/GroupView.swift index aaf2f9b..75aa3e1 100644 --- a/ApplicationLibrary/Views/Groups/GroupView.swift +++ b/ApplicationLibrary/Views/Groups/GroupView.swift @@ -3,29 +3,30 @@ import SwiftUI @MainActor 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 - public init(_ group: OutboundGroup) { - _viewModel = StateObject(wrappedValue: GroupViewModel(group: group)) + public init(_ group: Binding) { + _group = group } private var title: some View { HStack { - Text(viewModel.group.tag) + Text(group.tag) .font(.headline) - Text(viewModel.group.displayType) + Text(group.displayType) .font(.subheadline) .foregroundColor(.secondary) - Text("\(viewModel.group.items.count)") + Text("\(group.items.count)") .font(.subheadline) .padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4)) .background(Color.gray.opacity(0.5)) .cornerRadius(4) Button { - viewModel.toggleExpand() + listViewModel.toggleExpand(groupTag: group.tag) } label: { - if viewModel.group.isExpand { + if group.isExpand { Image(systemName: "arrow.down.to.line") } else { Image(systemName: "arrow.up.to.line") @@ -35,7 +36,7 @@ public struct GroupView: View { .buttonStyle(.plain) #endif Button { - viewModel.performURLTest() + listViewModel.performURLTest(group.tag) } label: { Image(systemName: "bolt.fill") } @@ -43,19 +44,18 @@ public struct GroupView: View { .buttonStyle(.plain) #endif } - .alertBinding($viewModel.alert) .padding([.top, .bottom], 8) - .animation(.easeInOut, value: viewModel.group.isExpand) + .animation(.easeInOut, value: group.isExpand) } public var body: some View { Section { - if viewModel.group.isExpand { + if group.isExpand { LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: explandColumnCount())) { - ForEach(viewModel.group.items, id: \.tag) { it in - GroupItemView($viewModel.group, it) + ForEach(group.items, id: \.tag) { it in + GroupItemView($group, it) } } } else { @@ -66,7 +66,7 @@ public struct GroupView: View { ZStack { Rectangle() .fill(it.delayColor) - if it.tag == viewModel.group.selected { + if it.tag == group.selected { Rectangle() .fill(Color.white) #if !os(tvOS) @@ -113,9 +113,9 @@ public struct GroupView: View { count = Int(Int(geometryWidth) / 20) #endif if count == 0 { - return [viewModel.group.items] + return [group.items] } else { - return viewModel.group.items.chunked( + return group.items.chunked( into: count ) } diff --git a/ApplicationLibrary/Views/Groups/GroupViewModel.swift b/ApplicationLibrary/Views/Groups/GroupViewModel.swift deleted file mode 100644 index a6268cd..0000000 --- a/ApplicationLibrary/Views/Groups/GroupViewModel.swift +++ /dev/null @@ -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) - } - } - } -} diff --git a/ApplicationLibrary/Views/Groups/OutboundGroup.swift b/ApplicationLibrary/Views/Groups/OutboundGroup.swift deleted file mode 100644 index 5ca4245..0000000 --- a/ApplicationLibrary/Views/Groups/OutboundGroup.swift +++ /dev/null @@ -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) - } -} diff --git a/ApplicationLibrary/Views/Groups/OutboundGroupItem+SwiftUI.swift b/ApplicationLibrary/Views/Groups/OutboundGroupItem+SwiftUI.swift new file mode 100644 index 0000000..ad01af2 --- /dev/null +++ b/ApplicationLibrary/Views/Groups/OutboundGroupItem+SwiftUI.swift @@ -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 + } + } +} diff --git a/ApplicationLibrary/Views/Groups/OutboundGroupItem.swift b/ApplicationLibrary/Views/Groups/OutboundGroupItem.swift deleted file mode 100644 index bdceb5a..0000000 --- a/ApplicationLibrary/Views/Groups/OutboundGroupItem.swift +++ /dev/null @@ -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 - } - } -} diff --git a/Library/Network/OutboundGroup.swift b/Library/Network/OutboundGroup.swift new file mode 100644 index 0000000..feb3e80 --- /dev/null +++ b/Library/Network/OutboundGroup.swift @@ -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" + } +}