This commit is contained in:
世界
2023-10-02 22:12:33 +08:00
parent c1c4f084b9
commit c5a29e0956
16 changed files with 256 additions and 247 deletions
@@ -0,0 +1,12 @@
import Foundation
import SwiftUI
public extension Binding {
func withSetter(_ setter: @escaping (Value) -> Void) -> Binding<Value> {
Binding {
wrappedValue
} set: { [setter] newValue, _ in
setter(newValue)
}
}
}
@@ -7,10 +7,11 @@ import SwiftUI
public struct ActiveDashboardView: View {
@Environment(\.scenePhase) var scenePhase
@Environment(\.selection) private var parentSelection
@EnvironmentObject private var environments: ExtensionEnvironments
@EnvironmentObject private var profile: ExtensionProfile
@State private var isLoading = true
@State private var profileList: [Profile] = []
@State private var selectedProfileID: Int64!
@State private var profileList: [ProfilePreview] = []
@State private var selectedProfileID: Int64 = 0
@State private var alert: Alert?
@State private var selection = DashboardPage.overview
@State private var systemProxyAvailable = false
@@ -73,22 +74,19 @@ public struct ActiveDashboardView: View {
OverviewView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled)
#endif
}
#if os(iOS) || os(tvOS)
.onChangeCompat(of: scenePhase) { newValue in
if newValue == .active {
Task {
await doReload()
.onReceive(environments.profileUpdate) { _ in
Task {
await doReload()
}
}
.onReceive(environments.selectedProfileUpdate) { _ in
Task {
selectedProfileID = await SharedPreferences.selectedProfileID.get()
if profile.status.isConnected {
await doReloadSystemProxy()
}
}
}
.onChangeCompat(of: parentSelection.wrappedValue) { newValue in
if newValue == .dashboard {
Task {
await doReload()
}
}
}
#endif
.alertBinding($alert)
}
@@ -98,8 +96,8 @@ public struct ActiveDashboardView: View {
}
if ApplicationLibrary.inPreview {
profileList = [
Profile(id: 0, name: "profile local", type: .local, path: ""),
Profile(id: 1, name: "profile remote", type: .remote, path: "", lastUpdated: Date(timeIntervalSince1970: 0)),
ProfilePreview(Profile(id: 0, name: "profile local", type: .local, path: "")),
ProfilePreview(Profile(id: 1, name: "profile remote", type: .remote, path: "", lastUpdated: Date(timeIntervalSince1970: 0))),
]
systemProxyAvailable = true
systemProxyEnabled = true
@@ -107,7 +105,7 @@ public struct ActiveDashboardView: View {
} else {
do {
profileList = try await ProfileManager.list()
profileList = try await ProfileManager.list().map { ProfilePreview($0) }
if profileList.isEmpty {
return
}
@@ -116,7 +114,7 @@ public struct ActiveDashboardView: View {
profile.id == selectedProfileID
})
.isEmpty {
selectedProfileID = profileList[0].id!
selectedProfileID = profileList[0].id
await SharedPreferences.selectedProfileID.set(selectedProfileID)
}
@@ -31,7 +31,7 @@ public extension DashboardPage {
}
@MainActor
func contentView(_ profileList: Binding<[Profile]>, _ selectedProfileID: Binding<Int64?>, _ systemProxyAvailable: Binding<Bool>, _ systemProxyEnabled: Binding<Bool>) -> some View {
func contentView(_ profileList: Binding<[ProfilePreview]>, _ selectedProfileID: Binding<Int64>, _ systemProxyAvailable: Binding<Bool>, _ systemProxyEnabled: Binding<Bool>) -> some View {
viewBuilder {
switch self {
case .overview:
@@ -5,19 +5,26 @@ import SwiftUI
@MainActor
public struct OverviewView: View {
public static let NotificationUpdateSelectedProfile = Notification.Name("update-selected-profile")
@Environment(\.selection) private var selection
@EnvironmentObject private var environments: ExtensionEnvironments
@EnvironmentObject private var profile: ExtensionProfile
@Binding private var profileList: [Profile]
@Binding private var selectedProfileID: Int64!
@Binding private var profileList: [ProfilePreview]
@Binding private var selectedProfileID: Int64
@Binding private var systemProxyAvailable: Bool
@Binding private var systemProxyEnabled: Bool
@State private var alert: Alert?
@State private var reasserting = false
@State private var observer: Any?
public init(_ profileList: Binding<[Profile]>, _ selectedProfileID: Binding<Int64?>, _ systemProxyAvailable: Binding<Bool>, _ systemProxyEnabled: Binding<Bool>) {
private var selectedProfileIDLocal: Binding<Int64> {
$selectedProfileID.withSetter { newValue in
reasserting = true
Task { [self] in
await switchProfile(newValue)
}
}
}
public init(_ profileList: Binding<[ProfilePreview]>, _ selectedProfileID: Binding<Int64>, _ systemProxyAvailable: Binding<Bool>, _ systemProxyEnabled: Binding<Bool>) {
_profileList = profileList
_selectedProfileID = selectedProfileID
_systemProxyAvailable = systemProxyAvailable
@@ -45,7 +52,7 @@ public struct OverviewView: View {
}
}
Section("Profile") {
Picker(selection: $selectedProfileID) {
Picker(selection: selectedProfileIDLocal) {
ForEach(profileList, id: \.id) { profile in
Text(profile.name).tag(profile.id)
}
@@ -63,7 +70,7 @@ public struct OverviewView: View {
}
Section("Profile") {
ForEach(profileList, id: \.id) { profile in
Picker(profile.name, selection: $selectedProfileID) {
Picker(profile.name, selection: selectedProfileIDLocal) {
Text("").tag(profile.id)
}
}
@@ -74,44 +81,24 @@ public struct OverviewView: View {
}
}
.alertBinding($alert)
.onChangeCompat(of: selectedProfileID) {
reasserting = true
Task {
await switchProfile(selectedProfileID!)
}
}
.disabled(!ApplicationLibrary.inPreview && (!profile.status.isSwitchable || reasserting))
#if os(macOS)
.onAppear {
if observer == nil {
observer = NotificationCenter.default.addObserver(forName: OverviewView.NotificationUpdateSelectedProfile, object: nil, queue: nil, using: { newProfileID in
selectedProfileID = newProfileID.object as! Int64
})
}
}
.onDisappear {
if let observer {
NotificationCenter.default.removeObserver(observer)
}
}
#endif
}
private nonisolated func switchProfile(_ newProfileID: Int64) async {
private func switchProfile(_ newProfileID: Int64) async {
await SharedPreferences.selectedProfileID.set(newProfileID)
NotificationCenter.default.post(name: OverviewView.NotificationUpdateSelectedProfile, object: newProfileID)
if await profile.status.isConnected {
environments.selectedProfileUpdate.send()
if profile.status.isConnected {
do {
try LibboxNewStandaloneCommandClient()!.serviceReload()
try await serviceReload()
} catch {
await MainActor.run {
alert = Alert(error)
}
alert = Alert(error)
}
}
await MainActor.run {
reasserting = false
}
reasserting = false
}
private nonisolated func serviceReload() async throws {
try LibboxNewStandaloneCommandClient()?.serviceReload()
}
private nonisolated func setSystemProxyEnabled(_ isEnabled: Bool) async {
+58 -50
View File
@@ -5,53 +5,30 @@ public struct LogView: View {
@Environment(\.selection) private var selection
@EnvironmentObject private var environments: ExtensionEnvironments
private let logFont = Font.system(.caption2, design: .monospaced)
public init() {}
public var body: some View {
if ApplicationLibrary.inPreview {
let logList = [
"(packet-tunnel) log server started",
"INFO[0000] router: loaded geoip database: 250 codes",
"INFO[0000] router: loaded geosite database: 1400 codes",
"INFO[0000] router: updated default interface en0, index 11",
"inbound/tun[0]: started at utun3",
"sing-box started (1.666s)",
]
ScrollView {
VStack(alignment: .leading, spacing: 0) {
ForEach(Array(logList.enumerated()), id: \.offset) { it in
Text(it.element)
.font(logFont)
#if os(tvOS)
.focusable()
#endif
Spacer(minLength: 8)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding()
}
#if os(tvOS)
.focusEffectDisabled()
.focusSection()
#endif
} else if environments.logClient.logList.isEmpty {
VStack {
if environments.logClient.isConnected {
Text("Empty logs")
} else {
Text("Service not started").onAppear {
environments.connectLog()
}
}
}
} else {
ScrollViewReader { reader in
LogView0().environmentObject(environments.logClient)
}
private struct LogView0: View {
@EnvironmentObject private var environments: ExtensionEnvironments
@EnvironmentObject private var logClient: CommandClient
private let logFont = Font.system(.caption2, design: .monospaced)
var body: some View {
if ApplicationLibrary.inPreview {
let logList = [
"(packet-tunnel) log server started",
"INFO[0000] router: loaded geoip database: 250 codes",
"INFO[0000] router: loaded geosite database: 1400 codes",
"INFO[0000] router: updated default interface en0, index 11",
"inbound/tun[0]: started at utun3",
"sing-box started (1.666s)",
]
ScrollView {
VStack(alignment: .leading, spacing: 0) {
ForEach(Array(environments.logClient.logList.enumerated()), id: \.offset) { it in
ForEach(Array(logList.enumerated()), id: \.offset) { it in
Text(it.element)
.font(logFont)
#if os(tvOS)
@@ -59,12 +36,6 @@ public struct LogView: View {
#endif
Spacer(minLength: 8)
}
.onChangeCompat(of: environments.logClient.logList.count) { newCount in
withAnimation {
reader.scrollTo(newCount - 1)
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding()
@@ -73,8 +44,45 @@ public struct LogView: View {
.focusEffectDisabled()
.focusSection()
#endif
.onAppear {
reader.scrollTo(environments.logClient.logList.count - 1)
} else if logClient.logList.isEmpty {
VStack {
if logClient.isConnected {
Text("Empty logs")
} else {
Text("Service not started").onAppear {
environments.connectLog()
}
}
}
} else {
ScrollViewReader { reader in
ScrollView {
VStack(alignment: .leading, spacing: 0) {
ForEach(Array(logClient.logList.enumerated()), id: \.offset) { it in
Text(it.element)
.font(logFont)
#if os(tvOS)
.focusable()
#endif
Spacer(minLength: 8)
}
.onChangeCompat(of: logClient.logList.count) { newCount in
withAnimation {
reader.scrollTo(newCount - 1)
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding()
}
#if os(tvOS)
.focusEffectDisabled()
.focusSection()
#endif
.onAppear {
reader.scrollTo(logClient.logList.count - 1)
}
}
}
}
@@ -7,17 +7,15 @@ public struct EditProfileView: View {
@Environment(\.openWindow) private var openWindow
#endif
@EnvironmentObject private var environments: ExtensionEnvironments
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var profile: Profile
@State private var isLoading = false
@State private var isChanged = false
@State private var alert: Alert?
private let updateCallback: (() -> Void)?
public init(_ updateCallback: (() -> Void)? = nil) {
self.updateCallback = updateCallback
}
public init() {}
public var body: some View {
FormView {
FormItem("Name") {
@@ -170,7 +168,7 @@ public struct EditProfileView: View {
do {
try await Task.sleep(nanoseconds: UInt64(100 * Double(NSEC_PER_MSEC)))
try await profile.updateRemoteProfile()
await performCallback()
environments.profileUpdate.send()
} catch {
alert = Alert(error)
}
@@ -183,7 +181,7 @@ public struct EditProfileView: View {
alert = Alert(error)
return
}
await performCallback()
environments.profileUpdate.send()
dismiss()
}
@@ -201,14 +199,6 @@ public struct EditProfileView: View {
}
isChanged = false
isLoading = false
await performCallback()
}
private func performCallback() async {
if let updateCallback {
updateCallback()
} else {
NotificationCenter.default.post(name: ProfileView.notificationName, object: nil)
}
environments.profileUpdate.send()
}
}
@@ -9,6 +9,7 @@ public struct NewProfileView: View {
public static let windowID = "new-profile"
#endif
@EnvironmentObject private var environments: ExtensionEnvironments
@Environment(\.dismiss) private var dismiss
@State private var isSaving = false
@@ -27,9 +28,7 @@ public struct NewProfileView: View {
public let url: String
}
private let callback: (() async -> Void)?
public init(_ importRequest: ImportRequest? = nil, _ callback: (() async -> Void)? = nil) {
self.callback = callback
public init(_ importRequest: ImportRequest? = nil) {
if let importRequest {
_profileName = .init(initialValue: importRequest.name)
_profileType = .init(initialValue: .remote)
@@ -156,12 +155,9 @@ public struct NewProfileView: View {
alert = Alert(error)
return
}
if let callback {
await callback()
}
environments.profileUpdate.send()
dismiss()
#if os(macOS)
NotificationCenter.default.post(name: ProfileView.notificationName, object: nil)
resetFields()
#endif
}
@@ -6,8 +6,7 @@ import SwiftUI
@MainActor
public struct ProfileView: View {
public static let notificationName = Notification.Name("\(FilePath.packageName).update-profile")
@EnvironmentObject private var environments: ExtensionEnvironments
@Environment(\.importProfile) private var importProfile
@Environment(\.importRemoteProfile) private var importRemoteProfile
@State private var importRemoteProfileRequest: NewProfileView.ImportRequest?
@@ -17,7 +16,7 @@ public struct ProfileView: View {
@State private var isUpdating = false
@State private var alert: Alert?
@State private var profileList: [Profile] = []
@State private var profileList: [ProfilePreview] = []
#if os(iOS) || os(tvOS)
@State private var editMode = EditMode.inactive
@@ -29,10 +28,7 @@ public struct ProfileView: View {
@Environment(\.devicePickerSupports) private var devicePickerSupports
#endif
@State private var observer: Any?
public init() {}
public var body: some View {
VStack {
if isLoading {
@@ -46,17 +42,13 @@ public struct ProfileView: View {
ZStack {
if let importRemoteProfileRequest {
NavigationDestinationCompat(isPresented: $importRemoteProfilePresented) {
NewProfileView(importRemoteProfileRequest) {
await doReload()
}
NewProfileView(importRemoteProfileRequest)
}
}
FormView {
#if os(iOS)
NavigationLink {
NewProfileView {
await doReload()
}
NewProfileView()
} label: {
Text("New Profile").foregroundColor(.accentColor)
}
@@ -64,9 +56,7 @@ public struct ProfileView: View {
#elseif os(tvOS)
Section {
NavigationLink {
NewProfileView {
await doReload()
}
NewProfileView()
} label: {
Text("New Profile").foregroundColor(.accentColor)
}
@@ -85,7 +75,7 @@ public struct ProfileView: View {
Text("Empty profiles")
} else {
List {
ForEach(profileList, id: \.mustID) { profile in
ForEach(profileList, id: \.id) { profile in
viewBuilder {
if editMode.isEditing == true {
Text(profile.name)
@@ -106,7 +96,7 @@ public struct ProfileView: View {
} else {
FormView {
List {
ForEach(profileList, id: \.mustID) { profile in
ForEach(profileList, id: \.id) { profile in
ProfileItem(self, profile)
}
.onMove(perform: moveProfile)
@@ -128,15 +118,6 @@ public struct ProfileView: View {
importRemoteProfile.wrappedValue = nil
createImportRemoteProfileDialog(remoteProfile)
}
#if os(macOS)
if observer == nil {
observer = NotificationCenter.default.addObserver(forName: ProfileView.notificationName, object: nil, queue: .main) { _ in
Task {
await doReload()
}
}
}
#endif
}
.onChangeCompat(of: importProfile.wrappedValue) { newValue in
if let newValue {
@@ -150,23 +131,14 @@ public struct ProfileView: View {
createImportRemoteProfileDialog(newValue)
}
}
#if os(macOS)
.onDisappear {
if let observer {
NotificationCenter.default.removeObserver(observer)
}
observer = nil
.onReceive(environments.profileUpdate) { _ in
profileList = []
isLoading = true
// not updated, but why?
// Task {
// await doReload()
// }
}
.toolbar {
ToolbarItem {
Button {
openWindow(id: NewProfileView.windowID)
} label: {
Label("New Profile", systemImage: "plus.square.fill")
}
}
}
#endif
#if os(iOS)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
@@ -232,18 +204,15 @@ public struct ProfileView: View {
private func doReload() async {
if ApplicationLibrary.inPreview {
profileList = [
Profile(id: 0, name: "profile local", type: .local, path: ""),
Profile(id: 1, name: "profile remote", type: .remote, path: "", lastUpdated: Date(timeIntervalSince1970: 0)),
ProfilePreview(Profile(id: 0, name: "profile local", type: .local, path: "")),
ProfilePreview(Profile(id: 1, name: "profile remote", type: .remote, path: "", lastUpdated: Date(timeIntervalSince1970: 0))),
]
} else {
defer {
isLoading = false
}
do {
if !profileList.isEmpty {
profileList.removeAll()
}
profileList = try await ProfileManager.list()
profileList = try await ProfileManager.list().map { ProfilePreview($0) }
} catch {
alert = Alert(error)
return
@@ -279,11 +248,12 @@ public struct ProfileView: View {
private func moveProfile(from source: IndexSet, to destination: Int) {
profileList.move(fromOffsets: source, toOffset: destination)
for (index, profile) in profileList.enumerated() {
profile.order = UInt32(index)
profileList[index].order = UInt32(index)
profile.origin.order = UInt32(index)
}
Task {
do {
try await ProfileManager.update(profileList)
try await ProfileManager.update(profileList.map(\.origin))
} catch {
alert = Alert(error)
}
@@ -292,7 +262,7 @@ public struct ProfileView: View {
private func deleteProfile(where profileIndex: IndexSet) {
let profileToDelete = profileIndex.map { index in
profileList[index]
profileList[index].origin
}
profileList.remove(atOffsets: profileIndex)
Task {
@@ -306,16 +276,16 @@ public struct ProfileView: View {
public struct ProfileItem: View {
private let parent: ProfileView
private let profile: Profile
public init(_ parent: ProfileView, _ profile: Profile) {
@State private var profile: ProfilePreview
public init(_ parent: ProfileView, _ profile: ProfilePreview) {
self.parent = parent
self.profile = profile
_profile = State(initialValue: profile)
}
public var body: some View {
#if os(iOS) || os(macOS)
if #available(iOS 16.0, macOS 13.0,*) {
body0.draggable(profile)
body0.draggable(profile.origin)
} else {
body0
}
@@ -329,23 +299,20 @@ public struct ProfileView: View {
viewBuilder {
#if !os(macOS)
NavigationLink {
EditProfileView {
Task {
await parent.doReload()
}
}.environmentObject(profile)
EditProfileView().environmentObject(profile.origin)
} label: {
Text(profile.name)
}
.contextMenu {
ProfileShareButton(parent.$alert, profile) {
ProfileShareButton(parent.$alert, profile.origin) {
Label("Share", systemImage: "square.and.arrow.up.fill")
}
if profile.type == .remote {
Button {
parent.isUpdating = true
Task {
await parent.updateProfile(profile)
await parent.updateProfile(profile.origin)
profile = ProfilePreview(profile.origin)
}
} label: {
Label("Update", systemImage: "arrow.clockwise")
@@ -353,7 +320,7 @@ public struct ProfileView: View {
}
Button(role: .destructive) {
Task {
await parent.deleteProfile(profile)
await parent.deleteProfile(profile.origin)
}
} label: {
Label("Delete", systemImage: "trash.fill")
@@ -365,7 +332,7 @@ public struct ProfileView: View {
Text(profile.name)
if profile.type == .remote {
Spacer(minLength: 4)
Text("Last Updated: \(profile.lastUpdatedString)").font(.caption)
Text("Last Updated: \(profile.origin.lastUpdatedString)").font(.caption)
}
}
HStack {
@@ -373,23 +340,24 @@ public struct ProfileView: View {
Button {
parent.isUpdating = true
Task {
await parent.updateProfile(profile)
await parent.updateProfile(profile.origin)
profile = ProfilePreview(profile.origin)
}
} label: {
Image(systemName: "arrow.clockwise")
}
}
ProfileShareButton(parent.$alert, profile) {
ProfileShareButton(parent.$alert, profile.origin) {
Image(systemName: "square.and.arrow.up.fill")
}
Button {
parent.openWindow(id: EditProfileWindowView.windowID, value: profile.mustID)
parent.openWindow(id: EditProfileWindowView.windowID, value: profile.id)
} label: {
Image(systemName: "pencil")
}
Button {
Task {
await parent.deleteProfile(profile)
await parent.deleteProfile(profile.origin)
}
} label: {
Image(systemName: "trash.fill")
+26
View File
@@ -69,6 +69,32 @@ public class Profile: Record, Identifiable, ObservableObject {
}
}
public struct ProfilePreview: Identifiable, Hashable {
public let id: Int64
public let name: String
public var order: UInt32
public let type: ProfileType
public let path: String
public let remoteURL: String?
public let autoUpdate: Bool
public let autoUpdateInterval: Int32
public let lastUpdated: Date?
public let origin: Profile
public init(_ profile: Profile) {
id = profile.mustID
name = profile.name
order = profile.order
type = profile.type
path = profile.path
remoteURL = profile.remoteURL
autoUpdate = profile.autoUpdate
autoUpdateInterval = profile.autoUpdateInterval
lastUpdated = profile.lastUpdated
origin = profile
}
}
public enum ProfileType: Int {
case local = 0, icloud, remote
}
+17 -13
View File
@@ -93,33 +93,37 @@ public class CommandClient: ObservableObject {
}
func connected() {
DispatchQueue.main.sync {
DispatchQueue.main.async { [self] in
commandClient.isConnected = true
}
}
func disconnected(_: String?) {
DispatchQueue.main.sync {
DispatchQueue.main.async { [self] in
commandClient.isConnected = false
}
}
func clearLog() {
DispatchQueue.main.async { [self] in
commandClient.logList.removeAll()
}
}
func writeLog(_ message: String?) {
guard let message else {
return
}
var logList = commandClient.logList
if logList.count > commandClient.logMaxLines {
logList.removeFirst()
}
logList.append(message)
DispatchQueue.main.sync {
commandClient.logList = logList
DispatchQueue.main.async { [self] in
if commandClient.logList.count > commandClient.logMaxLines {
commandClient.logList.removeFirst()
}
commandClient.logList.append(message)
}
}
func writeStatus(_ message: LibboxStatusMessage?) {
DispatchQueue.main.sync {
DispatchQueue.main.async { [self] in
commandClient.status = message
}
}
@@ -132,20 +136,20 @@ public class CommandClient: ObservableObject {
while groups.hasNext() {
newGroups.append(groups.next()!)
}
DispatchQueue.main.sync {
DispatchQueue.main.async { [self] in
commandClient.groups = newGroups
}
}
func initializeClashMode(_ modeList: LibboxStringIteratorProtocol?, currentMode: String?) {
DispatchQueue.main.sync {
DispatchQueue.main.async { [self] in
commandClient.clashModeList = modeList!.toArray()
commandClient.clashMode = currentMode!
}
}
func updateClashMode(_ newMode: String?) {
DispatchQueue.main.sync {
DispatchQueue.main.async { [self] in
commandClient.clashMode = newMode!
}
}
@@ -1,9 +1,12 @@
import Foundation
import SwiftUI
public class ExtensionEnvironments: ObservableObject {
@Published public var logClient = CommandClient(.log)
@Published public var extensionProfileLoading = true
@Published public var extensionProfile: ExtensionProfile?
public let profileUpdate = ObjectWillChangePublisher()
public let selectedProfileUpdate = ObjectWillChangePublisher()
public init() {}
@@ -173,7 +173,7 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
}
public func serviceReload() throws {
Task {
runBlocking { [self] in
await tunnel.reloadService()
}
}
@@ -214,4 +214,8 @@ public class ExtensionPlatformInterface: NSObject, LibboxPlatformInterfaceProtoc
try await self.tunnel.setTunnelNetworkSettings(networkSettings)
}
}
func reset() {
networkSettings = nil
}
}
+3
View File
@@ -131,6 +131,9 @@ open class ExtensionProvider: NEPacketTunnelProvider {
boxService = nil
commandServer.setService(nil)
}
if let platformInterface {
platformInterface.reset()
}
}
func reloadService() async {
+5
View File
@@ -39,21 +39,26 @@ public struct MacApplication: Scene {
WindowGroup("New Profile", id: NewProfileView.windowID, for: NewProfileView.ImportRequest.self) { importRequest in
NewProfileView(importRequest.wrappedValue)
.environmentObject(environments)
}.commandsRemoved()
WindowGroup("Edit Profile", id: EditProfileWindowView.windowID, for: Int64.self) { profileID in
EditProfileWindowView(profileID.wrappedValue)
.environmentObject(environments)
}.commandsRemoved()
WindowGroup("Edit Content", id: EditProfileContentView.windowID, for: EditProfileContentView.Context.self) { context in
EditProfileContentView(context.wrappedValue)
.environmentObject(environments)
}.commandsRemoved()
Window("Service Log", id: ServiceLogView.windowID) {
ServiceLogView()
.environmentObject(environments)
}
MenuBarExtra(isInserted: $showMenuBarExtra) {
MenuView(isMenuPresented: $isMenuPresented)
.environmentObject(environments)
} label: {
Image("MenuIcon")
}
+22 -21
View File
@@ -106,6 +106,7 @@ public struct MenuView: View {
}
private struct ProfilePicker: View {
@EnvironmentObject private var environments: ExtensionEnvironments
@ObservedObject private var profile: ExtensionProfile
init(_ profile: ExtensionProfile) {
@@ -113,12 +114,20 @@ public struct MenuView: View {
}
@State private var isLoading = true
@State private var profileList: [Profile] = []
@State private var selectedProfileID: Int64!
@State private var profileList: [ProfilePreview] = []
@State private var selectedProfileID: Int64 = 0
@State private var reasserting = false
@State private var observer: Any?
@State private var alert: Alert?
private var selectedProfileIDLocal: Binding<Int64> {
$selectedProfileID.withSetter { newValue in
reasserting = true
Task { [self] in
await switchProfile(newValue)
}
}
}
var body: some View {
viewBuilder {
if isLoading {
@@ -132,32 +141,24 @@ public struct MenuView: View {
Text("Empty profiles")
} else {
MenuSection("Profile")
Picker("", selection: $selectedProfileID) {
Picker("", selection: selectedProfileIDLocal) {
ForEach(profileList, id: \.id) { profile in
Text(profile.name)
}
}
.pickerStyle(.inline)
.onChangeCompat(of: selectedProfileID) {
reasserting = true
Task {
await switchProfile(selectedProfileID!)
}
}
.disabled(!profile.status.isSwitchable || reasserting)
}
}
}
.onAppear {
if observer == nil {
observer = NotificationCenter.default.addObserver(forName: OverviewView.NotificationUpdateSelectedProfile, object: nil, queue: nil, using: { notification in
selectedProfileID = notification.object as! Int64
})
.onReceive(environments.profileUpdate) { _ in
Task {
await doReload()
}
}
.onDisappear {
if let observer {
NotificationCenter.default.removeObserver(observer)
.onReceive(environments.selectedProfileUpdate) { _ in
Task {
selectedProfileID = await SharedPreferences.selectedProfileID.get()
}
}
.alertBinding($alert)
@@ -168,7 +169,7 @@ public struct MenuView: View {
isLoading = false
}
do {
profileList = try await ProfileManager.list()
profileList = try await ProfileManager.list().map { ProfilePreview($0) }
} catch {
alert = Alert(error)
return
@@ -181,14 +182,14 @@ public struct MenuView: View {
profile.id == selectedProfileID
})
.isEmpty {
selectedProfileID = profileList[0].id!
selectedProfileID = profileList[0].id
await SharedPreferences.selectedProfileID.set(selectedProfileID)
}
}
private func switchProfile(_ newProfileID: Int64) async {
await SharedPreferences.selectedProfileID.set(newProfileID)
NotificationCenter.default.post(name: OverviewView.NotificationUpdateSelectedProfile, object: newProfileID)
environments.selectedProfileUpdate.send()
if profile.status.isConnected {
do {
try await serviceReload()
+18 -14
View File
@@ -92,6 +92,7 @@
3AC729F02A75D9D000FE8EC1 /* Profile+Transferable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC729EF2A75D9D000FE8EC1 /* Profile+Transferable.swift */; };
3AC729F22A76088E00FE8EC1 /* ShareButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC729F12A76088E00FE8EC1 /* ShareButton.swift */; };
3AC8CF9B2A736C750002AF3C /* ImportProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC8CF9A2A736C750002AF3C /* ImportProfileView.swift */; };
3ACE6DE32ACADF55009D9A8A /* Binding+Setter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACE6DE22ACADF55009D9A8A /* Binding+Setter.swift */; };
3AD0953D2A70EB310052764E /* Profile+Share.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD0953C2A70EB310052764E /* Profile+Share.swift */; };
3ADBB4252A7389640041D44F /* ProfileServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ADBB4242A7389640041D44F /* ProfileServer.swift */; };
3ADBB42A2A73A7060041D44F /* NWSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ADBB4292A73A7060041D44F /* NWSocket.swift */; };
@@ -491,6 +492,7 @@
3AC729EF2A75D9D000FE8EC1 /* Profile+Transferable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Profile+Transferable.swift"; sourceTree = "<group>"; };
3AC729F12A76088E00FE8EC1 /* ShareButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareButton.swift; sourceTree = "<group>"; };
3AC8CF9A2A736C750002AF3C /* ImportProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportProfileView.swift; sourceTree = "<group>"; };
3ACE6DE22ACADF55009D9A8A /* Binding+Setter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+Setter.swift"; sourceTree = "<group>"; };
3AD0953C2A70EB310052764E /* Profile+Share.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Profile+Share.swift"; sourceTree = "<group>"; };
3ADBB4242A7389640041D44F /* ProfileServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileServer.swift; sourceTree = "<group>"; };
3ADBB4292A73A7060041D44F /* NWSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NWSocket.swift; sourceTree = "<group>"; };
@@ -988,6 +990,7 @@
3A99B42B2A75288C0010D4B0 /* ViewCompat.swift */,
3A99B42D2A752ABB0010D4B0 /* NavigationDestinationCompat.swift */,
3AC729F12A76088E00FE8EC1 /* ShareButton.swift */,
3ACE6DE22ACADF55009D9A8A /* Binding+Setter.swift */,
);
path = Abstract;
sourceTree = "<group>";
@@ -1463,6 +1466,7 @@
3A99B42E2A752ABB0010D4B0 /* NavigationDestinationCompat.swift in Sources */,
3AC729F22A76088E00FE8EC1 /* ShareButton.swift in Sources */,
3A0C6D3C2A79D46500A4DF2B /* OverviewView.swift in Sources */,
3ACE6DE32ACADF55009D9A8A /* Binding+Setter.swift in Sources */,
3A4EAD262A4FEB65005435B3 /* ExtensionStatusView.swift in Sources */,
3A4EAD252A4FEB65005435B3 /* StartStopButton.swift in Sources */,
3A4EAD2B2A4FEB6D005435B3 /* ViewBuilder.swift in Sources */,
@@ -1962,7 +1966,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.5.1;
MARKETING_VERSION = 1.6.0;
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa;
PRODUCT_NAME = "sing-box";
SDKROOT = appletvos;
@@ -1996,7 +2000,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.5.1;
MARKETING_VERSION = 1.6.0;
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa;
PRODUCT_NAME = "sing-box";
SDKROOT = appletvos;
@@ -2230,7 +2234,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.5.1;
MARKETING_VERSION = 1.6.0;
OTHER_CODE_SIGN_FLAGS = "--deep";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa;
PRODUCT_NAME = "sing-box";
@@ -2270,7 +2274,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.5.1;
MARKETING_VERSION = 1.6.0;
OTHER_CODE_SIGN_FLAGS = "--deep";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa;
PRODUCT_NAME = "sing-box";
@@ -2293,7 +2297,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 85;
CURRENT_PROJECT_VERSION = 88;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = Z56Z6NYZN2;
ENABLE_HARDENED_RUNTIME = YES;
@@ -2309,7 +2313,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.5.1;
MARKETING_VERSION = 1.6.0;
OTHER_CODE_SIGN_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa;
PRODUCT_NAME = "sing-box";
@@ -2331,7 +2335,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 85;
CURRENT_PROJECT_VERSION = 88;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = Z56Z6NYZN2;
ENABLE_HARDENED_RUNTIME = YES;
@@ -2347,7 +2351,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.5.1;
MARKETING_VERSION = 1.6.0;
OTHER_CODE_SIGN_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa;
PRODUCT_NAME = "sing-box";
@@ -2391,7 +2395,7 @@
"@executable_path/../Frameworks",
);
MACH_O_TYPE = mh_dylib;
MACOSX_DEPLOYMENT_TARGET = 12.0;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
@@ -2445,7 +2449,7 @@
"@executable_path/../Frameworks",
);
MACH_O_TYPE = mh_dylib;
MACOSX_DEPLOYMENT_TARGET = 12.0;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0;
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
@@ -2489,7 +2493,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.5.1;
MARKETING_VERSION = "1.6.0-alpha.1";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.system;
PRODUCT_NAME = "$(inherited)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2525,7 +2529,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.5.1;
MARKETING_VERSION = "1.6.0-alpha.1";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.system;
PRODUCT_NAME = "$(inherited)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2565,7 +2569,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.5.1;
MARKETING_VERSION = "1.6.0-alpha.1";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.independent;
PRODUCT_NAME = SFM;
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2604,7 +2608,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.5.1;
MARKETING_VERSION = "1.6.0-alpha.1";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa.independent;
PRODUCT_NAME = SFM;
PROVISIONING_PROFILE_SPECIFIER = "";