Fix groups
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user