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
|
||||
}
|
||||
|
||||
@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: - 主网络视图
|
||||
struct NetworkView: View {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user