refactor: Fix macOS standalone application

This commit is contained in:
世界
2026-01-05 01:21:24 +08:00
parent ef1c924c64
commit 59317f3c79
66 changed files with 2799 additions and 585 deletions
+88 -16
View File
@@ -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
}
}