mirror of
https://github.com/SagerNet/sing-box-for-apple.git
synced 2026-05-04 11:32:29 +00:00
333 lines
12 KiB
Swift
333 lines
12 KiB
Swift
import Libbox
|
|
import Library
|
|
import SwiftUI
|
|
#if os(iOS)
|
|
import FileProvider
|
|
import UIKit
|
|
#elseif os(macOS)
|
|
import ServiceManagement
|
|
#endif
|
|
|
|
@MainActor
|
|
public struct CoreView: View {
|
|
@Environment(\.scenePhase) private var scenePhase
|
|
@EnvironmentObject private var environments: ExtensionEnvironments
|
|
@State private var isLoading = true
|
|
@State private var alert: AlertState?
|
|
|
|
@State private var disableDeprecatedWarnings = false
|
|
|
|
@State private var version = ""
|
|
@State private var dataSize: String?
|
|
@State private var dataSizeLoaded = false
|
|
|
|
#if os(macOS)
|
|
@State private var helperUnavailable = false
|
|
#endif
|
|
|
|
public init() {}
|
|
public var body: some View {
|
|
Group {
|
|
if isLoading {
|
|
ProgressView().onAppear {
|
|
Task {
|
|
await loadSettings()
|
|
}
|
|
}
|
|
} else {
|
|
FormView {
|
|
FormTextItem("Version", version)
|
|
if let dataSize {
|
|
FormTextItem("Data Size", dataSize)
|
|
} else if !dataSizeLoaded {
|
|
HStack {
|
|
Text("Data Size")
|
|
Spacer()
|
|
ProgressView()
|
|
}
|
|
} else {
|
|
#if os(macOS)
|
|
HStack {
|
|
Text("Data Size")
|
|
Spacer()
|
|
Text("Unavailable")
|
|
.foregroundStyle(.red)
|
|
.onTapGesture {
|
|
alert = helperRequiredAlert()
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
if Variant.isBeta {
|
|
FormToggle("Disable Deprecated Warnings", "Do not show warnings about usages of deprecated features.", $disableDeprecatedWarnings, header: "Beta Settings") {
|
|
newValue in
|
|
await SharedPreferences.disableDeprecatedWarnings.set(newValue)
|
|
}
|
|
}
|
|
|
|
Section("Working Directory") {
|
|
#if os(macOS)
|
|
if !Variant.useSystemExtension {
|
|
FormButton {
|
|
NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: FilePath.workingDirectory.relativePath)
|
|
} label: {
|
|
Label("Open", systemImage: "macwindow.and.cursorarrow")
|
|
}
|
|
}
|
|
#elseif os(iOS)
|
|
if #available(iOS 16.0, *) {
|
|
FormButton {
|
|
Task {
|
|
await openInFilesApp()
|
|
}
|
|
} label: {
|
|
Label("Browse", systemImage: "folder.fill")
|
|
}
|
|
}
|
|
#endif
|
|
FormButton(role: .destructive) {
|
|
Task {
|
|
await confirmDestroyWorkingDirectory()
|
|
}
|
|
} label: {
|
|
Label("Destroy", systemImage: "trash.fill")
|
|
}
|
|
.foregroundColor(.red)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Core")
|
|
.alert($alert)
|
|
.onAppear {
|
|
guard !isLoading else {
|
|
return
|
|
}
|
|
Task {
|
|
await refreshWorkingDirectorySize()
|
|
}
|
|
}
|
|
.onChangeCompat(of: scenePhase) { newValue in
|
|
guard newValue == .active, !isLoading else {
|
|
return
|
|
}
|
|
Task {
|
|
await refreshWorkingDirectorySize()
|
|
}
|
|
}
|
|
#if os(iOS)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
#endif
|
|
}
|
|
|
|
private nonisolated func loadSettings() async {
|
|
if Variant.screenshotMode {
|
|
await MainActor.run {
|
|
version = "<redacted>"
|
|
dataSize = LibboxFormatBytes(1000 * 1000 * 10)
|
|
isLoading = false
|
|
}
|
|
} else {
|
|
await MainActor.run {
|
|
version = LibboxVersion()
|
|
isLoading = false
|
|
}
|
|
await loadSettingsBackground()
|
|
}
|
|
}
|
|
|
|
private nonisolated func loadSettingsBackground() async {
|
|
let disableDeprecatedWarnings = await SharedPreferences.disableDeprecatedWarnings.get()
|
|
await MainActor.run {
|
|
self.disableDeprecatedWarnings = disableDeprecatedWarnings
|
|
}
|
|
await refreshWorkingDirectorySize()
|
|
}
|
|
|
|
private nonisolated func refreshWorkingDirectorySize() async {
|
|
guard !Variant.screenshotMode else {
|
|
return
|
|
}
|
|
#if os(macOS)
|
|
let helperUnavailable = Variant.useSystemExtension && HelperServiceManager.rootHelperStatus != .enabled
|
|
#endif
|
|
let workingDirectory = FilePath.workingDirectory
|
|
let dataSize: String? = await BlockingIO.run {
|
|
#if os(macOS)
|
|
if Variant.useSystemExtension {
|
|
if helperUnavailable {
|
|
return nil
|
|
}
|
|
guard let size = try? RootHelperClient.shared.getWorkingDirectorySize() else {
|
|
return nil
|
|
}
|
|
return LibboxFormatBytes(size)
|
|
} else {
|
|
return (try? workingDirectory.formattedSize()) ?? "Unknown"
|
|
}
|
|
#else
|
|
return (try? workingDirectory.formattedSize()) ?? "Unknown"
|
|
#endif
|
|
}
|
|
await MainActor.run {
|
|
#if os(macOS)
|
|
self.helperUnavailable = helperUnavailable
|
|
#endif
|
|
self.dataSize = dataSize
|
|
dataSizeLoaded = true
|
|
}
|
|
}
|
|
|
|
private func confirmDestroyWorkingDirectory() async {
|
|
#if os(macOS)
|
|
if helperUnavailable {
|
|
alert = helperRequiredAlert()
|
|
return
|
|
}
|
|
#endif
|
|
if environments.extensionProfile?.status.isConnected == true {
|
|
alert = AlertState(
|
|
title: String(localized: "Service is Running"),
|
|
message: String(localized: "The service must be stopped before destroying the working directory."),
|
|
primaryButton: .destructive(String(localized: "Stop Service and Continue")) { [self] in
|
|
Task {
|
|
await stopServiceAndDestroy()
|
|
}
|
|
},
|
|
secondaryButton: .cancel()
|
|
)
|
|
} else {
|
|
await destroyWorkingDirectory()
|
|
}
|
|
}
|
|
|
|
private func stopServiceAndDestroy() async {
|
|
do {
|
|
try await environments.extensionProfile!.stop()
|
|
await destroyWorkingDirectory()
|
|
} catch {
|
|
alert = AlertState(action: "stop service before destroying working directory", error: error)
|
|
}
|
|
}
|
|
|
|
private func destroyWorkingDirectory() async {
|
|
do {
|
|
let workingDirectory = FilePath.workingDirectory
|
|
#if os(macOS)
|
|
if Variant.useSystemExtension {
|
|
try await BlockingIO.run {
|
|
try RootHelperClient.shared.cleanWorkingDirectory()
|
|
}
|
|
} else {
|
|
try await BlockingIO.run {
|
|
try Self.clearWorkingDirectoryContents(at: workingDirectory)
|
|
}
|
|
}
|
|
#else
|
|
try await BlockingIO.run {
|
|
try Self.clearWorkingDirectoryContents(at: workingDirectory)
|
|
}
|
|
#if os(iOS)
|
|
if #available(iOS 16.0, *) {
|
|
await notifyFileProviderWorkingDirectoryChanged()
|
|
}
|
|
#endif
|
|
#endif
|
|
isLoading = true
|
|
} catch {
|
|
alert = AlertState(action: "destroy working directory", error: error)
|
|
}
|
|
}
|
|
|
|
private nonisolated static func clearWorkingDirectoryContents(at url: URL) throws {
|
|
guard FileManager.default.fileExists(atPath: url.path) else {
|
|
return
|
|
}
|
|
let contents = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil)
|
|
for item in contents {
|
|
try FileManager.default.removeItem(at: item)
|
|
}
|
|
}
|
|
|
|
#if os(macOS)
|
|
private func helperRequiredAlert() -> AlertState {
|
|
AlertState(
|
|
title: String(localized: "Helper Service Required"),
|
|
message: String(localized: "Managing working directory requires Helper Service."),
|
|
primaryButton: .default(String(localized: "App Settings")) {
|
|
NotificationCenter.default.post(name: .navigateToSettingsPage, object: SettingsPage.app)
|
|
},
|
|
secondaryButton: .cancel(String(localized: "Ok"))
|
|
)
|
|
}
|
|
#endif
|
|
|
|
#if os(iOS)
|
|
@available(iOS 16.0, *)
|
|
private nonisolated func fileProviderManager() async throws -> NSFileProviderManager {
|
|
let domains = try await NSFileProviderManager.domains()
|
|
guard let domain = domains.first(where: { $0.identifier.rawValue == AppConfiguration.fileProviderDomainID }) else {
|
|
throw NSError(domain: "CoreView", code: 0, userInfo: [NSLocalizedDescriptionKey: "File provider domain not found"])
|
|
}
|
|
guard let manager = NSFileProviderManager(for: domain) else {
|
|
throw NSError(domain: "CoreView", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to get file provider manager"])
|
|
}
|
|
return manager
|
|
}
|
|
|
|
@available(iOS 16.0, *)
|
|
private nonisolated func notifyFileProviderWorkingDirectoryChanged() async {
|
|
do {
|
|
let manager = try await fileProviderManager()
|
|
try await manager.signalEnumerator(for: .rootContainer)
|
|
try await manager.signalEnumerator(for: .workingSet)
|
|
} catch {
|
|
await MainActor.run {
|
|
alert = AlertState(action: "notify Files app about working directory changes", error: error)
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(iOS 16.0, *)
|
|
private nonisolated func openInFilesApp() async {
|
|
do {
|
|
let manager = try await fileProviderManager()
|
|
try await manager.signalEnumerator(for: .workingSet)
|
|
let url = try await manager.getUserVisibleURL(for: .rootContainer)
|
|
guard let sharedURL = URL(string: "shareddocuments://\(url.path)") else {
|
|
throw NSError(domain: "CoreView", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to create shared documents URL"])
|
|
}
|
|
await MainActor.run {
|
|
UIApplication.shared.open(sharedURL)
|
|
}
|
|
} catch {
|
|
await MainActor.run {
|
|
alert = AlertState(action: "open working directory in Files", error: error)
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
private extension URL {
|
|
func formattedSize() throws -> String? {
|
|
guard let enumerator = FileManager.default.enumerator(
|
|
at: self,
|
|
includingPropertiesForKeys: [.totalFileAllocatedSizeKey]
|
|
) else {
|
|
return nil
|
|
}
|
|
var size = 0
|
|
while let url = enumerator.nextObject() as? URL {
|
|
size += try url.resourceValues(forKeys: [.totalFileAllocatedSizeKey]).totalFileAllocatedSize ?? 0
|
|
}
|
|
let formatter = ByteCountFormatter()
|
|
formatter.countStyle = .file
|
|
guard let byteCount = formatter.string(for: size) else {
|
|
return nil
|
|
}
|
|
return byteCount
|
|
}
|
|
}
|