Files
sing-box-for-apple/ApplicationLibrary/Views/Profile/ImportProfileViewModel.swift
T

158 lines
6.3 KiB
Swift

#if os(tvOS)
import DeviceDiscoveryUI
import Libbox
import Library
import Network
import SwiftUI
@MainActor
public final class ImportProfileViewModel: BaseViewModel {
@Published public var selected = false
@Published public var connection: NWConnection?
@Published public var socket: NWSocket?
@Published public var profiles: [LibboxProfilePreview]?
@Published public var isImporting = false
@Published public var importSucceeded = false
public func reset() {
if let connection {
connection.stateUpdateHandler = nil
connection.cancel()
self.connection = nil
}
if let socket {
socket.cancel()
self.socket = nil
}
selected = false
profiles = nil
}
public func handleEndpoint(_ endpoint: NWEndpoint, environments: ExtensionEnvironments) async {
let connection = NWConnection(to: endpoint, using: NWParameters.applicationService)
self.connection = connection
socket = NWSocket(connection)
connection.stateUpdateHandler = { state in
switch state {
case let .failed(error):
DispatchQueue.main.async { [self] in
reset()
alert = AlertState(error: error)
}
default: break
}
}
connection.start(queue: .global())
do {
try await loopMessages(environments: environments)
} catch {
alert = AlertState(error: error)
reset()
}
}
private nonisolated func loopMessages(environments: ExtensionEnvironments) async throws {
guard let socket = await socket else {
return
}
var message: Data
while true {
do {
message = try socket.read()
} catch {
throw NSError(domain: "ImportProfileViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Read from connection: \(error.localizedDescription)")])
}
var error: NSError?
switch Int64(message[0]) {
case LibboxMessageTypeError:
let message = LibboxDecodeErrorMessage(message, &error)
if let error {
throw error
}
if let message {
throw NSError(domain: "ImportProfileViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Remote error: \(message.message)")])
}
case LibboxMessageTypeProfileList:
let decoder = LibboxProfileDecoder()
try decoder.decode(message)
let iterator = decoder.iterator()!
var profiles = [LibboxProfilePreview]()
while iterator.hasNext() {
let profile = iterator.next()!
if profile.type == LibboxProfileTypeiCloud {
continue
}
profiles.append(profile)
}
await MainActor.run { [self, profiles] in
self.profiles = profiles
isImporting = false
}
case LibboxMessageTypeProfileContent:
let content = LibboxDecodeProfileContent(message, &error)
if let error {
throw error
}
try await importProfile(content!, environments: environments)
return
default:
throw NSError(domain: "ImportProfileViewModel", code: 0, userInfo: [NSLocalizedDescriptionKey: String(localized: "Unknown message type \(message[0])")])
}
}
}
public func selectProfile(profileID: Int64) {
guard let connection else {
return
}
guard let socket else {
return
}
connection.stateUpdateHandler = nil
let request = LibboxProfileContentRequest()
request.profileID = profileID
do {
try socket.write(request.encode())
isImporting = true
} catch {
alert = AlertState(error: error)
reset()
}
}
private nonisolated func importProfile(_ content: LibboxProfileContent, environments: ExtensionEnvironments) async throws {
var type: ProfileType = .local
switch content.type {
case LibboxProfileTypeLocal:
type = .local
case LibboxProfileTypeiCloud:
type = .icloud
case LibboxProfileTypeRemote:
type = .remote
default:
break
}
let nextProfileID = try await ProfileManager.nextID()
let profileConfigDirectory = FilePath.sharedDirectory.appendingPathComponent("configs", isDirectory: true)
try FileManager.default.createDirectory(at: profileConfigDirectory, withIntermediateDirectories: true)
let profileConfig = profileConfigDirectory.appendingPathComponent("config_\(nextProfileID).json")
try content.config.write(to: profileConfig, atomically: true, encoding: .utf8)
var lastUpdated: Date?
if content.lastUpdated > 0 {
lastUpdated = dateFromTimestamp(content.lastUpdated)
}
let uniqueProfileName = try await ProfileManager.uniqueName(content.name)
let profile = Profile(name: uniqueProfileName, type: type, path: profileConfig.relativePath, remoteURL: content.remotePath, autoUpdate: content.autoUpdate, lastUpdated: lastUpdated)
try await ProfileManager.create(profile)
await SharedPreferences.selectedProfileID.set(profile.mustID)
await reset()
await MainActor.run {
environments.profileUpdate.send()
importSucceeded = true
}
}
}
#endif