Files
UUVPN/iOS-SwiftUI-Code/ApplicationLibrary/Views/Profile/ImportProfileView.swift
T

211 lines
7.9 KiB
Swift

#if os(tvOS)
import DeviceDiscoveryUI
import Libbox
import Library
import SwiftUI
@MainActor
public struct ImportProfileView: View {
@EnvironmentObject private var environments: ExtensionEnvironments
@Environment(\.dismiss) private var dismiss
@State private var isLoading = false
@State private var selected = false
@State private var alert: Alert?
@State private var connection: NWConnection?
@State private var socket: NWSocket?
@State private var profiles: [LibboxProfilePreview]?
@State private var isImporting = false
public init() {}
public var body: some View {
VStack(alignment: .center) {
if !selected {
Form {
Section {
EmptyView()
} footer: {
Text("To import configurations from your iPhone or iPad, make sure sing-box is the **same version** on both devices and **VPN is disabled**.")
}
DevicePicker(
.applicationService(name: "sing-box:profile"))
{ endpoint in
selected = true
Task {
await handleEndpoint(endpoint)
}
} label: {
Text("Select Device")
} fallback: {
EmptyView()
} parameters: {
.applicationService
}
}
} else if let profiles {
Form {
Section {
EmptyView()
} footer: {
Text("\(profiles.count) Profiles")
}
ForEach(profiles, id: \.profileID) { profile in
Button(profile.name) {
isLoading = true
Task {
selectProfile(profileID: profile.profileID)
isLoading = false
}
}.disabled(isLoading || isImporting)
}
}
} else {
Text("Connecting...")
}
}
.focusSection()
.alertBinding($alert)
.navigationTitle("Import Profile")
}
private 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
}
private func handleEndpoint(_ endpoint: NWEndpoint) 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 = Alert(error)
}
default: break
}
}
connection.start(queue: .global())
do {
try await loopMessages()
} catch {
alert = Alert(error)
reset()
}
}
private nonisolated func loopMessages() async throws {
guard let socket = await socket else {
return
}
var message: Data
while true {
do {
message = try socket.read()
} catch {
throw NSError(domain: "read from connection: \(error.localizedDescription)", code: 0)
}
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: "remote error: \(message.message)", code: 0)
}
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 {
// not supported on tvOS
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!)
return
default:
throw NSError(domain: "unknown message type \(message[0])", code: 0)
}
}
}
private 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 = Alert(error)
reset()
}
}
private nonisolated func importProfile(_ content: LibboxProfileContent) 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 = Date(timeIntervalSince1970: Double(content.lastUpdated))
}
try await ProfileManager.create(Profile(name: content.name, type: type, path: profileConfig.relativePath, remoteURL: content.remotePath, autoUpdate: content.autoUpdate, lastUpdated: lastUpdated))
await reset()
await MainActor.run {
environments.profileUpdate.send()
dismiss()
}
}
}
#endif