Prepare system extension support

This commit is contained in:
世界
2023-07-24 20:08:02 +08:00
parent 8a5c1a80d1
commit 12bf02070e
44 changed files with 1469 additions and 382 deletions
+2 -89
View File
@@ -1,98 +1,11 @@
import ApplicationLibrary
import Library
import MacLibrary
import SwiftUI
@main
struct Application: App {
@NSApplicationDelegateAdaptor private var appDelegate: ApplicationDelegate
@State private var showMenuBarExtra = false
@State private var isMenuPresented = false
var body: some Scene {
Window("sing-box", id: "main", content: {
MainView()
.onAppear {
Task.detached {
await initialize()
}
}
.environment(\.showMenuBarExtra, $showMenuBarExtra)
})
.commands {
if showMenuBarExtra {
CommandGroup(replacing: .appTermination) {
Button("Quit sing-box") {
hide(closeApp: true)
}
.keyboardShortcut("q", modifiers: [.command])
}
CommandGroup(replacing: .saveItem) {
Button("Close") {
hide(closeApp: false)
}
.keyboardShortcut("w", modifiers: [.command])
}
}
SidebarCommands()
}
Window("New Profile", id: NewProfileView.windowID) {
NewProfileView()
}
WindowGroup("Edit Profile", id: EditProfileWindowView.windowID, for: Int64.self) { profileID in
EditProfileWindowView(profileID.wrappedValue)
}.commandsRemoved()
WindowGroup("Edit Content", id: EditProfileContentView.windowID, for: EditProfileContentView.Context.self) { context in
EditProfileContentView(context.wrappedValue)
}.commandsRemoved()
Window("Service Log", id: ServiceLogView.windowID) {
ServiceLogView()
}
MenuBarExtra(isInserted: $showMenuBarExtra) {
MenuView(isMenuPresented: $isMenuPresented)
} label: {
Image(systemName: "network.badge.shield.half.filled")
}
.menuBarExtraStyle(.window)
.menuBarExtraAccess(isPresented: $isMenuPresented)
}
private func initialize() {
let initialShowMenuBarExtra = SharedPreferences.showMenuBarExtra
DispatchQueue.main.async {
showMenuBarExtra = initialShowMenuBarExtra
}
}
private func hide(closeApp: Bool) {
Task.detached {
if SharedPreferences.menuBarExtraInBackground {
DispatchQueue.main.async {
hide0(closeApp: closeApp)
}
} else {
DispatchQueue.main.async {
if closeApp {
NSApp.terminate(nil)
} else {
NSApp.keyWindow?.close()
}
}
}
}
}
private func hide0(closeApp: Bool) {
if closeApp || NSApp.keyWindow?.identifier?.rawValue == "main" {
let transformState = ProcessApplicationTransformState(kProcessTransformToUIElementApplication)
var psn = ProcessSerialNumber(highLongOfPSN: 0, lowLongOfPSN: UInt32(kCurrentProcess))
TransformProcessType(&psn, transformState)
NSApp.setActivationPolicy(.accessory)
}
NSApp.keyWindow?.close()
MacApplication()
}
}
-52
View File
@@ -1,52 +0,0 @@
import AppKit
import ApplicationLibrary
import Foundation
import Libbox
import Library
class ApplicationDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_: Notification) {
NSLog("Here I stand")
// ServiceNotification.register() // Not work
let event = NSAppleEventManager.shared().currentAppleEvent
let launchedAsLogInItem =
event?.eventID == kAEOpenApplication &&
event?.paramDescriptor(forKeyword: keyAEPropData)?.enumCodeValue == keyAELaunchedAsLogInItem
if !launchedAsLogInItem || !SharedPreferences.showMenuBarExtra || !SharedPreferences.menuBarExtraInBackground {
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
} else {
NSApp.windows.first?.close()
}
Task.detached {
do {
try await self.postStart(launchedAsLogInItem)
} catch {
NSLog("application setup error: \(error.localizedDescription)")
}
}
}
private func postStart(_ launchedAsLogInItem: Bool) async throws {
try ProfileUpdateTask.setup()
if launchedAsLogInItem {
if SharedPreferences.startedByUser {
if let profile = try await ExtensionProfile.load() {
try await profile.start()
}
}
}
}
func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool {
!SharedPreferences.menuBarExtraInBackground
}
func applicationShouldHandleReopen(_: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
if !flag, NSApp.activationPolicy() == .accessory {
NSApp.setActivationPolicy(.regular)
NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.dock").first?.activate()
}
return true
}
}
-101
View File
@@ -1,101 +0,0 @@
import ApplicationLibrary
import Library
import SwiftUI
struct MainView: View {
@Environment(\.controlActiveState) private var controlActiveState
@State private var selection = NavigationPage.dashboard
@State private var extensionProfile: ExtensionProfile?
@State private var profileLoading = true
@State private var logClient: LogClient!
@State private var serviceNotificationTitle = ""
@State private var serviceNotificationContent = ""
@State private var serviceNotificationPresented = false
var body: some View {
NavigationSplitView {
VStack {
SidebarView()
}
.frame(minWidth: 150)
} detail: {
if profileLoading {
ProgressView().onAppear {
Task {
logClient = LogClient(SharedPreferences.maxLogLines)
await loadProfile()
}
}
} else {
selection.contentView
}
}
.alert(isPresented: $serviceNotificationPresented, content: {
Alert(
title: Text(serviceNotificationTitle),
message: Text(serviceNotificationContent),
dismissButton: .default(Text("Ok"))
)
})
.onAppear {
ServiceNotification.setServiceNotificationListener { notification in
serviceNotificationTitle = notification.title
serviceNotificationContent = notification.body
serviceNotificationPresented = true
}
}
.onDisappear {
ServiceNotification.removeServiceNotificationListener()
}
.toolbar {
ToolbarItem(placement: .navigation) {
StartStopButton()
}
}
.onChange(of: controlActiveState, perform: { newValue in
if newValue != .inactive {
Task {
await loadProfile()
connectLog()
}
}
})
.onChange(of: selection, perform: { value in
if value == .logs {
connectLog()
}
})
.formStyle(.grouped)
.environment(\.selection, $selection)
.environment(\.extensionProfile, $extensionProfile)
.environment(\.logClient, $logClient)
}
private func loadProfile() async {
defer {
profileLoading = false
}
if let newProfile = try? await ExtensionProfile.load() {
if extensionProfile == nil {
newProfile.register()
extensionProfile = newProfile
}
} else {
extensionProfile = nil
}
}
private func connectLog() {
guard let profile = extensionProfile else {
return
}
guard let logClient else {
return
}
if profile.status.isConnected, !logClient.isConnected {
logClient.reconnect()
}
}
}
-216
View File
@@ -1,216 +0,0 @@
import ApplicationLibrary
import Foundation
import Libbox
import Library
import MacControlCenterUI
import MenuBarExtraAccess
import SwiftUI
struct MenuView: View {
@Environment(\.openWindow) private var openWindow
private static let sliderWidth: CGFloat = 270
@Binding var isMenuPresented: Bool
@State private var isLoading = true
@State private var profile: ExtensionProfile?
var body: some View {
MacControlCenterMenu(isPresented: $isMenuPresented) {
MenuHeader("sing-box") {
if isLoading {
Text("Loading...").foregroundColor(.secondary).onAppear {
Task.detached {
await loadProfile()
}
}
} else if let profile {
Text(LibboxVersion()).foregroundColor(.secondary)
StatusSwitch(profile)
} else {
Text("NetworkExtension not installed")
}
}
.frame(minWidth: MenuView.sliderWidth)
if let profile {
ProfilePicker(profile)
}
Divider()
MenuCommand {
NSApp.setActivationPolicy(.regular)
openWindow(id: "main")
if let dockApp = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.dock").first {
dockApp.activate()
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
NSApp.activate(ignoringOtherApps: true)
}
}
} label: {
Text("Open")
}
MenuCommand {
NSApp.terminate(nil)
} label: {
Text("Quit")
}
}
}
private func loadProfile() async {
profile = try? await ExtensionProfile.load()
if let profile {
profile.register()
}
isLoading = false
}
private struct StatusSwitch: View {
@ObservedObject private var profile: ExtensionProfile
@State private var errorPresented = false
@State private var errorMessage = ""
init(_ profile: ExtensionProfile) {
self.profile = profile
}
var body: some View {
Toggle(isOn: Binding(get: {
profile.status.isConnected
}, set: { _ in
Task.detached {
await switchProfile(!profile.status.isConnected)
}
})) {}
.toggleStyle(.switch)
.disabled(!profile.status.isEnabled)
.alert(isPresented: $errorPresented) {
Alert(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("Ok"))
)
}
}
private func switchProfile(_ isEnabled: Bool) async {
do {
if isEnabled {
try await profile.start()
} else {
profile.stop()
}
} catch {
errorMessage = error.localizedDescription
errorPresented = true
return
}
}
}
private struct ProfilePicker: View {
@ObservedObject private var profile: ExtensionProfile
init(_ profile: ExtensionProfile) {
self.profile = profile
}
@State private var isLoading = true
@State private var profileList: [Profile] = []
@State private var selectedProfileID: Int64!
@State private var reasserting = false
@State private var errorPresented = false
@State private var errorMessage = ""
@State private var observer: Any?
var body: some View {
viewBuilder {
if isLoading {
ProgressView().onAppear {
Task.detached {
await doReload()
}
}
} else {
if profileList.isEmpty {
Text("Empty profiles")
} else {
MenuSection("Profile")
Picker("", selection: $selectedProfileID) {
ForEach(profileList, id: \.id) { profile in
Text(profile.name)
}
}
.pickerStyle(.inline)
.onChange(of: selectedProfileID) { _ in
reasserting = true
Task.detached {
await switchProfile(selectedProfileID!)
}
}
.disabled(!profile.status.isSwitchable || reasserting)
}
}
}
.onAppear {
if observer == nil {
observer = NotificationCenter.default.addObserver(forName: ActiveDashboardView.NotificationUpdateSelectedProfile, object: nil, queue: nil, using: { _ in
doReload()
})
}
}
.onDisappear {
if let observer {
NotificationCenter.default.removeObserver(observer)
}
}
.alert(isPresented: $errorPresented) {
Alert(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("Ok"))
)
}
}
private func doReload() {
defer {
isLoading = false
}
do {
profileList = try ProfileManager.list()
} catch {
errorMessage = error.localizedDescription
errorPresented = true
return
}
if profileList.isEmpty {
return
}
selectedProfileID = SharedPreferences.selectedProfileID
if profileList.filter({ profile in
profile.id == selectedProfileID
})
.isEmpty {
selectedProfileID = profileList[0].id!
SharedPreferences.selectedProfileID = selectedProfileID
}
}
private func switchProfile(_ newProfileID: Int64) {
SharedPreferences.selectedProfileID = newProfileID
NotificationCenter.default.post(name: ActiveDashboardView.NotificationUpdateSelectedProfile, object: nil)
if profile.status.isConnected {
do {
try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)?.serviceReload()
} catch {
errorMessage = error.localizedDescription
errorPresented = true
}
}
reasserting = false
}
}
}
-2
View File
@@ -28,7 +28,5 @@
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
-47
View File
@@ -1,47 +0,0 @@
import ApplicationLibrary
import Library
import SwiftUI
struct SidebarView: View {
@Environment(\.selection) private var selection
@Environment(\.extensionProfile) private var extensionProfile
var body: some View {
viewBuilder {
if let profile = extensionProfile.wrappedValue {
SidebarView0().environmentObject(profile)
} else {
SidebarView1()
}
}
}
struct SidebarView0: View {
@Environment(\.selection) private var selection
@EnvironmentObject private var extensionProfile: ExtensionProfile
var body: some View {
List(NavigationPage.allCases.filter { it in
it.visible(extensionProfile)
}, selection: selection) { it in
it.label
}.onChange(of: extensionProfile.status) { _ in
if !selection.wrappedValue.visible(extensionProfile) {
selection.wrappedValue = NavigationPage.dashboard
}
}
}
}
struct SidebarView1: View {
@Environment(\.selection) private var selection
var body: some View {
List(NavigationPage.allCases.filter { it in
it.visible(nil)
}, selection: selection) { it in
it.label
}
}
}
}