add swiftUI code

This commit is contained in:
zeus
2025-01-22 14:09:10 +08:00
parent 68e7b7347c
commit 8a99853829
2531 changed files with 486215 additions and 0 deletions
@@ -0,0 +1,280 @@
import Libbox
import Network
import Library
import SwiftUI
@MainActor
public struct GroupItemView: View {
private let _group: Binding<OutboundGroup>
private var group: OutboundGroup {
_group.wrappedValue
}
private let item: OutboundGroupItem
public init(_ group: Binding<OutboundGroup>, _ item: OutboundGroupItem) {
_group = group
self.item = item
}
@State private var alert: Alert?
/* old style
public var body: some View {
Button {
if group.selectable, group.selected != item.tag {
Task {
await selectOutbound()
UserDefaults.standard.setValue("\(item.tag)", forKey: "selectedNode")
}
}
} label: {
HStack {
VStack {
HStack {
Text(item.tag)
.font(.subheadline)
.foregroundStyle(.foreground)
.lineLimit(1)
.truncationMode(.tail)
Spacer(minLength: 0)
}
Spacer(minLength: 8)
HStack {
Text(item.displayType)
.font(.caption)
.foregroundColor(.secondary)
Spacer(minLength: 0)
}
}
Spacer(minLength: 0)
VStack {
if group.selected == item.tag {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(Color.accentColor)
}
Spacer(minLength: 0)
if item.urlTestDelay > 0 {
Text(item.delayString)
.font(.caption)
.foregroundColor(item.delayColor)
}
}
}
}
#if !os(tvOS)
.buttonStyle(.borderless)
.padding(EdgeInsets(top: 10, leading: 13, bottom: 10, trailing: 13))
.background(backgroundColor)
.cornerRadius(10)
#endif
.alertBinding($alert)
}
*/
func testPing(to server: String, port: Int, completion: @escaping (TimeInterval?) -> Void) {
let startTime = Date()
let connection = NWConnection(host: NWEndpoint.Host(server), port: NWEndpoint.Port(rawValue: UInt16(port))!, using: .tcp)
connection.stateUpdateHandler = { state in
switch state {
case .ready:
//
let pingTime = Date().timeIntervalSince(startTime)
completion(pingTime)
connection.cancel() //
case .failed(_):
completion(nil)
connection.cancel()
default:
break
}
}
//
connection.start(queue: .global())
}
public var body: some View {
Button {
print("\(group.selectable) group.selected - item.tag: >>> "+group.selected + " " + item.tag)
// print(item.toString)
if group.selectable, group.selected != item.tag {
// var item = _group.wrappedValue.items[index] // Make a mutable copy of the item
Task {
if let server = item.server ,let port = item.server_port{
//
testPing(to: server, port: port) { pingTime in
if let ping = pingTime {
print("Ping 时间: \(ping)")
Task{
await pingOutbound(tag: item.tag,pingtime: UInt16((pingTime ?? 0)*1000))
}
} else {
print("连接超时")
}
}
UserDefaults.standard.setValue("\(item.tag)", forKey: "selectedNode")
await selectOutbound()
}else{
await selectOutbound()
UserDefaults.standard.setValue("\(item.tag)", forKey: "selectedNode")
//
await restartVPNServer()
}
}
}
} label: {
VStack(spacing: 2) {
HStack {
VStack(alignment: .leading, spacing: 8) {
HStack {
// Image(server.name)
// .resizable()
// .aspectRatio(contentMode: .fit)
// .frame(width: 20, height: 20)
Text(item.tag)
.font(.subheadline)
.fontWeight(.semibold)
.foregroundColor(.white)
}
Label {
Text(item.type)
// Text(item.urlTestDelay <= 3000 ? "线" : "")
} icon: {
// Image(systemName: item.urlTestDelay <= 3000 ? "checkmark" : "xmark")
} .foregroundColor(.gray)
// .foregroundColor(item.urlTestDelay <= 3000 ?.green : .red)
.font(.caption2)
}
Spacer(minLength: 10)
// Change Server Button...
VStack {
if group.selected == item.tag {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(Color.accentColor)
}
Spacer(minLength: 0)
if item.urlTestDelay > 0 {
Text(item.delayString)
.font(.caption)
.foregroundColor(item.delayColor)
}else{
//
Text("...")
.font(.caption)
.foregroundColor(.gray)
}
}
}
.frame(height: 50)
.padding(.horizontal)
Divider()
}
}
#if !os(tvOS)
.buttonStyle(.borderless)
// .padding(EdgeInsets(top: 10, leading: 13, bottom: 10, trailing: 13))
// .background(backgroundColor)
// .cornerRadius(10)
#endif
.alertBinding($alert)
}
private nonisolated func pingOutbound(tag: String,pingtime: UInt16) async {
let newGroup = await group
let newitems = newGroup.items.map { item in
var mutableItem = item
if (item.tag == tag) {
mutableItem.urlTestDelay = pingtime
}
return mutableItem
}
let newOut = OutboundGroup(tag: newGroup.tag, type: newGroup.type, selected:tag, selectable: newGroup.selectable, isExpand: newGroup.isExpand, items: newitems)
await MainActor.run { [newOut] in
_group.wrappedValue = newOut
}
}
private nonisolated func selectOutbound() async {
do {
try await LibboxNewStandaloneCommandClient()!.selectOutbound(group.tag, outboundTag: item.tag)
var newGroup = await group
newGroup.selected = item.tag
await MainActor.run { [newGroup] in
_group.wrappedValue = newGroup
}
} catch {
await MainActor.run {
alert = Alert(error)
}
}
}
private nonisolated func setSystemProxyEnabled(_ isEnabled: Bool) async {
do {
try LibboxNewStandaloneCommandClient()!.setSystemProxyEnabled(isEnabled)
await SharedPreferences.systemProxyEnabled.set(isEnabled)
} catch {
await MainActor.run {
alert = Alert(error)
}
}
}
private nonisolated func restartVPNServer() async {
do {
try LibboxNewStandaloneCommandClient()!.serviceReload()
} catch {
await MainActor.run {
alert = Alert(error)
}
}
}
private var backgroundColor: Color {
#if os(iOS)
return Color(uiColor: .secondarySystemGroupedBackground)
#elseif os(macOS)
return Color(nsColor: .textBackgroundColor)
#elseif os(tvOS)
return Color.black
#endif
}
}
@@ -0,0 +1,124 @@
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] = []
public init() {
}
public var body: some View {
VStack {
if isLoading {
Spacer().frame(height: 20)
Text("请开启VPN后查看节点数据").foregroundColor(.red)
} else if !groups.isEmpty {
ScrollView {
VStack {
ForEach(groups, id: \.hashValue) { it in
GroupView(it)
}
}.padding() //.background(Color.gray.opacity(0.1))
}
} else {
Spacer().frame(height: 20)
ProgressView()
}
}
.onAppear {
connect()
}
.onDisappear {
commandClient.disconnect()
}
.onChangeCompat(of: scenePhase) { newValue in
if newValue == .active {
commandClient.connect()
} else {
commandClient.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 {
#if targetEnvironment(simulator)
groups = [
OutboundGroup(tag: "my_group", type: "selector", selected: "server", selectable: true, isExpand: true, items: [
OutboundGroupItem(tag: "🇭🇰🇭🇰Hong Kong 01", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: 12),
OutboundGroupItem(tag: "🇭🇰🇭🇰Hong Kong 03", type: "WireGuard", urlTestTime: .now, urlTestDelay: 34),
OutboundGroupItem(tag: "🇭🇰🇭🇰Hong Kong 04", type: "URLTest", urlTestTime: .now, urlTestDelay: 100),
OutboundGroupItem(tag: "🇯🇵🇯🇵🇯🇵Japan 01", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: 12),
OutboundGroupItem(tag: "🇯🇵🇯🇵Japan Kong 01", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: 112),
OutboundGroupItem(tag: "🇯🇵🇯🇵Japan Kong 02", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: 1222),
OutboundGroupItem(tag: "🇯🇵🇯🇵Japan Kong 03", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: 4412),
OutboundGroupItem(tag: "🇯🇵🇯🇵Japan Kong 04", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: 512),
]),
]
isLoading = false
#endif
commandClient.connect()
}
}
private func setGroups(_ goGroups: [LibboxOutboundGroup]) {
var groups = [OutboundGroup]()
if let goGroup = goGroups.first {
goGroup.isExpand = true
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))
UserDefaults.standard.setValue("\(goGroup.tag)", forKey: "goGrouptag")
}
/*
var groups = [OutboundGroup]()
for goGroup in goGroups {
print(goGroup.tag)
//
goGroup.isExpand = true
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,176 @@
import Libbox
import Library
import SwiftUI
@MainActor
public struct GroupView: View {
@State private var group: OutboundGroup
@State private var geometryWidth: CGFloat = 300
@State private var alert: Alert?
public init(_ group: OutboundGroup) {
_group = State(initialValue: group)
}
private var title: some View {
HStack {
// Text(group.tag)
// .font(.headline)
//Text()
Text(group.tag)
.font(.subheadline)
.foregroundColor(.secondary)
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 {
group.isExpand = !group.isExpand
Task {
await setGroupExpand()
}
} label: {
HStack{
if group.isExpand {
Image(systemName: "arrow.down.to.line")
Text("收起").font(.subheadline)
} else {
Image(systemName: "arrow.up.to.line")
Text("展开").font(.subheadline)
}
}
}
#if os(macOS) || os(tvOS)
.buttonStyle(.plain)
#endif
Button {
Task {
await doURLTest()
}
} label: {
Image(systemName: "bolt.fill")
Text("测速").font(.subheadline)
}
#if os(macOS) || os(tvOS)
.buttonStyle(.plain)
#endif
}
.alertBinding($alert)
.padding([.top, .bottom], 10)
}
public var body: some View {
Section {
if group.isExpand {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()),
count: explandColumnCount()))
{
ForEach(group.items, id: \.tag) { it in
GroupItemView($group, it)
}
}
} else {
VStack(spacing: 5) {
ForEach(Array(itemGroups.enumerated()), id: \.offset) { items in
HStack(spacing: 5) {
ForEach(items.element, id: \.tag) { it in
ZStack {
Rectangle()
.fill(it.delayColor)
if it.tag == group.selected {
Rectangle()
.fill(Color.white)
#if !os(tvOS)
.frame(width: 5, height: 5)
#else
.frame(width: 15, height: 15)
#endif
}
}
#if !os(tvOS)
.frame(width: 10, height: 10)
#else
.frame(width: 30, height: 30)
#endif
}
}.frame(maxWidth: .infinity, alignment: .topLeading)
}
}
}
} header: {
//title .frame(maxWidth: .infinity, alignment: .topLeading)
}
.background {
/* GeometryReader { geometry in
Rectangle()
.fill(.clear)
.frame(height: 1)
.onChangeCompat(of: geometry.size.width) { newValue in
geometryWidth = newValue
}
.onAppear {
geometryWidth = geometry.size.width
}
}.padding() */
}.padding(.leading,-18.0)
}
private var itemGroups: [[OutboundGroupItem]] {
let count: Int
#if os(tvOS)
count = Int(Int(geometryWidth) / 40)
#else
count = Int(Int(geometryWidth) / 20)
#endif
if count == 0 {
return [group.items]
} else {
return group.items.chunked(
into: count
)
}
}
private func explandColumnCount() -> Int {
let standardCount = Int(Int(geometryWidth) / 180)
#if os(iOS)
return standardCount < 2 ? 1 : standardCount
#elseif os(tvOS)
return 4
#else
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 {
func chunked(into size: Int) -> [[Element]] {
stride(from: 0, to: count, by: size).map {
Array(self[$0 ..< Swift.min($0 + size, count)])
}
}
}
@@ -0,0 +1,12 @@
//
// NodesListView.swift
// ApplicationLibrary
//
// Created by Mac on 2024/10/26.
//
import Foundation
import SwiftUI
//NodesListView
@@ -0,0 +1,55 @@
import Foundation
import Libbox
import SwiftUI
public struct OutboundGroup: Codable {
let tag: String
let type: String
var selected: String
let selectable: Bool
var isExpand: Bool
let items: [OutboundGroupItem]
var hashValue: Int {
var value = tag.hashValue
(value, _) = value.addingReportingOverflow(selected.hashValue)
for item in items {
(value, _) = value.addingReportingOverflow(item.urlTestTime.hashValue)
}
return value
}
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 extension OutboundGroup {
var newhashValue: Int {
var value = tag.hashValue
(value, _) = value.addingReportingOverflow(selected.hashValue)
for item in items {
(value, _) = value.addingReportingOverflow(item.urlTestTime.hashValue)
}
return value
}
}
public extension OutboundGroup {
var displayType: String {
LibboxProxyDisplayType(type)
}
}
@@ -0,0 +1,63 @@
import Foundation
import Libbox
import SwiftUI
public struct OutboundGroupItem: Codable {
public let tag: String
public let type: String
public let urlTestTime: Date
public var urlTestDelay: UInt16
public var server: String? = ""
public var server_port: Int? = 0
public var method: String? = ""
public var password: String? = ""
//server: String?,server_port: String?,method: String?,serverpassword String?
//
public init(tag: String, type: String, urlTestTime: Date, urlTestDelay: UInt16) {
self.tag = tag
self.type = type
self.urlTestTime = urlTestTime
self.urlTestDelay = urlTestDelay
}
}
public extension OutboundGroupItem {
mutating func setpingms(pingDelay: UInt16){
urlTestDelay = pingDelay
}
var toString: String {
"\(tag) - \(type) - \(server ?? "") - \(server_port ?? 0) - \(method ?? "") - \(password ?? "") "
}
var displayType: String {
LibboxProxyDisplayType(type)
}
var delayString: String {
"\(urlTestDelay)ms"
}
var delayColor: Color {
switch urlTestDelay {
case 0:
return .gray
case ..<400:
return .green
case ..<800:
return .yellow
case 800 ..< 1500:
return .orange
default:
return .red
}
}
}