punchnet-macos/punchnet/Features/Network/Views/NetworkStatusBar.swift
2026-04-17 15:56:56 +08:00

158 lines
5.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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))"
}
}