fix views
This commit is contained in:
parent
46baa8f9a7
commit
c5b2cb3e83
@ -415,3 +415,17 @@ final class NetworkModel {
|
||||
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)
|
||||
struct NetworkDeviceGroupView: View {
|
||||
private struct NetworkDeviceGroupView: View {
|
||||
var model: NetworkModel
|
||||
|
||||
// 侧边栏宽度
|
||||
@ -90,7 +90,7 @@ struct NetworkDeviceGroupView: View {
|
||||
}
|
||||
|
||||
// MARK: - 子组件
|
||||
struct NetworkNodeHeadView: View {
|
||||
private struct NetworkNodeHeadView: View {
|
||||
var node: NetworkContext.Node
|
||||
|
||||
var body: some View {
|
||||
@ -112,7 +112,7 @@ struct NetworkNodeHeadView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct NetworkNodeDetailView: View {
|
||||
private struct NetworkNodeDetailView: View {
|
||||
var model: NetworkModel
|
||||
var node: NetworkContext.Node
|
||||
|
||||
@ -152,7 +152,7 @@ struct NetworkNodeDetailView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct ResourceItemCard: View {
|
||||
private struct ResourceItemCard: View {
|
||||
let resource: NetworkContext.Resource
|
||||
@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