From c5a29e09568955cda03a044f6777585c4ae043f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 2 Oct 2023 22:12:33 +0800 Subject: [PATCH] UI fixes --- .../Views/Abstract/Binding+Setter.swift | 12 ++ .../Views/Dashboard/ActiveDashboardView.swift | 36 +++--- .../Views/Dashboard/DashboardPage.swift | 2 +- .../Views/Dashboard/OverviewView.swift | 63 ++++------ ApplicationLibrary/Views/Log/LogView.swift | 108 ++++++++++-------- .../Views/Profile/EditProfileView.swift | 20 +--- .../Views/Profile/NewProfileView.swift | 10 +- .../Views/Profile/ProfileView.swift | 104 ++++++----------- Library/Database/Profile.swift | 26 +++++ Library/Network/CommandClient.swift | 30 ++--- Library/Network/ExtensionEnvironments.swift | 3 + .../Network/ExtensionPlatformInterface.swift | 6 +- Library/Network/ExtensionProvider.swift | 3 + MacLibrary/MacApplication.swift | 5 + MacLibrary/MenuView.swift | 43 +++---- sing-box.xcodeproj/project.pbxproj | 32 +++--- 16 files changed, 256 insertions(+), 247 deletions(-) create mode 100644 ApplicationLibrary/Views/Abstract/Binding+Setter.swift diff --git a/ApplicationLibrary/Views/Abstract/Binding+Setter.swift b/ApplicationLibrary/Views/Abstract/Binding+Setter.swift new file mode 100644 index 0000000..395f4fe --- /dev/null +++ b/ApplicationLibrary/Views/Abstract/Binding+Setter.swift @@ -0,0 +1,12 @@ +import Foundation +import SwiftUI + +public extension Binding { + func withSetter(_ setter: @escaping (Value) -> Void) -> Binding { + Binding { + wrappedValue + } set: { [setter] newValue, _ in + setter(newValue) + } + } +} diff --git a/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift b/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift index 6647465..2eade72 100644 --- a/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift +++ b/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift @@ -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) } diff --git a/ApplicationLibrary/Views/Dashboard/DashboardPage.swift b/ApplicationLibrary/Views/Dashboard/DashboardPage.swift index c3dfe5c..d0a2245 100644 --- a/ApplicationLibrary/Views/Dashboard/DashboardPage.swift +++ b/ApplicationLibrary/Views/Dashboard/DashboardPage.swift @@ -31,7 +31,7 @@ public extension DashboardPage { } @MainActor - func contentView(_ profileList: Binding<[Profile]>, _ selectedProfileID: Binding, _ systemProxyAvailable: Binding, _ systemProxyEnabled: Binding) -> some View { + func contentView(_ profileList: Binding<[ProfilePreview]>, _ selectedProfileID: Binding, _ systemProxyAvailable: Binding, _ systemProxyEnabled: Binding) -> some View { viewBuilder { switch self { case .overview: diff --git a/ApplicationLibrary/Views/Dashboard/OverviewView.swift b/ApplicationLibrary/Views/Dashboard/OverviewView.swift index ab58346..7258ebf 100644 --- a/ApplicationLibrary/Views/Dashboard/OverviewView.swift +++ b/ApplicationLibrary/Views/Dashboard/OverviewView.swift @@ -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, _ systemProxyAvailable: Binding, _ systemProxyEnabled: Binding) { + private var selectedProfileIDLocal: Binding { + $selectedProfileID.withSetter { newValue in + reasserting = true + Task { [self] in + await switchProfile(newValue) + } + } + } + + public init(_ profileList: Binding<[ProfilePreview]>, _ selectedProfileID: Binding, _ systemProxyAvailable: Binding, _ systemProxyEnabled: Binding) { _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 { diff --git a/ApplicationLibrary/Views/Log/LogView.swift b/ApplicationLibrary/Views/Log/LogView.swift index afe0601..5b6963f 100644 --- a/ApplicationLibrary/Views/Log/LogView.swift +++ b/ApplicationLibrary/Views/Log/LogView.swift @@ -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) + } } } } diff --git a/ApplicationLibrary/Views/Profile/EditProfileView.swift b/ApplicationLibrary/Views/Profile/EditProfileView.swift index c2a8741..0f977be 100644 --- a/ApplicationLibrary/Views/Profile/EditProfileView.swift +++ b/ApplicationLibrary/Views/Profile/EditProfileView.swift @@ -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() } } diff --git a/ApplicationLibrary/Views/Profile/NewProfileView.swift b/ApplicationLibrary/Views/Profile/NewProfileView.swift index 95b315f..f27051f 100644 --- a/ApplicationLibrary/Views/Profile/NewProfileView.swift +++ b/ApplicationLibrary/Views/Profile/NewProfileView.swift @@ -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 } diff --git a/ApplicationLibrary/Views/Profile/ProfileView.swift b/ApplicationLibrary/Views/Profile/ProfileView.swift index b914b14..283bf12 100644 --- a/ApplicationLibrary/Views/Profile/ProfileView.swift +++ b/ApplicationLibrary/Views/Profile/ProfileView.swift @@ -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") diff --git a/Library/Database/Profile.swift b/Library/Database/Profile.swift index af81d10..0b49bf9 100644 --- a/Library/Database/Profile.swift +++ b/Library/Database/Profile.swift @@ -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 } diff --git a/Library/Network/CommandClient.swift b/Library/Network/CommandClient.swift index 4961013..031a5f4 100644 --- a/Library/Network/CommandClient.swift +++ b/Library/Network/CommandClient.swift @@ -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! } } diff --git a/Library/Network/ExtensionEnvironments.swift b/Library/Network/ExtensionEnvironments.swift index 0bdd86c..cabbd90 100644 --- a/Library/Network/ExtensionEnvironments.swift +++ b/Library/Network/ExtensionEnvironments.swift @@ -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() {} diff --git a/Library/Network/ExtensionPlatformInterface.swift b/Library/Network/ExtensionPlatformInterface.swift index 1affffb..003e5ef 100644 --- a/Library/Network/ExtensionPlatformInterface.swift +++ b/Library/Network/ExtensionPlatformInterface.swift @@ -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 + } } diff --git a/Library/Network/ExtensionProvider.swift b/Library/Network/ExtensionProvider.swift index ed170b1..1d14f37 100644 --- a/Library/Network/ExtensionProvider.swift +++ b/Library/Network/ExtensionProvider.swift @@ -131,6 +131,9 @@ open class ExtensionProvider: NEPacketTunnelProvider { boxService = nil commandServer.setService(nil) } + if let platformInterface { + platformInterface.reset() + } } func reloadService() async { diff --git a/MacLibrary/MacApplication.swift b/MacLibrary/MacApplication.swift index 81e3271..0ec36fa 100644 --- a/MacLibrary/MacApplication.swift +++ b/MacLibrary/MacApplication.swift @@ -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") } diff --git a/MacLibrary/MenuView.swift b/MacLibrary/MenuView.swift index 8dd042c..ee35ff5 100644 --- a/MacLibrary/MenuView.swift +++ b/MacLibrary/MenuView.swift @@ -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 { + $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() diff --git a/sing-box.xcodeproj/project.pbxproj b/sing-box.xcodeproj/project.pbxproj index 024f8a1..3a97751 100644 --- a/sing-box.xcodeproj/project.pbxproj +++ b/sing-box.xcodeproj/project.pbxproj @@ -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 = ""; }; 3AC729F12A76088E00FE8EC1 /* ShareButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareButton.swift; sourceTree = ""; }; 3AC8CF9A2A736C750002AF3C /* ImportProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportProfileView.swift; sourceTree = ""; }; + 3ACE6DE22ACADF55009D9A8A /* Binding+Setter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+Setter.swift"; sourceTree = ""; }; 3AD0953C2A70EB310052764E /* Profile+Share.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Profile+Share.swift"; sourceTree = ""; }; 3ADBB4242A7389640041D44F /* ProfileServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileServer.swift; sourceTree = ""; }; 3ADBB4292A73A7060041D44F /* NWSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NWSocket.swift; sourceTree = ""; }; @@ -988,6 +990,7 @@ 3A99B42B2A75288C0010D4B0 /* ViewCompat.swift */, 3A99B42D2A752ABB0010D4B0 /* NavigationDestinationCompat.swift */, 3AC729F12A76088E00FE8EC1 /* ShareButton.swift */, + 3ACE6DE22ACADF55009D9A8A /* Binding+Setter.swift */, ); path = Abstract; sourceTree = ""; @@ -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 = "";