fix views
This commit is contained in:
parent
46baa8f9a7
commit
c5b2cb3e83
@ -415,3 +415,17 @@ final class NetworkModel {
|
|||||||
return "\(context.identityId)-\(context.ip)"
|
return "\(context.identityId)-\(context.ip)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension NetworkModel {
|
||||||
|
struct ExitNodeOption: Identifiable, Equatable {
|
||||||
|
let id: Int
|
||||||
|
let nodeName: String
|
||||||
|
let ip: String
|
||||||
|
let system: String?
|
||||||
|
|
||||||
|
var nodeNameWithIp: String {
|
||||||
|
"\(nodeName) (\(ip))"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@ -35,7 +35,7 @@ struct NetworkConnectedView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 设备组视图 (NavigationSplitView)
|
// MARK: - 设备组视图 (NavigationSplitView)
|
||||||
struct NetworkDeviceGroupView: View {
|
private struct NetworkDeviceGroupView: View {
|
||||||
var model: NetworkModel
|
var model: NetworkModel
|
||||||
|
|
||||||
// 侧边栏宽度
|
// 侧边栏宽度
|
||||||
@ -90,7 +90,7 @@ struct NetworkDeviceGroupView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 子组件
|
// MARK: - 子组件
|
||||||
struct NetworkNodeHeadView: View {
|
private struct NetworkNodeHeadView: View {
|
||||||
var node: NetworkContext.Node
|
var node: NetworkContext.Node
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -112,7 +112,7 @@ struct NetworkNodeHeadView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NetworkNodeDetailView: View {
|
private struct NetworkNodeDetailView: View {
|
||||||
var model: NetworkModel
|
var model: NetworkModel
|
||||||
var node: NetworkContext.Node
|
var node: NetworkContext.Node
|
||||||
|
|
||||||
@ -152,7 +152,7 @@ struct NetworkNodeDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ResourceItemCard: View {
|
private struct ResourceItemCard: View {
|
||||||
let resource: NetworkContext.Resource
|
let resource: NetworkContext.Resource
|
||||||
@State private var isHovered = false
|
@State private var isHovered = false
|
||||||
|
|
||||||
|
|||||||
@ -1,157 +0,0 @@
|
|||||||
//
|
|
||||||
// NetworkStatusBar.swift
|
|
||||||
// punchnet
|
|
||||||
//
|
|
||||||
// Created by 安礼成 on 2026/4/17.
|
|
||||||
//
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct NetworkStatusBar: View {
|
|
||||||
var model: NetworkModel
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
let isOnBinding = Binding(
|
|
||||||
get: { self.model.isTunnelEnabled },
|
|
||||||
set: { newValue in
|
|
||||||
Task { @MainActor in
|
|
||||||
await self.model.setConnectionEnabled(newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
HStack(spacing: 12) {
|
|
||||||
// 左侧:状态指示器与文字
|
|
||||||
HStack(spacing: 20) {
|
|
||||||
ZStack {
|
|
||||||
Circle()
|
|
||||||
.fill(self.model.isTunnelEnabled ? Color.green.opacity(0.15) : Color.primary.opacity(0.05))
|
|
||||||
.frame(width: 36, height: 36)
|
|
||||||
|
|
||||||
Image(systemName: self.model.isTunnelEnabled ? "checkmark.shield.fill" : "shield.slash.fill")
|
|
||||||
.symbolRenderingMode(.hierarchical)
|
|
||||||
.foregroundStyle(self.model.isTunnelEnabled ? Color.green : Color.secondary)
|
|
||||||
.font(.system(size: 16))
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 1) {
|
|
||||||
if let networkSession = self.model.networkSession {
|
|
||||||
Text(networkSession.networkName)
|
|
||||||
.font(.system(size: 12, weight: .semibold))
|
|
||||||
|
|
||||||
Text("局域网IP: \(self.model.networkContext?.ip ?? "0.0.0.0")")
|
|
||||||
.font(.system(size: 10, design: .monospaced))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
} else {
|
|
||||||
Text("未登录网络")
|
|
||||||
.font(.system(size: 12, weight: .semibold))
|
|
||||||
|
|
||||||
Text("登录后可建立连接")
|
|
||||||
.font(.system(size: 10))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.model.networkSession != nil {
|
|
||||||
exitNodeMenu
|
|
||||||
}
|
|
||||||
|
|
||||||
// 右侧:Switch 开关
|
|
||||||
// 注意:这里使用 Binding 手动接管连接/断开逻辑
|
|
||||||
Toggle("", isOn: isOnBinding)
|
|
||||||
.toggleStyle(.switch)
|
|
||||||
.controlSize(.small) // macOS 顶部栏或面板推荐使用 small 尺寸
|
|
||||||
.disabled(self.model.phase == .connecting || self.model.phase == .disconnecting || self.model.networkSession == nil)
|
|
||||||
}
|
|
||||||
.padding(.vertical, 5)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var exitNodeMenu: some View {
|
|
||||||
Menu {
|
|
||||||
Button {
|
|
||||||
Task { @MainActor in
|
|
||||||
await self.model.updateExitNodeSelection(nil)
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
if self.model.selectedExitNode == nil {
|
|
||||||
Label("不设置出口节点", systemImage: "checkmark")
|
|
||||||
} else {
|
|
||||||
Text("不设置出口节点")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.model.exitNodeOptions.isEmpty {
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
ForEach(self.model.exitNodeOptions) { option in
|
|
||||||
Button {
|
|
||||||
Task { @MainActor in
|
|
||||||
await self.model.updateExitNodeSelection(option.ip)
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
if self.model.selectedExitNode?.ip == option.ip {
|
|
||||||
Label(option.nodeNameWithIp, systemImage: "checkmark")
|
|
||||||
} else {
|
|
||||||
Text(option.nodeNameWithIp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
HStack(spacing: 10) {
|
|
||||||
VStack(alignment: .leading, spacing: 3) {
|
|
||||||
Text("出口节点")
|
|
||||||
.font(.system(size: 10, weight: .medium))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
|
|
||||||
Text(self.model.exitNodeTitle)
|
|
||||||
.font(.system(size: 12, weight: .semibold))
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
.lineLimit(1)
|
|
||||||
|
|
||||||
Text(self.model.exitNodeSubtitle)
|
|
||||||
.font(.system(size: 10, design: .monospaced))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.lineLimit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(minLength: 0)
|
|
||||||
|
|
||||||
if self.model.isUpdatingExitNode {
|
|
||||||
ProgressView()
|
|
||||||
.controlSize(.small)
|
|
||||||
} else {
|
|
||||||
Image(systemName: "chevron.down")
|
|
||||||
.font(.system(size: 10, weight: .semibold))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
.frame(width: 220, alignment: .leading)
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
||||||
.fill(Color.primary.opacity(0.04))
|
|
||||||
)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
||||||
.stroke(Color.primary.opacity(0.06), lineWidth: 1)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.disabled(!self.model.canSelectExitNode)
|
|
||||||
.opacity(self.model.canSelectExitNode ? 1 : 0.7)
|
|
||||||
.help(self.model.exitNodeHelpText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ExitNodeOption: Identifiable, Equatable {
|
|
||||||
let id: Int
|
|
||||||
let nodeName: String
|
|
||||||
let ip: String
|
|
||||||
let system: String?
|
|
||||||
|
|
||||||
var nodeNameWithIp: String {
|
|
||||||
"\(nodeName) (\(ip))"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -100,5 +100,141 @@ struct NetworkView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct NetworkStatusBar: View {
|
||||||
|
var model: NetworkModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let isOnBinding = Binding(
|
||||||
|
get: { self.model.isTunnelEnabled },
|
||||||
|
set: { newValue in
|
||||||
|
Task { @MainActor in
|
||||||
|
await self.model.setConnectionEnabled(newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
// 左侧:状态指示器与文字
|
||||||
|
HStack(spacing: 20) {
|
||||||
|
ZStack {
|
||||||
|
Circle()
|
||||||
|
.fill(self.model.isTunnelEnabled ? Color.green.opacity(0.15) : Color.primary.opacity(0.05))
|
||||||
|
.frame(width: 36, height: 36)
|
||||||
|
|
||||||
|
Image(systemName: self.model.isTunnelEnabled ? "checkmark.shield.fill" : "shield.slash.fill")
|
||||||
|
.symbolRenderingMode(.hierarchical)
|
||||||
|
.foregroundStyle(self.model.isTunnelEnabled ? Color.green : Color.secondary)
|
||||||
|
.font(.system(size: 16))
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 1) {
|
||||||
|
if let networkSession = self.model.networkSession {
|
||||||
|
Text(networkSession.networkName)
|
||||||
|
.font(.system(size: 12, weight: .semibold))
|
||||||
|
|
||||||
|
Text("局域网IP: \(self.model.networkContext?.ip ?? "0.0.0.0")")
|
||||||
|
.font(.system(size: 10, design: .monospaced))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
} else {
|
||||||
|
Text("未登录网络")
|
||||||
|
.font(.system(size: 12, weight: .semibold))
|
||||||
|
|
||||||
|
Text("登录后可建立连接")
|
||||||
|
.font(.system(size: 10))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.model.networkSession != nil {
|
||||||
|
exitNodeMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右侧:Switch 开关
|
||||||
|
// 注意:这里使用 Binding 手动接管连接/断开逻辑
|
||||||
|
Toggle("", isOn: isOnBinding)
|
||||||
|
.toggleStyle(.switch)
|
||||||
|
.controlSize(.small) // macOS 顶部栏或面板推荐使用 small 尺寸
|
||||||
|
.disabled(self.model.phase == .connecting || self.model.phase == .disconnecting || self.model.networkSession == nil)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var exitNodeMenu: some View {
|
||||||
|
Menu {
|
||||||
|
Button {
|
||||||
|
Task { @MainActor in
|
||||||
|
await self.model.updateExitNodeSelection(nil)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
if self.model.selectedExitNode == nil {
|
||||||
|
Label("不设置出口节点", systemImage: "checkmark")
|
||||||
|
} else {
|
||||||
|
Text("不设置出口节点")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.model.exitNodeOptions.isEmpty {
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
ForEach(self.model.exitNodeOptions) { option in
|
||||||
|
Button {
|
||||||
|
Task { @MainActor in
|
||||||
|
await self.model.updateExitNodeSelection(option.ip)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
if self.model.selectedExitNode?.ip == option.ip {
|
||||||
|
Label(option.nodeNameWithIp, systemImage: "checkmark")
|
||||||
|
} else {
|
||||||
|
Text(option.nodeNameWithIp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
|
Text("出口节点")
|
||||||
|
.font(.system(size: 10, weight: .medium))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
|
Text(self.model.exitNodeTitle)
|
||||||
|
.font(.system(size: 12, weight: .semibold))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.lineLimit(1)
|
||||||
|
|
||||||
|
Text(self.model.exitNodeSubtitle)
|
||||||
|
.font(.system(size: 10, design: .monospaced))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
|
||||||
|
if self.model.isUpdatingExitNode {
|
||||||
|
ProgressView()
|
||||||
|
.controlSize(.small)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "chevron.down")
|
||||||
|
.font(.system(size: 10, weight: .semibold))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.frame(width: 220, alignment: .leading)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||||
|
.fill(Color.primary.opacity(0.04))
|
||||||
|
)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||||
|
.stroke(Color.primary.opacity(0.06), lineWidth: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.disabled(!self.model.canSelectExitNode)
|
||||||
|
.opacity(self.model.canSelectExitNode ? 1 : 0.7)
|
||||||
|
.help(self.model.exitNodeHelpText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user