import Library import SwiftUI #if os(macOS) import AppKit import ServiceManagement #endif public struct AppView: View { private struct LanguageOption: Hashable { let code: String? let name: String } private static var supportedLanguages: [LanguageOption] { var options = [LanguageOption(code: nil, name: String(localized: "System Default"))] options.append(contentsOf: configuredLanguageCodes().map { code in let name = Locale(identifier: code).localizedString(forIdentifier: code) ?? code return LanguageOption(code: code, name: name) }) return options } @State private var isLoading = true @State private var selectedLanguage: String? #if os(macOS) @State private var startAtLogin = false @Environment(\.showMenuBarExtra) private var showMenuBarExtra @Environment(\.menuBarExtraSpeedMode) private var menuBarExtraSpeedMode @State private var menuBarExtraInBackground = false @State private var helperStatusLoaded = false @State private var rootHelperRegistrationStatus: SMAppService.Status = .notRegistered @EnvironmentObject private var environments: ExtensionEnvironments @EnvironmentObject private var updateManager: UpdateManager @State private var updateTrack: UpdateTrack = .stable @State private var checkUpdateEnabled = false @State private var cacheSize: Int64 = 0 @State private var cacheSizeText = "" #endif @State private var alert: AlertState? public init() {} public var body: some View { Group { if isLoading { ProgressView().onAppear { Task { await loadSettings() } } } else { FormView { Picker("Language", selection: $selectedLanguage) { ForEach(Self.supportedLanguages, id: \.code) { language in Text(language.name).tag(language.code) } } .onChangeCompat(of: selectedLanguage) { newValue in updateLanguage(newValue) } #if os(macOS) FormToggle("Start At Login", "Launch the application when the system is logged in. If enabled at the same time as `Show in Menu Bar` and `Keep Menu Bar in Background`, the application interface will not be opened automatically.", $startAtLogin) { newValue in updateLoginItems(newValue) } Toggle("Show in Menu Bar", isOn: showMenuBarExtra) .onChangeCompat(of: showMenuBarExtra.wrappedValue) { newValue in Task { await SharedPreferences.showMenuBarExtra.set(newValue) if !newValue { menuBarExtraInBackground = false } } } if showMenuBarExtra.wrappedValue { Picker("Real-time Speed", selection: menuBarExtraSpeedMode) { ForEach(MenuBarExtraSpeedMode.allCases, id: \.rawValue) { mode in Text(mode.name).tag(mode.rawValue) } } .onChangeCompat(of: menuBarExtraSpeedMode.wrappedValue) { newValue in Task { await SharedPreferences.menuBarExtraSpeedMode.set(newValue) } } Toggle("Keep Menu Bar in Background", isOn: $menuBarExtraInBackground) .onChangeCompat(of: menuBarExtraInBackground) { newValue in Task { await SharedPreferences.menuBarExtraInBackground.set(newValue) } } } if Variant.useSystemExtension { FormTextItem("Cache Size", cacheSizeText) if cacheSize > 0 { // Safe: System Extension's working directory is in its own container // (/var/root/Library/Containers/…), not under the app's cacheDirectory. FormButton(role: .destructive) { Task.detached { let cacheDir = FilePath.cacheDirectory if let contents = try? FileManager.default.contentsOfDirectory( at: cacheDir, includingPropertiesForKeys: nil ) { for item in contents { try? FileManager.default.removeItem(at: item) } } await MainActor.run { cacheSize = 0 cacheSizeText = ByteCountFormatter.string(fromByteCount: 0, countStyle: .file) } } } label: { Label("Clear Cache", systemImage: "trash") .foregroundColor(.red) } } } if Variant.useSystemExtension { Section("Update Settings") { Picker("Update Track", selection: $updateTrack) { Text("Stable").tag(UpdateTrack.stable) Text("Beta").tag(UpdateTrack.beta) } .onChangeCompat(of: updateTrack) { newValue in Task { await updateManager.updateTrackChanged(to: newValue) } } Toggle("Automatic Update Check", isOn: $checkUpdateEnabled) .onChangeCompat(of: checkUpdateEnabled) { newValue in Task { await SharedPreferences.checkUpdateEnabled.set(newValue) } } FormButton { Task { do { if try await updateManager.refreshUpdateInfo() != nil { await updateManager.showUpdateSheet() } else { alert = AlertState( title: String(localized: "Check Update"), message: String(localized: "No updates available") ) } } catch {} } } label: { if updateManager.isChecking { HStack(spacing: 6) { ProgressView() .controlSize(.small) Text("Checking...") } } else { Label("Check Update", systemImage: "arrow.triangle.2.circlepath") } } .disabled(updateManager.isChecking) .contextMenu { Button("Force Show Latest Version as Update") { Task { do { if try await updateManager.refreshUpdateInfo(force: true) != nil { await updateManager.showUpdateSheet() } else { alert = AlertState( title: String(localized: "Check Update"), message: String(localized: "No updates available") ) } } catch {} } } .disabled(updateManager.isChecking) } if let info = updateManager.updateInfo { FormButton { Task { await updateManager.showUpdateSheet() } } label: { HStack { Label("Update", systemImage: "arrow.down.circle") Spacer() Text("v\(info.versionName)") .foregroundStyle(.secondary) } } } } Section("System Extension") { FormButton { Task { await updateSystemExtension() } } label: { Label("Update", systemImage: "arrow.down.doc.fill") } FormButton(role: .destructive) { Task { await uninstallSystemExtension() } } label: { Label("Uninstall", systemImage: "trash.fill").foregroundColor(.red) } } Section { if !helperStatusLoaded { ProgressView() } else if rootHelperRegistrationStatus == .enabled { FormButton { Task { do { try HelperServiceManager.unregisterRootHelper() try await Task.sleep(for: .seconds(1)) try HelperServiceManager.registerRootHelper() refreshHelperStatus() } catch { alert = AlertState(action: "update helper service", error: error) } } } label: { Label("Update", systemImage: "arrow.down.doc.fill") } FormButton(role: .destructive) { performHelperAction(actionName: "uninstall helper service") { 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(actionName: "install helper service") { 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 } } } .alert($alert) #if os(macOS) .alert($updateManager.alert) #endif .navigationTitle("App") #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif } private func loadSettings() async { selectedLanguage = Self.currentLanguage() #if os(macOS) startAtLogin = SMAppService.mainApp.status == .enabled menuBarExtraInBackground = await SharedPreferences.menuBarExtraInBackground.get() if Variant.useSystemExtension { let trackString = await SharedPreferences.updateTrack.get() updateTrack = UpdateTrack.resolved(from: trackString) checkUpdateEnabled = await SharedPreferences.checkUpdateEnabled.get() } #endif isLoading = false #if os(macOS) if Variant.useSystemExtension { refreshHelperStatus() helperStatusLoaded = true refreshCacheSize() } #endif } private static func currentLanguage() -> String? { guard let languages = UserDefaults.standard.array(forKey: "AppleLanguages") as? [String], let first = languages.first else { return nil } let current = canonicalLanguageCode(first) for language in supportedLanguages { guard let code = language.code else { continue } if current == code || current.hasPrefix("\(code)-") || current.hasPrefix("\(code)_") { return code } } return nil } private func updateLanguage(_ language: String?) { if let language { UserDefaults.standard.set([Self.canonicalLanguageCode(language)], forKey: "AppleLanguages") } else { UserDefaults.standard.removeObject(forKey: "AppleLanguages") } alert = AlertState( title: String(localized: "Restart Required"), message: String(localized: "Language will be changed after restarting the app.") ) } private static func configuredLanguageCodes() -> [String] { let rawCodes: [String] if let configured = Bundle.main.object(forInfoDictionaryKey: "CFBundleLocalizations") as? [String], !configured.isEmpty { rawCodes = configured } else if let development = Bundle.main.developmentLocalization, !development.isEmpty { rawCodes = [development] } else { rawCodes = [] } var seen = Set() var codes: [String] = [] for rawCode in rawCodes { let trimmed = rawCode.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { continue } let canonical = canonicalLanguageCode(trimmed) guard !canonical.isEmpty, seen.insert(canonical).inserted else { continue } codes.append(canonical) } return codes } private static func canonicalLanguageCode(_ code: String) -> String { Locale.canonicalLanguageIdentifier(from: code) } #if os(macOS) private func updateLoginItems(_ startAtLogin: Bool) { do { if startAtLogin { if SMAppService.mainApp.status == .enabled { try? SMAppService.mainApp.unregister() } try SMAppService.mainApp.register() } else { try SMAppService.mainApp.unregister() } } catch { alert = AlertState(action: "update login items", error: error) } } private func updateSystemExtension() async { do { if let result = try await SystemExtension.install(forceUpdate: true) { switch result { case .completed: alert = AlertState( title: String(localized: "Update"), message: String(localized: "System Extension updated.") ) case .willCompleteAfterReboot: alert = AlertState( title: String(localized: "Update"), message: String(localized: "Reboot required.") ) } } } catch { alert = AlertState(action: "update system extension", error: error) } } private func uninstallSystemExtension() async { do { if let result = try await SystemExtension.uninstall() { switch result { case .completed: alert = AlertState( title: String(localized: "Uninstall"), message: String(localized: "System Extension removed.") ) case .willCompleteAfterReboot: alert = AlertState( title: String(localized: "Uninstall"), message: String(localized: "Reboot required.") ) } } } catch { alert = AlertState(action: "uninstall system extension", error: error) } } private func performHelperAction(actionName: String, _ action: () throws -> Void) { do { try action() refreshHelperStatus() } catch { alert = AlertState(action: actionName, 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")) } private func refreshCacheSize() { Task.detached { let size = Self.calculateDirSize(FilePath.cacheDirectory) await MainActor.run { cacheSize = size cacheSizeText = ByteCountFormatter.string(fromByteCount: size, countStyle: .file) } } } private static func calculateDirSize(_ dir: URL) -> Int64 { guard let enumerator = FileManager.default.enumerator( at: dir, includingPropertiesForKeys: [.fileSizeKey], options: [.skipsHiddenFiles] ) else { return 0 } var size: Int64 = 0 for case let fileURL as URL in enumerator { if let fileSize = try? fileURL.resourceValues(forKeys: [.fileSizeKey]).fileSize { size += Int64(fileSize) } } return size } #endif }