Prepare system extension support
This commit is contained in:
+2
-89
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,5 @@
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.server</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user