Files
sing-box-for-apple/MacLibrary/MenuView.swift
T

204 lines
6.4 KiB
Swift

import ApplicationLibrary
import Foundation
import Libbox
import Library
import MacControlCenterUI
import MenuBarExtraAccess
import SwiftUI
@MainActor
public struct MenuView: View {
@Environment(\.openWindow) private var openWindow
private static let sliderWidth: CGFloat = 270
@Binding private var isMenuPresented: Bool
@State private var isLoading = true
@State private var profile: ExtensionProfile?
public init(isMenuPresented: Binding<Bool>) {
_isMenuPresented = isMenuPresented
}
public var body: some View {
MacControlCenterMenu(isPresented: $isMenuPresented) {
MenuHeader("sing-box") {
if isLoading {
Text("Loading...").foregroundColor(.secondary).onAppear {
Task {
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 alert: AlertState?
init(_ profile: ExtensionProfile) {
self.profile = profile
}
var body: some View {
Toggle(isOn: Binding(get: {
profile.status.isConnected
}, set: { _ in
Task {
await switchProfile(!profile.status.isConnected)
}
})) {}
.toggleStyle(.switch)
.disabled(!profile.status.isEnabled)
.alert($alert)
}
private func switchProfile(_ isEnabled: Bool) async {
do {
if isEnabled {
try await profile.start()
} else {
try await profile.stop()
}
} catch {
alert = AlertState(error: error)
return
}
}
}
private struct ProfilePicker: View {
@EnvironmentObject private var environments: ExtensionEnvironments
@ObservedObject private var profile: ExtensionProfile
init(_ profile: ExtensionProfile) {
self.profile = profile
}
@State private var isLoading = true
@State private var profileList: [ProfilePreview] = []
@State private var selectedProfileID: Int64 = 0
@State private var reasserting = false
@State private var alert: AlertState?
private var selectedProfileIDLocal: Binding<Int64> {
$selectedProfileID.withSetter { newValue in
reasserting = true
Task { [self] in
await switchProfile(newValue)
}
}
}
var body: some View {
Group {
if isLoading {
ProgressView().onAppear {
Task {
await doReload()
}
}
} else {
if profileList.isEmpty {
Text("Empty profiles")
} else {
MenuSection(String(localized: "Profile"))
Picker("", selection: selectedProfileIDLocal) {
ForEach(profileList, id: \.id) { profile in
Text(profile.name)
}
}
.pickerStyle(.inline)
.disabled(!profile.status.isSwitchable || reasserting)
}
}
}
.onReceive(environments.profileUpdate) { _ in
Task {
await doReload()
}
}
.onReceive(environments.selectedProfileUpdate) { _ in
Task {
selectedProfileID = await SharedPreferences.selectedProfileID.get()
}
}
.alert($alert)
}
private func doReload() async {
defer {
isLoading = false
}
do {
profileList = try await ProfileManager.list().map { ProfilePreview($0) }
} catch {
alert = AlertState(error: error)
return
}
if profileList.isEmpty {
return
}
selectedProfileID = await SharedPreferences.selectedProfileID.get()
if profileList.filter({ profile in
profile.id == selectedProfileID
})
.isEmpty {
selectedProfileID = profileList[0].id
await SharedPreferences.selectedProfileID.set(selectedProfileID)
}
}
private func switchProfile(_ newProfileID: Int64) async {
await SharedPreferences.selectedProfileID.set(newProfileID)
environments.selectedProfileUpdate.send()
if profile.status.isConnected {
do {
try await profile.reloadService()
} catch {
alert = AlertState(error: error)
}
}
reasserting = false
}
}
}