punchnet-macos/punchnet/Views/Network/NetworkView.swift

367 lines
11 KiB
Swift

//
// NetworkView.swift
// punchnet
//
// Created by on 2026/1/16.
//
import SwiftUI
//
enum ConnectState {
case waitAuth
case connected
case disconnected
}
struct NetworkView: View {
@Environment(UserContext.self) var userContext: UserContext
@Environment(\.openWindow) private var openWindow
@State private var networkModel = NetworkModel()
@State private var showMode: ShowMode = .resource
//
@State private var connectState: ConnectState = .disconnected
@State private var isOn: Bool = false
@State private var vpnManager: VPNManager = VPNManager.shared
//
enum ShowMode {
case resource
case device
}
var body: some View {
VStack {
HStack {
VStack {
HStack(alignment: .center) {
Text(userContext.networkSession?.networkName ?? "未知")
Text(">")
Spacer()
}
HStack {
Toggle("", isOn: $isOn)
.toggleStyle(SwitchToggleStyle(tint: .green))
.disabled(true)
Text("已连接")
Spacer()
}
}
.frame(width: 320)
if self.connectState == .connected {
//
HStack {
Button {
self.showMode = .resource
} label: {
Text("资源")
}
Button {
self.showMode = .device
} label: {
Text("设备")
}
}
}
Spacer()
}
Group {
switch self.connectState {
case .waitAuth:
NetworkWaitAuthView(networkModel: self.networkModel)
case .connected:
Group {
switch self.showMode {
case .resource:
NetworkResourceGroupView(networkModel: self.networkModel)
case .device:
NetworkDeviceGroupView(networkModel: self.networkModel)
}
}
case .disconnected:
NetworkDisconnctedView(networkModel: self.networkModel)
}
}
Spacer()
}
.padding(.top, 10)
.padding(.leading, 10)
.onChange(of: vpnManager.vpnStatus) { _, newState in
NSLog("print view change: \(newState)")
switch newState {
case .connected:
self.connectState = .connected
self.isOn = true
case .disconnected:
self.connectState = .disconnected
self.isOn = false
}
}
.toolbar {
if self.connectState == .connected {
ToolbarItem(placement: .primaryAction) {
Button {
openWindow(id: "settings")
} label: {
Image(systemName: "gearshape")
}
}
}
}
}
}
//
struct NetworkDisconnctedView: View {
@Bindable var networkModel: NetworkModel
@Environment(AppContext.self) var appContext: AppContext
@Environment(UserContext.self) var userContext: UserContext
@State private var showAlert = false
@State private var errorMessage = ""
var body: some View {
ZStack {
Color.clear
VStack {
Button {
Task { @MainActor in
do {
try await self.connect()
try await self.startVpn()
} catch let err {
self.showAlert = true
self.errorMessage = err.localizedDescription
}
}
} label: {
Text("连接")
.font(.system(size: 14, weight: .regular))
.padding([.top, .bottom], 8)
.padding([.leading, .trailing], 30)
.foregroundColor(.white)
}
.background(Color(red: 74/255, green: 207/255, blue: 154/255))
.cornerRadius(5)
.frame(width: 120, height: 35)
Button {
Task {
try await VPNManager.shared.disableVpn()
}
} label: {
Text("关闭")
.font(.system(size: 14, weight: .regular))
.padding([.top, .bottom], 8)
.padding([.leading, .trailing], 30)
.foregroundColor(.white)
}
.background(Color(red: 74/255, green: 207/255, blue: 154/255))
.cornerRadius(5)
.frame(width: 120, height: 35)
}
}
.alert(isPresented: $showAlert) {
Alert(title: Text("提示"), message: Text(self.errorMessage))
}
}
private func connect() async throws {
guard let networkSession = userContext.networkSession else {
return
}
try await networkModel.connect(networkSession: networkSession)
}
//
private func startVpn() async throws {
guard let networkSession = userContext.networkSession else {
return
}
let networkContext = networkModel.networkContext
let options = SystemConfig.getOptions(networkId: UInt32(networkSession.networkId),
networkDomain: networkSession.networkDomain,
ip: networkContext.ip,
maskLen: networkContext.maskLen,
accessToken: networkSession.accessToken,
identityId: networkContext.identityId,
hostname: networkContext.hostname,
noticePort: self.appContext.noticePort)
// token使token
try await VPNManager.shared.enableVpn(options: options!)
}
}
//
//
struct NetworkResourceGroupView: View {
@Bindable var networkModel: NetworkModel
var body: some View {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 8) {
ForEach(self.networkModel.networkContext.resourceList, id: \.id) { resource in
NetworkResourceView(resource: resource)
}
}
}
}
struct NetworkResourceView: View {
var resource: Resource
var body: some View {
VStack {
HStack {
Text(resource.connectionStatus)
Text(resource.name)
.font(.system(size: 14, weight: .regular))
}
Text(resource.url)
.font(.system(size: 14, weight: .regular))
.padding(.leading, 30)
}
}
}
//
struct NetworkDeviceGroupView: View {
@Bindable var networkModel: NetworkModel
@State private var selectedId: Int?
var body: some View {
NavigationSplitView {
List(self.networkModel.networkContext.nodeList, id: \.id, selection: $selectedId) { node in
NetworkNodeHeadView(node: node)
}
.listStyle(.sidebar)
.onChange(of: selectedId) {
self.networkModel.changeSelectedNode(nodeId: selectedId)
}
.onAppear {
if selectedId == nil {
selectedId = self.networkModel.networkContext.nodeList.first?.id
}
}
} detail: {
NetworkNodeDetailView(node: networkModel.selectedNode)
}
}
}
struct NetworkNodeHeadView: View {
var node: Node
var body: some View {
VStack {
HStack {
Text(node.connectionStatus)
Text(node.name)
.font(.system(size: 14, weight: .regular))
}
Text(node.ip)
.font(.system(size: 14, weight: .regular))
.padding(.leading, 30)
}
}
}
struct NetworkNodeDetailView: View {
@Environment(UserContext.self) var userContext: UserContext
var node: Node?
@State private var resources: [Resource] = []
var body: some View {
Group {
if let node {
List {
Section {
HStack {
Text("连接状态")
Text("\(node.connectionStatus)")
Spacer()
}
HStack {
Text("虚拟IPv4")
Text("\(node.ip)")
Spacer()
}
HStack {
Text("操作系统")
Text(node.system ?? "未知")
Spacer()
}
}
Section("服务列表") {
ForEach(resources, id: \.id) { resource in
HStack {
Text("\(resource.name)")
Text("\(resource.url)")
}
}
}
}
.task(id: node.id) {
await self.loadNodeResources(id: node.id)
}
} else {
EmptyView()
}
}
}
private func loadNodeResources(id: Int) async {
guard let networkSession = userContext.networkSession else {
return
}
let params: [String: Any] = [
"client_id": SystemConfig.getClientId(),
"access_token": networkSession.accessToken,
"id": id
]
if let nodeDetail = try? await SDLAPIClient.doPost(path: "/get_node_resources", params: params, as: NodeDetail.self) {
self.resources = nodeDetail.resourceList
}
}
}
struct NetworkWaitAuthView: View {
@Bindable var networkModel: NetworkModel
var body: some View {
Color.clear
.overlay {
Text("等待确认中")
}
}
}
#Preview {
NetworkView()
}