refactor: Fix macOS standalone application
This commit is contained in:
@@ -4,6 +4,8 @@ import SwiftUI
|
||||
#if os(iOS)
|
||||
import FileProvider
|
||||
import UIKit
|
||||
#elseif os(macOS)
|
||||
import ServiceManagement
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
@@ -15,7 +17,11 @@ public struct CoreView: View {
|
||||
@State private var disableDeprecatedWarnings = false
|
||||
|
||||
@State private var version = ""
|
||||
@State private var dataSize = ""
|
||||
@State private var dataSize: String?
|
||||
|
||||
#if os(macOS)
|
||||
@State private var helperUnavailable = false
|
||||
#endif
|
||||
|
||||
public init() {}
|
||||
public var body: some View {
|
||||
@@ -29,7 +35,21 @@ public struct CoreView: View {
|
||||
} else {
|
||||
FormView {
|
||||
FormTextItem("Version", version)
|
||||
FormTextItem("Data Size", dataSize)
|
||||
if let dataSize {
|
||||
FormTextItem("Data Size", dataSize)
|
||||
} else {
|
||||
#if os(macOS)
|
||||
HStack {
|
||||
Text("Data Size")
|
||||
Spacer()
|
||||
Text("Unavailable")
|
||||
.foregroundStyle(.red)
|
||||
.onTapGesture {
|
||||
alert = helperRequiredAlert()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if Variant.isBeta {
|
||||
Section {}
|
||||
@@ -40,10 +60,12 @@ public struct CoreView: View {
|
||||
|
||||
Section("Working Directory") {
|
||||
#if os(macOS)
|
||||
FormButton {
|
||||
NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: FilePath.workingDirectory.relativePath)
|
||||
} label: {
|
||||
Label("Open", systemImage: "macwindow.and.cursorarrow")
|
||||
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, *) {
|
||||
@@ -85,7 +107,11 @@ public struct CoreView: View {
|
||||
} else {
|
||||
await MainActor.run {
|
||||
version = LibboxVersion()
|
||||
dataSize = "Loading..."
|
||||
#if os(macOS)
|
||||
if Variant.useSystemExtension {
|
||||
helperUnavailable = HelperServiceManager.rootHelperStatus != .enabled
|
||||
}
|
||||
#endif
|
||||
isLoading = false
|
||||
}
|
||||
await loadSettingsBackground()
|
||||
@@ -94,7 +120,20 @@ public struct CoreView: View {
|
||||
|
||||
private nonisolated func loadSettingsBackground() async {
|
||||
let disableDeprecatedWarnings = await SharedPreferences.disableDeprecatedWarnings.get()
|
||||
let dataSize = (try? FilePath.workingDirectory.formattedSize()) ?? "Unknown"
|
||||
let dataSize: String?
|
||||
#if os(macOS)
|
||||
if Variant.useSystemExtension {
|
||||
if let size = try? RootHelperClient.shared.getWorkingDirectorySize() {
|
||||
dataSize = LibboxFormatBytes(size)
|
||||
} else {
|
||||
dataSize = nil
|
||||
}
|
||||
} else {
|
||||
dataSize = (try? FilePath.workingDirectory.formattedSize()) ?? "Unknown"
|
||||
}
|
||||
#else
|
||||
dataSize = (try? FilePath.workingDirectory.formattedSize()) ?? "Unknown"
|
||||
#endif
|
||||
await MainActor.run {
|
||||
self.disableDeprecatedWarnings = disableDeprecatedWarnings
|
||||
self.dataSize = dataSize
|
||||
@@ -102,6 +141,12 @@ public struct CoreView: View {
|
||||
}
|
||||
|
||||
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"),
|
||||
@@ -119,17 +164,44 @@ public struct CoreView: View {
|
||||
}
|
||||
|
||||
private func stopServiceAndDestroy() async {
|
||||
try? await environments.extensionProfile?.stop()
|
||||
await destroyWorkingDirectory()
|
||||
}
|
||||
|
||||
private nonisolated func destroyWorkingDirectory() async {
|
||||
try? FileManager.default.removeItem(at: FilePath.workingDirectory)
|
||||
await MainActor.run {
|
||||
isLoading = true
|
||||
do {
|
||||
try await environments.extensionProfile!.stop()
|
||||
await destroyWorkingDirectory()
|
||||
} catch {
|
||||
alert = AlertState(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
private func destroyWorkingDirectory() async {
|
||||
do {
|
||||
#if os(macOS)
|
||||
if Variant.useSystemExtension {
|
||||
try RootHelperClient.shared.cleanWorkingDirectory()
|
||||
} else {
|
||||
try FileManager.default.removeItem(at: FilePath.workingDirectory)
|
||||
}
|
||||
#else
|
||||
try FileManager.default.removeItem(at: FilePath.workingDirectory)
|
||||
#endif
|
||||
isLoading = true
|
||||
} catch {
|
||||
alert = AlertState(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
#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 openInFilesApp() async {
|
||||
|
||||
@@ -12,6 +12,11 @@ public struct AppView: View {
|
||||
@State private var startAtLogin = false
|
||||
@Environment(\.showMenuBarExtra) private var showMenuBarExtra
|
||||
@State private var menuBarExtraInBackground = false
|
||||
#if os(macOS)
|
||||
|
||||
@State private var rootHelperRegistrationStatus: SMAppService.Status = .notRegistered
|
||||
|
||||
#endif
|
||||
|
||||
@State private var alert: AlertState?
|
||||
|
||||
@@ -67,6 +72,48 @@ public struct AppView: View {
|
||||
Label("Uninstall", systemImage: "trash.fill").foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
if rootHelperRegistrationStatus == .enabled {
|
||||
FormButton {
|
||||
performHelperAction {
|
||||
try HelperServiceManager.unregisterRootHelper()
|
||||
try HelperServiceManager.registerRootHelper()
|
||||
}
|
||||
} label: {
|
||||
Label("Update", systemImage: "arrow.down.doc.fill")
|
||||
}
|
||||
FormButton(role: .destructive) {
|
||||
performHelperAction {
|
||||
try HelperServiceManager.unregisterRootHelper()
|
||||
}
|
||||
} label: {
|
||||
Label("Uninstall", systemImage: "trash.fill").foregroundColor(.red)
|
||||
}
|
||||
} else if rootHelperRegistrationStatus == .requiresApproval {
|
||||
FormButton {
|
||||
openHelperSettings()
|
||||
} label: {
|
||||
Label("Enable", systemImage: "switch.2")
|
||||
}
|
||||
} else {
|
||||
FormButton {
|
||||
performHelperAction {
|
||||
try HelperServiceManager.registerRootHelper()
|
||||
}
|
||||
} label: {
|
||||
Label("Install", systemImage: "square.and.arrow.down.fill")
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Helper Service")
|
||||
Text("This helper service provides process lookup for `process_name` and `process_path` routing rules, and manages the working directory.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.textCase(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -83,6 +130,9 @@ public struct AppView: View {
|
||||
#if os(macOS)
|
||||
startAtLogin = SMAppService.mainApp.status == .enabled
|
||||
menuBarExtraInBackground = await SharedPreferences.menuBarExtraInBackground.get()
|
||||
if Variant.useSystemExtension {
|
||||
refreshHelperStatus()
|
||||
}
|
||||
#endif
|
||||
isLoading = false
|
||||
}
|
||||
@@ -147,5 +197,31 @@ public struct AppView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func performHelperAction(_ action: () throws -> Void) {
|
||||
do {
|
||||
try action()
|
||||
refreshHelperStatus()
|
||||
} catch {
|
||||
alert = AlertState(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshHelperStatus() {
|
||||
rootHelperRegistrationStatus = HelperServiceManager.rootHelperStatus
|
||||
}
|
||||
|
||||
private func openHelperSettings() {
|
||||
if #available(macOS 13.0, *) {
|
||||
SMAppService.openSystemSettingsLoginItems()
|
||||
return
|
||||
}
|
||||
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.users?LoginItems"),
|
||||
NSWorkspace.shared.open(url)
|
||||
{
|
||||
return
|
||||
}
|
||||
NSWorkspace.shared.open(URL(fileURLWithPath: "/System/Applications/System Preferences.app"))
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -296,7 +296,7 @@ public struct OnDemandRulesView: View {
|
||||
await SharedPreferences.onDemandRules.set(rules)
|
||||
let savedRules = await SharedPreferences.onDemandRules.get()
|
||||
if savedRules != rules {
|
||||
alert = AlertState(errorMessage: "Failed to save rules")
|
||||
alert = AlertState(errorMessage: String(localized: "Failed to save rules"))
|
||||
return
|
||||
}
|
||||
await updateService()
|
||||
|
||||
@@ -19,7 +19,7 @@ struct PacketTunnelView: View {
|
||||
Group {
|
||||
if isLoading {
|
||||
ProgressView().onAppear {
|
||||
Task.detached {
|
||||
Task {
|
||||
await loadSettings()
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,7 @@ struct PacketTunnelView: View {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func loadSettings() async {
|
||||
ignoreMemoryLimit = await SharedPreferences.ignoreMemoryLimit.get()
|
||||
#if !os(tvOS)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
@@ -15,7 +14,7 @@ public struct ProfileOverrideView: View {
|
||||
Group {
|
||||
if isLoading {
|
||||
ProgressView().onAppear {
|
||||
Task.detached {
|
||||
Task {
|
||||
await loadSettings()
|
||||
}
|
||||
}
|
||||
@@ -64,12 +63,13 @@ public struct ProfileOverrideView: View {
|
||||
return
|
||||
}
|
||||
do {
|
||||
try LibboxNewStandaloneCommandClient()?.serviceReload()
|
||||
try await profile.reloadService()
|
||||
} catch {
|
||||
alert = AlertState(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func loadSettings() async {
|
||||
excludeDefaultRoute = await SharedPreferences.excludeDefaultRoute.get()
|
||||
autoRouteUseSubRangesByDefault = await SharedPreferences.autoRouteUseSubRangesByDefault.get()
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
#if os(macOS)
|
||||
private struct SettingsNavigationPathKey: EnvironmentKey {
|
||||
static let defaultValue: Binding<NavigationPath>? = nil
|
||||
}
|
||||
|
||||
public extension EnvironmentValues {
|
||||
var settingsNavigationPath: Binding<NavigationPath>? {
|
||||
get { self[SettingsNavigationPathKey.self] }
|
||||
set { self[SettingsNavigationPathKey.self] = newValue }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
public enum SettingsPage: Hashable {
|
||||
case app
|
||||
case core, packetTunnel, onDemandRules, profileOverride, sponsors
|
||||
}
|
||||
#endif
|
||||
|
||||
public struct SettingView: View {
|
||||
private enum Tabs: Int, CaseIterable, Identifiable {
|
||||
var id: Self {
|
||||
@@ -13,6 +33,25 @@ public struct SettingView: View {
|
||||
|
||||
case core, packetTunnel, onDemandRules, profileOverride, sponsors
|
||||
|
||||
#if os(macOS)
|
||||
var page: SettingsPage {
|
||||
switch self {
|
||||
case .app:
|
||||
return .app
|
||||
case .core:
|
||||
return .core
|
||||
case .packetTunnel:
|
||||
return .packetTunnel
|
||||
case .onDemandRules:
|
||||
return .onDemandRules
|
||||
case .profileOverride:
|
||||
return .profileOverride
|
||||
case .sponsors:
|
||||
return .sponsors
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
var label: some View {
|
||||
Label(title, systemImage: iconImage)
|
||||
}
|
||||
@@ -83,16 +122,45 @@ public struct SettingView: View {
|
||||
|
||||
@MainActor
|
||||
var navigationLink: some View {
|
||||
FormNavigationLink {
|
||||
contentView
|
||||
} label: {
|
||||
label
|
||||
}
|
||||
#if os(macOS)
|
||||
FormNavigationLink(value: page) {
|
||||
label
|
||||
}
|
||||
#else
|
||||
FormNavigationLink {
|
||||
contentView
|
||||
} label: {
|
||||
label
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@StateObject private var viewModel = SettingViewModel()
|
||||
#if os(macOS)
|
||||
@MainActor
|
||||
@ViewBuilder
|
||||
private static func destinationView(for page: SettingsPage) -> some View {
|
||||
Group {
|
||||
switch page {
|
||||
case .app:
|
||||
AppView()
|
||||
case .core:
|
||||
CoreView()
|
||||
case .packetTunnel:
|
||||
PacketTunnelView()
|
||||
case .onDemandRules:
|
||||
OnDemandRulesView()
|
||||
case .profileOverride:
|
||||
ProfileOverrideView()
|
||||
case .sponsors:
|
||||
SponsorsView()
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||
}
|
||||
#endif
|
||||
|
||||
@StateObject private var viewModel = SettingViewModel()
|
||||
public init() {}
|
||||
public var body: some View {
|
||||
FormView {
|
||||
@@ -159,5 +227,10 @@ public struct SettingView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
.formNavigationDestination(for: SettingsPage.self) { page in
|
||||
Self.destinationView(for: page)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user