fix
This commit is contained in:
parent
aa3cc6abfb
commit
ec817b27b8
417
punchnet/Features/Network/ViewModels/NetworkModel.swift
Normal file
417
punchnet/Features/Network/ViewModels/NetworkModel.swift
Normal file
@ -0,0 +1,417 @@
|
|||||||
|
//
|
||||||
|
// NetworkModel.swift
|
||||||
|
// punchnet
|
||||||
|
//
|
||||||
|
// Created by 安礼成 on 2026/4/17.
|
||||||
|
//
|
||||||
|
import Foundation
|
||||||
|
import Observation
|
||||||
|
|
||||||
|
@Observable
|
||||||
|
final class NetworkModel {
|
||||||
|
@ObservationIgnored
|
||||||
|
private weak var appContext: AppContext?
|
||||||
|
|
||||||
|
@ObservationIgnored
|
||||||
|
private let vpnManager = VPNManager.shared
|
||||||
|
|
||||||
|
@ObservationIgnored
|
||||||
|
private var vpnStatusTask: Task<Void, Never>?
|
||||||
|
|
||||||
|
var showMode: NetworkShowMode = .resource
|
||||||
|
var phase: NetworkConnectionPhase = .disconnected
|
||||||
|
|
||||||
|
var networkSession: SDLAPIClient.NetworkSession?
|
||||||
|
var networkContext: SDLAPIClient.NetworkContext?
|
||||||
|
var selectedExitNodeIp: String?
|
||||||
|
var selectedNodeId: Int?
|
||||||
|
|
||||||
|
private(set) var nodeResourcesById: [Int: [SDLAPIClient.NetworkContext.Resource]] = [:]
|
||||||
|
private(set) var loadingNodeIDs: Set<Int> = []
|
||||||
|
private(set) var isUpdatingExitNode: Bool = false
|
||||||
|
private(set) var errorMessage: String?
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.vpnStatusTask?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
var isBusy: Bool {
|
||||||
|
switch self.phase {
|
||||||
|
case .connecting, .disconnecting:
|
||||||
|
return true
|
||||||
|
case .connected, .disconnected:
|
||||||
|
return self.isUpdatingExitNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isTunnelEnabled: Bool {
|
||||||
|
switch self.phase {
|
||||||
|
case .connecting, .connected:
|
||||||
|
return true
|
||||||
|
case .disconnecting, .disconnected:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var shouldShowModePicker: Bool {
|
||||||
|
self.phase == .connected
|
||||||
|
}
|
||||||
|
|
||||||
|
var resourceList: [SDLAPIClient.NetworkContext.Resource] {
|
||||||
|
self.networkContext?.resourceList ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
var nodeList: [SDLAPIClient.NetworkContext.Node] {
|
||||||
|
self.networkContext?.nodeList ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedNode: SDLAPIClient.NetworkContext.Node? {
|
||||||
|
self.networkContext?.getNode(id: self.selectedNodeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
var canSelectExitNode: Bool {
|
||||||
|
guard self.networkSession != nil else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !self.isUpdatingExitNode else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch self.phase {
|
||||||
|
case .connected:
|
||||||
|
return self.networkContext != nil || self.selectedExitNodeIp != nil
|
||||||
|
case .disconnected:
|
||||||
|
return self.selectedExitNodeIp != nil
|
||||||
|
case .connecting, .disconnecting:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var exitNodeOptions: [ExitNodeOption] {
|
||||||
|
guard let networkContext = self.networkContext else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return networkContext.exitNodeList.compactMap { exitNode in
|
||||||
|
guard let node = networkContext.getNode(id: exitNode.nnid) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExitNodeOption(
|
||||||
|
id: exitNode.nnid,
|
||||||
|
nodeName: exitNode.nodeName,
|
||||||
|
ip: node.ip,
|
||||||
|
system: node.system
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedExitNode: ExitNodeOption? {
|
||||||
|
guard let selectedExitNodeIp = self.selectedExitNodeIp else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.exitNodeOptions.first(where: { $0.ip == selectedExitNodeIp })
|
||||||
|
?? ExitNodeOption(
|
||||||
|
id: -1,
|
||||||
|
nodeName: "已保存出口节点",
|
||||||
|
ip: selectedExitNodeIp,
|
||||||
|
system: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var exitNodeTitle: String {
|
||||||
|
if self.isUpdatingExitNode {
|
||||||
|
return "正在切换..."
|
||||||
|
}
|
||||||
|
|
||||||
|
if let selectedExitNode = self.selectedExitNode {
|
||||||
|
return selectedExitNode.nodeName
|
||||||
|
}
|
||||||
|
|
||||||
|
return "未设置"
|
||||||
|
}
|
||||||
|
|
||||||
|
var exitNodeSubtitle: String {
|
||||||
|
if let selectedExitNode = self.selectedExitNode {
|
||||||
|
if let system = selectedExitNode.system, !system.isEmpty {
|
||||||
|
return "\(selectedExitNode.ip) · \(system)"
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedExitNode.ip
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.networkContext == nil {
|
||||||
|
return "连接后可选择"
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.exitNodeOptions.isEmpty {
|
||||||
|
return "当前网络没有可用节点"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "当前流量保持默认出口"
|
||||||
|
}
|
||||||
|
|
||||||
|
var exitNodeHelpText: String {
|
||||||
|
if self.isUpdatingExitNode {
|
||||||
|
return "正在更新出口节点"
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.networkContext == nil {
|
||||||
|
return "建立连接后可选择当前网络的出口节点"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "切换当前网络流量的出口节点,也可以保持未设置"
|
||||||
|
}
|
||||||
|
|
||||||
|
func activate(appContext: AppContext) async {
|
||||||
|
if self.appContext !== appContext {
|
||||||
|
self.appContext = appContext
|
||||||
|
}
|
||||||
|
|
||||||
|
self.startObservingVPNStatusIfNeeded()
|
||||||
|
await self.handleVPNStatusChange(self.vpnManager.vpnStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearError() {
|
||||||
|
self.errorMessage = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setConnectionEnabled(_ enabled: Bool) async {
|
||||||
|
if enabled {
|
||||||
|
await self.connect()
|
||||||
|
} else {
|
||||||
|
await self.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func connect() async {
|
||||||
|
guard let appContext = self.appContext else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !self.isBusy else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.errorMessage = nil
|
||||||
|
self.phase = .connecting
|
||||||
|
|
||||||
|
do {
|
||||||
|
if appContext.networkContext == nil {
|
||||||
|
try await appContext.connectNetwork()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.syncSharedStateFromAppContext()
|
||||||
|
await self.applyNetworkContext(appContext.networkContext)
|
||||||
|
try await appContext.startTun()
|
||||||
|
|
||||||
|
if self.vpnManager.vpnStatus == .connected {
|
||||||
|
self.phase = .connected
|
||||||
|
}
|
||||||
|
} catch let err as SDLAPIError {
|
||||||
|
self.errorMessage = err.message
|
||||||
|
self.handleDisconnectedState(syncAppContext: true)
|
||||||
|
} catch let err as AppContextError {
|
||||||
|
self.errorMessage = err.message
|
||||||
|
self.handleDisconnectedState(syncAppContext: true)
|
||||||
|
} catch {
|
||||||
|
self.errorMessage = error.localizedDescription
|
||||||
|
self.handleDisconnectedState(syncAppContext: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func disconnect() async {
|
||||||
|
guard let appContext = self.appContext else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !self.isBusy else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.errorMessage = nil
|
||||||
|
self.phase = .disconnecting
|
||||||
|
|
||||||
|
do {
|
||||||
|
try await appContext.stopTun()
|
||||||
|
self.handleDisconnectedState(syncAppContext: false)
|
||||||
|
} catch let err as AppContextError {
|
||||||
|
self.errorMessage = err.message
|
||||||
|
await self.handleVPNStatusChange(self.vpnManager.vpnStatus)
|
||||||
|
} catch {
|
||||||
|
self.errorMessage = error.localizedDescription
|
||||||
|
await self.handleVPNStatusChange(self.vpnManager.vpnStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectNode(id: Int?) {
|
||||||
|
self.selectedNodeId = id
|
||||||
|
|
||||||
|
guard let id else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Task { @MainActor in
|
||||||
|
await self.loadResourcesIfNeeded(for: id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resources(for nodeId: Int) -> [SDLAPIClient.NetworkContext.Resource] {
|
||||||
|
self.nodeResourcesById[nodeId] ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLoadingResources(for nodeId: Int) -> Bool {
|
||||||
|
self.loadingNodeIDs.contains(nodeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadResourcesIfNeeded(for nodeId: Int) async {
|
||||||
|
guard let session = self.networkSession else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard self.nodeResourcesById[nodeId] == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !self.loadingNodeIDs.contains(nodeId) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentContextIdentity = self.contextIdentity(self.networkContext)
|
||||||
|
|
||||||
|
self.loadingNodeIDs.insert(nodeId)
|
||||||
|
defer {
|
||||||
|
self.loadingNodeIDs.remove(nodeId)
|
||||||
|
}
|
||||||
|
|
||||||
|
let resources = await SDLAPIClient.loadNodeResources(accesToken: session.accessToken, id: nodeId)
|
||||||
|
|
||||||
|
guard currentContextIdentity == self.contextIdentity(self.networkContext) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.nodeResourcesById[nodeId] = resources
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateExitNodeSelection(_ ip: String?) async {
|
||||||
|
guard let appContext = self.appContext else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !self.isUpdatingExitNode else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.errorMessage = nil
|
||||||
|
self.isUpdatingExitNode = true
|
||||||
|
defer {
|
||||||
|
self.isUpdatingExitNode = false
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try await appContext.updateExitNodeIp(exitNodeIp: ip)
|
||||||
|
self.syncSharedStateFromAppContext()
|
||||||
|
} catch let err as AppContextError {
|
||||||
|
self.errorMessage = err.message
|
||||||
|
} catch {
|
||||||
|
self.errorMessage = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startObservingVPNStatusIfNeeded() {
|
||||||
|
guard self.vpnStatusTask == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let vpnStatusStream = self.vpnManager.vpnStatusStream
|
||||||
|
|
||||||
|
self.vpnStatusTask = Task { [weak self, vpnStatusStream] in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for await status in vpnStatusStream {
|
||||||
|
if Task.isCancelled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await self.handleVPNStatusChange(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func syncSharedStateFromAppContext() {
|
||||||
|
self.networkSession = self.appContext?.networkSession
|
||||||
|
self.selectedExitNodeIp = self.appContext?.selectedExitNodeIp
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleVPNStatusChange(_ status: VPNManager.VPNStatus) async {
|
||||||
|
self.syncSharedStateFromAppContext()
|
||||||
|
|
||||||
|
switch status {
|
||||||
|
case .connecting:
|
||||||
|
self.phase = .connecting
|
||||||
|
await self.applyNetworkContext(self.appContext?.networkContext)
|
||||||
|
case .connected:
|
||||||
|
self.phase = .connected
|
||||||
|
await self.applyNetworkContext(self.appContext?.networkContext)
|
||||||
|
case .disconnecting:
|
||||||
|
self.phase = .disconnecting
|
||||||
|
case .disconnected:
|
||||||
|
self.handleDisconnectedState(syncAppContext: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleDisconnectedState(syncAppContext: Bool) {
|
||||||
|
if syncAppContext {
|
||||||
|
self.appContext?.networkContext = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.phase = .disconnected
|
||||||
|
self.networkContext = nil
|
||||||
|
self.selectedNodeId = nil
|
||||||
|
self.nodeResourcesById.removeAll()
|
||||||
|
self.loadingNodeIDs.removeAll()
|
||||||
|
self.showMode = .resource
|
||||||
|
self.syncSharedStateFromAppContext()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyNetworkContext(_ newContext: SDLAPIClient.NetworkContext?) async {
|
||||||
|
let contextChanged = self.contextIdentity(self.networkContext) != self.contextIdentity(newContext)
|
||||||
|
|
||||||
|
self.networkContext = newContext
|
||||||
|
|
||||||
|
if contextChanged {
|
||||||
|
self.nodeResourcesById.removeAll()
|
||||||
|
self.loadingNodeIDs.removeAll()
|
||||||
|
self.selectedNodeId = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let newContext else {
|
||||||
|
self.selectedNodeId = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let selectedNodeId = self.selectedNodeId,
|
||||||
|
newContext.getNode(id: selectedNodeId) != nil {
|
||||||
|
await self.loadResourcesIfNeeded(for: selectedNodeId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.selectedNodeId = newContext.firstNodeId()
|
||||||
|
|
||||||
|
if let selectedNodeId = self.selectedNodeId {
|
||||||
|
await self.loadResourcesIfNeeded(for: selectedNodeId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func contextIdentity(_ context: SDLAPIClient.NetworkContext?) -> String? {
|
||||||
|
guard let context else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\(context.identityId)-\(context.ip)"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,415 +18,7 @@ enum NetworkConnectionPhase {
|
|||||||
case disconnecting
|
case disconnecting
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
@Observable
|
|
||||||
final class NetworkModel {
|
|
||||||
@ObservationIgnored
|
|
||||||
private weak var appContext: AppContext?
|
|
||||||
|
|
||||||
@ObservationIgnored
|
|
||||||
private let vpnManager = VPNManager.shared
|
|
||||||
|
|
||||||
@ObservationIgnored
|
|
||||||
private var vpnStatusTask: Task<Void, Never>?
|
|
||||||
|
|
||||||
var showMode: NetworkShowMode = .resource
|
|
||||||
var phase: NetworkConnectionPhase = .disconnected
|
|
||||||
|
|
||||||
var networkSession: SDLAPIClient.NetworkSession?
|
|
||||||
var networkContext: SDLAPIClient.NetworkContext?
|
|
||||||
var selectedExitNodeIp: String?
|
|
||||||
var selectedNodeId: Int?
|
|
||||||
|
|
||||||
private(set) var nodeResourcesById: [Int: [SDLAPIClient.NetworkContext.Resource]] = [:]
|
|
||||||
private(set) var loadingNodeIDs: Set<Int> = []
|
|
||||||
private(set) var isUpdatingExitNode: Bool = false
|
|
||||||
private(set) var errorMessage: String?
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
self.vpnStatusTask?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
var isBusy: Bool {
|
|
||||||
switch self.phase {
|
|
||||||
case .connecting, .disconnecting:
|
|
||||||
return true
|
|
||||||
case .connected, .disconnected:
|
|
||||||
return self.isUpdatingExitNode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var isTunnelEnabled: Bool {
|
|
||||||
switch self.phase {
|
|
||||||
case .connecting, .connected:
|
|
||||||
return true
|
|
||||||
case .disconnecting, .disconnected:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var shouldShowModePicker: Bool {
|
|
||||||
self.phase == .connected
|
|
||||||
}
|
|
||||||
|
|
||||||
var resourceList: [SDLAPIClient.NetworkContext.Resource] {
|
|
||||||
self.networkContext?.resourceList ?? []
|
|
||||||
}
|
|
||||||
|
|
||||||
var nodeList: [SDLAPIClient.NetworkContext.Node] {
|
|
||||||
self.networkContext?.nodeList ?? []
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectedNode: SDLAPIClient.NetworkContext.Node? {
|
|
||||||
self.networkContext?.getNode(id: self.selectedNodeId)
|
|
||||||
}
|
|
||||||
|
|
||||||
var canSelectExitNode: Bool {
|
|
||||||
guard self.networkSession != nil else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
guard !self.isUpdatingExitNode else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
switch self.phase {
|
|
||||||
case .connected:
|
|
||||||
return self.networkContext != nil || self.selectedExitNodeIp != nil
|
|
||||||
case .disconnected:
|
|
||||||
return self.selectedExitNodeIp != nil
|
|
||||||
case .connecting, .disconnecting:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var exitNodeOptions: [ExitNodeOption] {
|
|
||||||
guard let networkContext = self.networkContext else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
return networkContext.exitNodeList.compactMap { exitNode in
|
|
||||||
guard let node = networkContext.getNode(id: exitNode.nnid) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return ExitNodeOption(
|
|
||||||
id: exitNode.nnid,
|
|
||||||
nodeName: exitNode.nodeName,
|
|
||||||
ip: node.ip,
|
|
||||||
system: node.system
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectedExitNode: ExitNodeOption? {
|
|
||||||
guard let selectedExitNodeIp = self.selectedExitNodeIp else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.exitNodeOptions.first(where: { $0.ip == selectedExitNodeIp })
|
|
||||||
?? ExitNodeOption(
|
|
||||||
id: -1,
|
|
||||||
nodeName: "已保存出口节点",
|
|
||||||
ip: selectedExitNodeIp,
|
|
||||||
system: nil
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var exitNodeTitle: String {
|
|
||||||
if self.isUpdatingExitNode {
|
|
||||||
return "正在切换..."
|
|
||||||
}
|
|
||||||
|
|
||||||
if let selectedExitNode = self.selectedExitNode {
|
|
||||||
return selectedExitNode.nodeName
|
|
||||||
}
|
|
||||||
|
|
||||||
return "未设置"
|
|
||||||
}
|
|
||||||
|
|
||||||
var exitNodeSubtitle: String {
|
|
||||||
if let selectedExitNode = self.selectedExitNode {
|
|
||||||
if let system = selectedExitNode.system, !system.isEmpty {
|
|
||||||
return "\(selectedExitNode.ip) · \(system)"
|
|
||||||
}
|
|
||||||
|
|
||||||
return selectedExitNode.ip
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.networkContext == nil {
|
|
||||||
return "连接后可选择"
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.exitNodeOptions.isEmpty {
|
|
||||||
return "当前网络没有可用节点"
|
|
||||||
}
|
|
||||||
|
|
||||||
return "当前流量保持默认出口"
|
|
||||||
}
|
|
||||||
|
|
||||||
var exitNodeHelpText: String {
|
|
||||||
if self.isUpdatingExitNode {
|
|
||||||
return "正在更新出口节点"
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.networkContext == nil {
|
|
||||||
return "建立连接后可选择当前网络的出口节点"
|
|
||||||
}
|
|
||||||
|
|
||||||
return "切换当前网络流量的出口节点,也可以保持未设置"
|
|
||||||
}
|
|
||||||
|
|
||||||
func activate(appContext: AppContext) async {
|
|
||||||
if self.appContext !== appContext {
|
|
||||||
self.appContext = appContext
|
|
||||||
}
|
|
||||||
|
|
||||||
self.startObservingVPNStatusIfNeeded()
|
|
||||||
await self.handleVPNStatusChange(self.vpnManager.vpnStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
func clearError() {
|
|
||||||
self.errorMessage = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setConnectionEnabled(_ enabled: Bool) async {
|
|
||||||
if enabled {
|
|
||||||
await self.connect()
|
|
||||||
} else {
|
|
||||||
await self.disconnect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func connect() async {
|
|
||||||
guard let appContext = self.appContext else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard !self.isBusy else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.errorMessage = nil
|
|
||||||
self.phase = .connecting
|
|
||||||
|
|
||||||
do {
|
|
||||||
if appContext.networkContext == nil {
|
|
||||||
try await appContext.connectNetwork()
|
|
||||||
}
|
|
||||||
|
|
||||||
self.syncSharedStateFromAppContext()
|
|
||||||
await self.applyNetworkContext(appContext.networkContext)
|
|
||||||
try await appContext.startTun()
|
|
||||||
|
|
||||||
if self.vpnManager.vpnStatus == .connected {
|
|
||||||
self.phase = .connected
|
|
||||||
}
|
|
||||||
} catch let err as SDLAPIError {
|
|
||||||
self.errorMessage = err.message
|
|
||||||
self.handleDisconnectedState(syncAppContext: true)
|
|
||||||
} catch let err as AppContextError {
|
|
||||||
self.errorMessage = err.message
|
|
||||||
self.handleDisconnectedState(syncAppContext: true)
|
|
||||||
} catch {
|
|
||||||
self.errorMessage = error.localizedDescription
|
|
||||||
self.handleDisconnectedState(syncAppContext: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func disconnect() async {
|
|
||||||
guard let appContext = self.appContext else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard !self.isBusy else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.errorMessage = nil
|
|
||||||
self.phase = .disconnecting
|
|
||||||
|
|
||||||
do {
|
|
||||||
try await appContext.stopTun()
|
|
||||||
self.handleDisconnectedState(syncAppContext: false)
|
|
||||||
} catch let err as AppContextError {
|
|
||||||
self.errorMessage = err.message
|
|
||||||
await self.handleVPNStatusChange(self.vpnManager.vpnStatus)
|
|
||||||
} catch {
|
|
||||||
self.errorMessage = error.localizedDescription
|
|
||||||
await self.handleVPNStatusChange(self.vpnManager.vpnStatus)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func selectNode(id: Int?) {
|
|
||||||
self.selectedNodeId = id
|
|
||||||
|
|
||||||
guard let id else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Task { @MainActor in
|
|
||||||
await self.loadResourcesIfNeeded(for: id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func resources(for nodeId: Int) -> [SDLAPIClient.NetworkContext.Resource] {
|
|
||||||
self.nodeResourcesById[nodeId] ?? []
|
|
||||||
}
|
|
||||||
|
|
||||||
func isLoadingResources(for nodeId: Int) -> Bool {
|
|
||||||
self.loadingNodeIDs.contains(nodeId)
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadResourcesIfNeeded(for nodeId: Int) async {
|
|
||||||
guard let session = self.networkSession else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard self.nodeResourcesById[nodeId] == nil else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard !self.loadingNodeIDs.contains(nodeId) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentContextIdentity = self.contextIdentity(self.networkContext)
|
|
||||||
|
|
||||||
self.loadingNodeIDs.insert(nodeId)
|
|
||||||
defer {
|
|
||||||
self.loadingNodeIDs.remove(nodeId)
|
|
||||||
}
|
|
||||||
|
|
||||||
let resources = await SDLAPIClient.loadNodeResources(accesToken: session.accessToken, id: nodeId)
|
|
||||||
|
|
||||||
guard currentContextIdentity == self.contextIdentity(self.networkContext) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.nodeResourcesById[nodeId] = resources
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateExitNodeSelection(_ ip: String?) async {
|
|
||||||
guard let appContext = self.appContext else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard !self.isUpdatingExitNode else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.errorMessage = nil
|
|
||||||
self.isUpdatingExitNode = true
|
|
||||||
defer {
|
|
||||||
self.isUpdatingExitNode = false
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
try await appContext.updateExitNodeIp(exitNodeIp: ip)
|
|
||||||
self.syncSharedStateFromAppContext()
|
|
||||||
} catch let err as AppContextError {
|
|
||||||
self.errorMessage = err.message
|
|
||||||
} catch {
|
|
||||||
self.errorMessage = error.localizedDescription
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func startObservingVPNStatusIfNeeded() {
|
|
||||||
guard self.vpnStatusTask == nil else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let vpnStatusStream = self.vpnManager.vpnStatusStream
|
|
||||||
|
|
||||||
self.vpnStatusTask = Task { [weak self, vpnStatusStream] in
|
|
||||||
guard let self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for await status in vpnStatusStream {
|
|
||||||
if Task.isCancelled {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await self.handleVPNStatusChange(status)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func syncSharedStateFromAppContext() {
|
|
||||||
self.networkSession = self.appContext?.networkSession
|
|
||||||
self.selectedExitNodeIp = self.appContext?.selectedExitNodeIp
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleVPNStatusChange(_ status: VPNManager.VPNStatus) async {
|
|
||||||
self.syncSharedStateFromAppContext()
|
|
||||||
|
|
||||||
switch status {
|
|
||||||
case .connecting:
|
|
||||||
self.phase = .connecting
|
|
||||||
await self.applyNetworkContext(self.appContext?.networkContext)
|
|
||||||
case .connected:
|
|
||||||
self.phase = .connected
|
|
||||||
await self.applyNetworkContext(self.appContext?.networkContext)
|
|
||||||
case .disconnecting:
|
|
||||||
self.phase = .disconnecting
|
|
||||||
case .disconnected:
|
|
||||||
self.handleDisconnectedState(syncAppContext: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleDisconnectedState(syncAppContext: Bool) {
|
|
||||||
if syncAppContext {
|
|
||||||
self.appContext?.networkContext = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
self.phase = .disconnected
|
|
||||||
self.networkContext = nil
|
|
||||||
self.selectedNodeId = nil
|
|
||||||
self.nodeResourcesById.removeAll()
|
|
||||||
self.loadingNodeIDs.removeAll()
|
|
||||||
self.showMode = .resource
|
|
||||||
self.syncSharedStateFromAppContext()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func applyNetworkContext(_ newContext: SDLAPIClient.NetworkContext?) async {
|
|
||||||
let contextChanged = self.contextIdentity(self.networkContext) != self.contextIdentity(newContext)
|
|
||||||
|
|
||||||
self.networkContext = newContext
|
|
||||||
|
|
||||||
if contextChanged {
|
|
||||||
self.nodeResourcesById.removeAll()
|
|
||||||
self.loadingNodeIDs.removeAll()
|
|
||||||
self.selectedNodeId = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let newContext else {
|
|
||||||
self.selectedNodeId = nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if let selectedNodeId = self.selectedNodeId,
|
|
||||||
newContext.getNode(id: selectedNodeId) != nil {
|
|
||||||
await self.loadResourcesIfNeeded(for: selectedNodeId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.selectedNodeId = newContext.firstNodeId()
|
|
||||||
|
|
||||||
if let selectedNodeId = self.selectedNodeId {
|
|
||||||
await self.loadResourcesIfNeeded(for: selectedNodeId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func contextIdentity(_ context: SDLAPIClient.NetworkContext?) -> String? {
|
|
||||||
guard let context else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return "\(context.identityId)-\(context.ip)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - 主网络视图
|
// MARK: - 主网络视图
|
||||||
struct NetworkView: View {
|
struct NetworkView: View {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user