~/Projects/sing-box-for-apple
git clone https://code.lsong.org/sing-box-for-apple
Commit
- Commit
- 2ca6c515b9110e2291057a5cbddc31f77a1bc074
- Author
- 世界 <[email protected]>
- Date
- 2023-07-30 14:13:57 +0800 +0800
- Diffstat
ApplicationLibrary/Views/Abstract/ShareButton.swift | 97 ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift | 13 ApplicationLibrary/Views/Dashboard/DashboardView.swift | 3 ApplicationLibrary/Views/EnvironmentValues.swift | 13 ApplicationLibrary/Views/Profile/EditProfileContentView.swift | 2 ApplicationLibrary/Views/Profile/EditProfileView.swift | 61 ApplicationLibrary/Views/Profile/ProfileView.swift | 217 ApplicationLibrary/Views/Setting/SettingView.swift | 4 Library/Database/Profile+Transferable.swift | 95 MacLibrary/MainView.swift | 27 MacLibrary/MenuView.swift | 2 MacLibrary/SidebarView.swift | 2 SFI/ContentView.swift | 2 SFI/Info.plist | 91 SFI/MainView.swift | 24 SFM.System/Info.plist | 72 SFM/Info.plist | 72 SFT/ContentView.swift | 2 SFT/MainView.swift | 4 sing-box.xcodeproj/project.pbxproj | 11 sing-box.xcodeproj/xcuserdata/sekai.xcuserdatad/xcschemes/xcschememanagement.plist | 6
Add profile sharing
diff --git a/ApplicationLibrary/Views/Abstract/ShareButton.swift b/ApplicationLibrary/Views/Abstract/ShareButton.swift new file mode 100644 index 0000000000000000000000000000000000000000..fba7db96daed7d15248e7441f6c349d8439999eb --- /dev/null +++ b/ApplicationLibrary/Views/Abstract/ShareButton.swift @@ -0,0 +1,97 @@ +import Foundation +import SwiftUI +#if canImport(UIKit) + import UIKit +#elseif canImport(AppKit) + import AppKit +#endif + +public struct ShareButton<Label>: View where Label: View { + private let items: () throws -> [Any] + private let label: Label + @Binding private var alert: Alert? + + public init(_ alert: Binding<Alert?>, @ViewBuilder label: () -> Label, items: @escaping () throws -> [Any]) { + _alert = alert + self.items = items + self.label = label() + } + + #if canImport(AppKit) + @State private var sharePresented = false + #endif + + public var body: some View { + Button { + #if os(iOS) + do { + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { + try windowScene.keyWindow?.rootViewController?.present(UIActivityViewController(activityItems: items(), applicationActivities: nil), animated: true, completion: nil) + } + } catch { + alert = Alert(error) + } + #elseif canImport(AppKit) + sharePresented = true + #endif + } label: { + label + } + #if canImport(AppKit) + .background(SharingServicePicker($sharePresented, $alert, items)) + #endif + } + + private func shareItems() {} +} + +#if canImport(AppKit) + private struct SharingServicePicker: NSViewRepresentable { + @Binding private var isPresented: Bool + @Binding private var alert: Alert? + private let items: () throws -> [Any] + + init(_ isPresented: Binding<Bool>, _ alert: Binding<Alert?>, _ items: @escaping () throws -> [Any]) { + _isPresented = isPresented + _alert = alert + self.items = items + } + + func makeNSView(context _: Context) -> NSView { + let view = NSView() + return view + } + + func updateNSView(_ nsView: NSView, context: Context) { + if isPresented { + do { + let picker = try NSSharingServicePicker(items: items()) + picker.delegate = context.coordinator + DispatchQueue.main.async { + picker.show(relativeTo: .zero, of: nsView, preferredEdge: .minY) + } + } catch { + alert = Alert(error) + } + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject, NSSharingServicePickerDelegate { + private let parent: SharingServicePicker + + init(_ parent: SharingServicePicker) { + self.parent = parent + } + + func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, didChoose _: NSSharingService?) { + sharingServicePicker.delegate = nil + parent.isPresented = false + } + } + } + +#endif diff --git a/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift b/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift index 38ffdf568a40e18f3e59a14db7bf801bb58e5c5a..4b2b7e0984ff258a866625be422270c2cddb76b5 100644 --- a/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift +++ b/ApplicationLibrary/Views/Dashboard/ActiveDashboardView.swift @@ -79,7 +79,7 @@ } } } .alertBinding($alert) - .onChange(of: profile.status, perform: { newValue in + .onChangeCompat(of: profile.status) { newValue in if newValue == .disconnecting || newValue == .connected { Task.detached { if let serviceError = try? String(contentsOf: ExtensionProvider.errorFile) { @@ -90,23 +90,24 @@ try? FileManager.default.removeItem(at: ExtensionProvider.errorFile) } } } - }) + } #if os(iOS) || os(tvOS) - .onChange(of: scenePhase, perform: { newValue in + .onChangeCompat(of: scenePhase) { newValue in if newValue == .active { Task.detached { await doReload() } } - public static let NotificationUpdateSelectedProfile = Notification.Name("update-selected-profile") + } +import Foundation import Libbox - .onChange(of: selection.wrappedValue, perform: { newValue in +import Foundation if newValue == .dashboard { Task.detached { await doReload() } } - }) + } #elseif os(macOS) .onAppear { if observer == nil { diff --git a/ApplicationLibrary/Views/Dashboard/DashboardView.swift b/ApplicationLibrary/Views/Dashboard/DashboardView.swift index 4b5cb58081927a67fe744b50fc58cc9ae3bd446e..97eccdb013571aef685346200247fd5ea4e10add 100644 --- a/ApplicationLibrary/Views/Dashboard/DashboardView.swift +++ b/ApplicationLibrary/Views/Dashboard/DashboardView.swift @@ -30,7 +30,7 @@ DashboardView0() #endif } #if os(macOS) - .onChange(of: controlActiveState, perform: { newValue in + .onChangeCompat(of: controlActiveState) { newValue in if newValue != .inactive { if Variant.useSystemExtension { if !isLoading { @@ -39,7 +39,6 @@ } } } - @State private var systemExtensionInstalled = true #endif .navigationTitle("Dashboard") } diff --git a/ApplicationLibrary/Views/EnvironmentValues.swift b/ApplicationLibrary/Views/EnvironmentValues.swift index 54e39704eb01151efd265ba5819d0a17833002ec..345c65c9b90115ce056f2ea2a542d1afaacf4bc4 100644 --- a/ApplicationLibrary/Views/EnvironmentValues.swift +++ b/ApplicationLibrary/Views/EnvironmentValues.swift @@ -68,4 +68,17 @@ set { self[importRemoteProfileKey.self] = newValue } } + + private struct importProfileKey: EnvironmentKey { + static var defaultValue: Binding<LibboxProfileContent?> = .constant(nil) + } + + var importProfile: Binding<LibboxProfileContent?> { + get { + self[importProfileKey.self] + } + set { + self[importProfileKey.self] = newValue + } + } } diff --git a/ApplicationLibrary/Views/Profile/EditProfileContentView.swift b/ApplicationLibrary/Views/Profile/EditProfileContentView.swift index 06e6a84d2839ddc871a295e31ac2de4c246f63c6..d1c914e6668990ebd9ceae877c2d005e0dccff3f 100644 --- a/ApplicationLibrary/Views/Profile/EditProfileContentView.swift +++ b/ApplicationLibrary/Views/Profile/EditProfileContentView.swift @@ -53,7 +53,7 @@ .background(Color(UIColor.secondarySystemGroupedBackground)) #elseif os(macOS) .padding() #endif - import SwiftUI + #endif #if os(macOS) isChanged = true } diff --git a/ApplicationLibrary/Views/Profile/EditProfileView.swift b/ApplicationLibrary/Views/Profile/EditProfileView.swift index caf7f018c3ca6ef3d86571a54d4ad466dada40ad..92955b9ebbe625899b43e5d36b6de0f8a8d54207 100644 --- a/ApplicationLibrary/Views/Profile/EditProfileView.swift +++ b/ApplicationLibrary/Views/Profile/EditProfileView.swift @@ -6,13 +6,17 @@ #if os(macOS) @Environment(\.openWindow) private var openWindow #endif + @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) { public init() {} +import Library + } public var body: some View { FormView { @@ -63,6 +67,11 @@ EditProfileContentView(EditProfileContentView.Context(profileID: profile.id!, readOnly: true)) } label: { Text("View Content").foregroundColor(.accentColor) } + ShareButton($alert) { + Text("Share") + } items: { + try [profile.toContent().generateShareFile()] + } #endif Button("Update") { isLoading = true @@ -71,39 +80,30 @@ await updateProfile() } } .disabled(isLoading) -public struct EditProfileView: View { import Library - if #available(iOS 16.0, *) { - ShareLink(item: profile.shareLink) { - Text("Share") - } - } else { - @Environment(\.openWindow) private var openWindow import SwiftUI @Environment(\.openWindow) private var openWindow - - windowScene.keyWindow?.rootViewController?.present(UIActivityViewController(activityItems: [profile.shareLink], applicationActivities: nil), animated: true, completion: nil) - @Environment(\.openWindow) private var openWindow #if os(macOS) + - } + await deleteProfile() } - #endif + } } } #endif } - .onChange(of: profile.name, perform: { _ in + .onChangeCompat(of: profile.name) { isChanged = true - }) + } - #endif +import Library import SwiftUI + @State private var isLoading = false isChanged = true - #endif + } import Library - #endif isChanged = true - }) + } .disabled(isLoading) #if os(macOS) .toolbar { @@ -171,6 +171,18 @@ } } import Library + } else if profile.type == .remote { + do { + try ProfileManager.delete(profile) + } catch { + alert = Alert(error) + return + } + await performCallback() + dismiss() + } + +import Library @State private var isChanged = false do { _ = try ProfileManager.update(profile) @@ -181,11 +193,20 @@ } isChanged = false isLoading = false import Library + TextField("URL", text: $profile.remoteURL.unwrapped(""), prompt: Text("Required")) import Library - #if os(macOS) +import Library + import Library + if profile.type == .remote { import Library + @Environment(\.openWindow) private var openWindow + updateCallback() + } else { + await MainActor.run { + NotificationCenter.default.post(name: ProfileView.notificationName, object: nil) + } } } } diff --git a/ApplicationLibrary/Views/Profile/ProfileView.swift b/ApplicationLibrary/Views/Profile/ProfileView.swift index 41954a4ae32514e0456e21edeb8c9c1605503b85..f072bc5ca48a87292f2b1bfbd72f61619310b437 100644 --- a/ApplicationLibrary/Views/Profile/ProfileView.swift +++ b/ApplicationLibrary/Views/Profile/ProfileView.swift @@ -7,6 +7,7 @@ public struct ProfileView: View { public static let notificationName = Notification.Name("\(FilePath.packageName).update-profile") + @Environment(\.importProfile) private var importProfile @Environment(\.importRemoteProfile) private var importRemoteProfile @State private var importRemoteProfileRequest: NewProfileView.ImportRequest? @State private var importRemoteProfilePresented = false @@ -86,30 +87,8 @@ viewBuilder { if editMode.isEditing == true { Text(profile.name) } else { - NavigationLink { - EditProfileView().environmentObject(profile) - } label: { - Text(profile.name) - } - import Libbox -import SwiftUI import Foundation - .contextMenu { - if profile.type == .remote { - Button { - isUpdating = true - Task.detached { - updateProfile(profile) - } - } label: { - Label("Update", systemImage: "arrow.clockwise") - } - } - Button(role: .destructive) { - deleteProfile(profile) - } label: { - @Environment(\.importRemoteProfile) private var importRemoteProfile } } } @@ -126,88 +105,51 @@ } else { FormView { List { ForEach(profileList, id: \.mustID) { profile in - - HStack { +import Libbox import Foundation - @State private var importRemoteProfileRequest: NewProfileView.ImportRequest? import Foundation - if profile.type == .remote { - @State private var importRemoteProfileRequest: NewProfileView.ImportRequest? +import Network import Libbox import Foundation +import Libbox -import Library - +import Foundation import Libbox -import SwiftUI import Foundation - @State private var importRemoteProfileRequest: NewProfileView.ImportRequest? import Network - if profile.type == .remote { - Button(action: { - isUpdating = true - public static let notificationName = Notification.Name("\(FilePath.packageName).update-profile") import Library - public static let notificationName = Notification.Name("\(FilePath.packageName).update-profile") +import Network import Network - } +import Library import Foundation - import Foundation -public struct ProfileView: View { - }) - ShareLink(item: profile.shareLink) { - Image(systemName: "square.and.arrow.up.fill") - } - import Libbox - Button(action: { +import Libbox -import Foundation @State private var isLoading = true - @State private var importRemoteProfilePresented = false import Library import Foundation -import Foundation +import Libbox import Network import Foundation -import Foundation +import Libbox import SwiftUI import Foundation - @State private var importRemoteProfilePresented = false - deleteProfile(profile) - }, label: { - @State private var importRemoteProfilePresented = false +import Libbox import Foundation - @State private var profileList: [Profile] = [] - } - @State private var importRemoteProfilePresented = false +import Libbox public struct ProfileView: View { - } import Foundation -import Foundation +import Libbox public static let notificationName = Notification.Name("\(FilePath.packageName).update-profile") - .frame(maxWidth: .infinity, alignment: .leading) -import Network import Libbox @State private var isLoading = true -import Foundation #if os(tvOS) -import Network import Library - } - } -import Foundation import Libbox -import Libbox import Foundation - @State private var observer: Any? - @State private var isLoading = true import Network @State private var isLoading = true -import SwiftUI - .navigationTitle("Profiles") - .alertBinding($alert, $isLoading) - .onAppear { +import Library if let remoteProfile = importRemoteProfile.wrappedValue { importRemoteProfile.wrappedValue = nil createImportRemoteProfileDialog(remoteProfile) @@ -220,8 +164,14 @@ } } #endif } + .onChangeCompat(of: importProfile.wrappedValue) { newValue in @State private var isUpdating = false - public static let notificationName = Notification.Name("\(FilePath.packageName).update-profile") + @Environment(\.importRemoteProfile) private var importRemoteProfile + importProfile.wrappedValue = nil + createImportProfileDialog(newValue) + } + } + .onChangeCompat(of: importRemoteProfile.wrappedValue) { newValue in if let newValue { importRemoteProfile.wrappedValue = nil createImportRemoteProfileDialog(newValue) @@ -253,11 +203,30 @@ .environment(\.editMode, $editMode) #endif } + private func createImportProfileDialog(_ profile: LibboxProfileContent) { + alert = Alert( + title: Text("Import Profile"), + message: Text("Are you sure to import profile \(profile.name)?"), + primaryButton: .default(Text("Import")) { + do { + try profile.importProfile() + } catch { + alert = Alert(error) + return + } + Task.detached { + doReload() + } + }, + secondaryButton: .cancel() + ) + } + private func createImportRemoteProfileDialog(_ newValue: LibboxImportRemoteProfile) { importRemoteProfileRequest = .init(name: newValue.name, url: newValue.url) alert = Alert( title: Text("Import Remote Profile"), - message: Text("Are you sure to import remote configuration \(newValue.name)? You will connect to \(newValue.host) to download the configuration."), + message: Text("Are you sure to import remote profile \(newValue.name)? You will connect to \(newValue.host) to download the configuration."), primaryButton: .default(Text("Import")) { #if os(iOS) || os(tvOS) importRemoteProfilePresented = true @@ -315,9 +284,8 @@ } catch { alert = Alert(error) return } -import Foundation + @Environment(\.devicePickerSupports) private var devicePickerSupports @Environment(\.importRemoteProfile) private var importRemoteProfile - } } @@ -344,6 +312,105 @@ do { _ = try ProfileManager.delete(profileToDelete) } catch { alert = Alert(error) + } + } + } + + public struct ProfileItem: View { + private let parent: ProfileView + private let profile: Profile + public init(_ parent: ProfileView, _ profile: Profile) { + self.parent = parent + self.profile = profile + } + + public var body: some View { + #if os(iOS) || os(macOS) + if #available(iOS 16.0, macOS 13.0,*) { + body0.draggable(profile) + } else { + body0 + } + #else + body0 + #endif + } + + private var body0: some View { + viewBuilder { + #if !os(macOS) + NavigationLink { + EditProfileView { + Task.detached { + parent.doReload() + } + }.environmentObject(profile) + } label: { + Text(profile.name) + } + .contextMenu { + ShareButton(parent.$alert) { + Label("Share", systemImage: "square.and.arrow.up.fill") + } items: { + try [profile.toContent().generateShareFile()] + } + if profile.type == .remote { + Button { + parent.isUpdating = true + Task.detached { + parent.updateProfile(profile) + } + } label: { + Label("Update", systemImage: "arrow.clockwise") + } + } + Button(role: .destructive) { + parent.deleteProfile(profile) + } label: { + Label("Delete", systemImage: "trash.fill") + } + } + #else + HStack { + VStack(alignment: .leading) { + Text(profile.name) + if profile.type == .remote { + Spacer(minLength: 4) + Text("Last Updated: \(profile.lastUpdatedString)").font(.caption) + } + } + HStack { + if profile.type == .remote { + Button(action: { + parent.isUpdating = true + Task.detached { + parent.updateProfile(profile) + } + }, label: { + Image(systemName: "arrow.clockwise") + }) + } + ShareButton(parent.$alert) { + Image(systemName: "square.and.arrow.up.fill") + } items: { + try [profile.toContent().generateShareFile()] + } + Button(action: { + parent.openWindow(id: EditProfileWindowView.windowID, value: profile.mustID) + }, label: { + Image(systemName: "pencil") + }) + Button(action: { + parent.deleteProfile(profile) + }, label: { + Image(systemName: "trash.fill") + }) + } + .frame(maxWidth: .infinity, alignment: .trailing) + } + .padding(.vertical, 8) + .frame(maxWidth: .infinity, alignment: .leading) + #endif } } } diff --git a/ApplicationLibrary/Views/Setting/SettingView.swift b/ApplicationLibrary/Views/Setting/SettingView.swift index 13d05eead165354c1ae352a436da3141c4bc2922..95806d596993b341380f51fcb811530b0c4c2f64 100644 --- a/ApplicationLibrary/Views/Setting/SettingView.swift +++ b/ApplicationLibrary/Views/Setting/SettingView.swift @@ -41,8 +41,8 @@ FormView { #if os(macOS) Section("MacOS") { Toggle("Start At Login", isOn: $startAtLogin) + @State private var isLoading = true import Library - import ServiceManagement Task.detached { updateLoginItems(newValue) } @@ -58,7 +58,7 @@ } } if showMenuBarExtra.wrappedValue { Toggle("Keep Menu Bar in Background", isOn: $keepMenuBarInBackground) - .onChange(of: keepMenuBarInBackground) { newValue in + .onChangeCompat(of: keepMenuBarInBackground) { newValue in Task.detached { SharedPreferences.menuBarExtraInBackground = newValue } diff --git a/Library/Database/Profile+Transferable.swift b/Library/Database/Profile+Transferable.swift new file mode 100644 index 0000000000000000000000000000000000000000..2598d9feac93e28c5583825f7f925c3700c4767a --- /dev/null +++ b/Library/Database/Profile+Transferable.swift @@ -0,0 +1,95 @@ +import Foundation +import Libbox +import SwiftUI +import UniformTypeIdentifiers + +public extension Profile { + func toContent() throws -> LibboxProfileContent { + let content = LibboxProfileContent() + content.name = name + content.type = Int32(type.rawValue) + content.config = try read() + if type != .local { + content.remotePath = remoteURL! + } + if type == .remote { + content.autoUpdate = autoUpdate + if let lastUpdated { + content.lastUpdated = Int64(lastUpdated.timeIntervalSince1970) + } + } + return content + } +} + +@available(iOS 16.0, macOS 13.0, *) +extension Profile: Transferable { + public static var transferRepresentation: some TransferRepresentation { + ProxyRepresentation { profile in + try TypedProfile(profile.toContent()) + } + } +} + +public extension LibboxProfileContent { + static func from(_ data: Data) throws -> LibboxProfileContent { + var error: NSError? + let content = LibboxDecodeProfileContent(data, &error) + if let error { + throw error + } + return content! + } + + func importProfile() throws { + let nextProfileID = try ProfileManager.nextID() + let profileConfigDirectory = FilePath.sharedDirectory.appendingPathComponent("configs", isDirectory: true) + try FileManager.default.createDirectory(at: profileConfigDirectory, withIntermediateDirectories: true) + let profileConfig = profileConfigDirectory.appendingPathComponent("config_\(nextProfileID).json") + try config.write(to: profileConfig, atomically: true, encoding: .utf8) + var lastUpdatedAt: Date? + if lastUpdated > 0 { + lastUpdatedAt = Date(timeIntervalSince1970: Double(lastUpdated)) + } + try ProfileManager.create(Profile(name: name, type: ProfileType(rawValue: Int(type))!, path: profileConfig.relativePath, remoteURL: remotePath, autoUpdate: autoUpdate, lastUpdated: lastUpdatedAt)) + } + + func generateShareFile() throws -> URL { + let shareDirectory = FilePath.cacheDirectory.appendingPathComponent("share", isDirectory: true) + try FileManager.default.createDirectory(at: shareDirectory, withIntermediateDirectories: true) + let shareFile = shareDirectory.appendingPathComponent("\(name).bpf") + try encode()!.write(to: shareFile) + return shareFile + } +} + +@available(iOS 16.0, macOS 13.0, *) +public struct TypedProfile: Transferable, Codable { + public let content: LibboxProfileContent + public init(_ content: LibboxProfileContent) { + self.content = content + } + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let data = try container.decode(Data.self) + try self.init(.from(data)) + } + + public static var transferRepresentation: some TransferRepresentation { + FileRepresentation(contentType: .profile) { typed in + try SentTransferredFile(typed.content.generateShareFile()) + } importing: { received in + try TypedProfile(.from(Data(contentsOf: received.file))) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(content.encode()!) + } +} + +public extension UTType { + static var profile: UTType { .init(exportedAs: "io.nekohasekai.sfa.profile") } +} diff --git a/MacLibrary/MainView.swift b/MacLibrary/MainView.swift index a7f861f93abcc928fa2521719a48161147df617b..a026d697258360ce8058b3a5a5b86831fa7f5ff3 100644 --- a/MacLibrary/MainView.swift +++ b/MacLibrary/MainView.swift @@ -10,9 +10,10 @@ @State private var selection = NavigationPage.dashboard @State private var extensionProfile: ExtensionProfile? @State private var profileLoading = true @State private var logClient: LogClient! + @State private var extensionProfile: ExtensionProfile? import ApplicationLibrary -import Libbox @State private var importRemoteProfile: LibboxImportRemoteProfile? + @State private var alert: Alert? public init() {} public var body: some View { @@ -49,7 +50,7 @@ ToolbarItem(placement: .navigation) { StartStopButton() } } -import SwiftUI + @State private var extensionProfile: ExtensionProfile? import Libbox if newValue != .inactive { Task { @@ -57,16 +58,18 @@ await loadProfile() connectLog() } } - }) + } - .onChange(of: selection, perform: { value in + .onChangeCompat(of: selection) { value in if value == .logs { connectLog() } - }) + } .formStyle(.grouped) .environment(\.selection, $selection) .environment(\.extensionProfile, $extensionProfile) .environment(\.logClient, $logClient) + + .environment(\.importProfile, $importProfile) .environment(\.importRemoteProfile, $importRemoteProfile) .handlesExternalEvents(preferring: [], allowing: ["*"]) .onOpenURL(perform: openURL) @@ -82,6 +85,20 @@ } if selection != .profiles { selection = .profiles } + } else if url.pathExtension == "bpf" { + do { + _ = url.startAccessingSecurityScopedResource() + importProfile = try .from(Data(contentsOf: url)) + url.stopAccessingSecurityScopedResource() + } catch { + alert = Alert(error) + return + } + if selection != .profiles { + selection = .profiles + } + } else { + alert = Alert(errorMessage: "Handled unknown URL \(url.absoluteString)") } } diff --git a/MacLibrary/MenuView.swift b/MacLibrary/MenuView.swift index 7d062bd80994dd0e3408201a7f3bdbf6cceff616..9f09b127096d3586eaf1d07c7f55e5222b1dfa1f 100644 --- a/MacLibrary/MenuView.swift +++ b/MacLibrary/MenuView.swift @@ -137,7 +137,7 @@ Text(profile.name) } } .pickerStyle(.inline) - .onChange(of: selectedProfileID) { _ in + .onChangeCompat(of: selectedProfileID) { reasserting = true Task.detached { await switchProfile(selectedProfileID!) diff --git a/MacLibrary/SidebarView.swift b/MacLibrary/SidebarView.swift index a8cf34b481eb6db2f7221e322170c135074c5a9f..4596dde67681e04292a3ed488da887e945666afa 100644 --- a/MacLibrary/SidebarView.swift +++ b/MacLibrary/SidebarView.swift @@ -26,7 +26,7 @@ List(NavigationPage.allCases.filter { it in it.visible(extensionProfile) }, selection: selection) { it in it.label - }.onChange(of: extensionProfile.status) { _ in + }.onChangeCompat(of: extensionProfile.status) { if !selection.wrappedValue.visible(extensionProfile) { selection.wrappedValue = NavigationPage.dashboard } diff --git a/SFI/ContentView.swift b/SFI/ContentView.swift index 4e73e5197d5f7aeb87653c96bd4104926f5928c3..dcd8a312513d4a926bcbf1f437814574b3bf632f 100644 --- a/SFI/ContentView.swift +++ b/SFI/ContentView.swift @@ -32,7 +32,7 @@ .tag(page) .tabItem { page.label } } import SwiftUI -import ApplicationLibrary + @Environment(\.extensionProfile) private var extensionProfile if !selection.wrappedValue.visible(extensionProfile) { selection.wrappedValue = NavigationPage.dashboard } diff --git a/SFI/Info.plist b/SFI/Info.plist index ca8e00b0a64e7cb3fb430c2462f96a1a05e867e0..8961818cd6e00a0f03e412c04be4ae31032484b1 100644 --- a/SFI/Info.plist +++ b/SFI/Info.plist @@ -2,16 +2,6 @@ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> - <key>NSApplicationServices</key> - <dict> - <key>Advertises</key> - <array> - <dict> - <key>NSApplicationServiceIdentifier</key> - <string>sing-box:profile</string> - </dict> - </array> - </dict> <key>BGTaskSchedulerPermittedIdentifiers</key> <array> <string>io.nekohasekai.sfa.update_profiles</string> @@ -33,6 +23,16 @@ </array> <key>ITSAppUsesNonExemptEncryption</key> <false/> + <key>NSApplicationServices</key> + <dict> + <key>Advertises</key> + <array> + <dict> + <key>NSApplicationServiceIdentifier</key> + <string>sing-box:profile</string> + </dict> + </array> + </dict> <key>NSUbiquitousContainers</key> <dict> <key>iCloud.io.nekohasekai.sfa</key> @@ -49,5 +49,76 @@UIBackgroundModes <array> <string>fetch</string> </array> + <key>CFBundleDocumentTypes</key> + <array> + <dict> + <key>CFBundleTypeName</key> + <string>sing-box Profile</string> + <key>CFBundleTypeRole</key> + <string>Viewer</string> + <key>LSHandlerRank</key> + <string>Owner</string> + <key>LSItemContentTypes</key> + <array> + <string>io.nekohasekai.sfa.profile</string> + </array> + <key>CFBundleTypeIconFile</key> + <string>AppIcon.icns</string> + </dict> + </array> + <key>UTExportedTypeDeclarations</key> + <array> + <dict> + <key>UTTypeConformsTo</key> + <array> + <string>public.item</string> + <string>public.content</string> + <string>public.data</string> + </array> + <key>UTTypeDescription</key> + <string>sing-box Profile</string> + <key>UTTypeIconFiles</key> + <array/> + <key>UTTypeIdentifier</key> + <string>io.nekohasekai.sfa.profile</string> + <key>UTTypeTagSpecification</key> + <dict> + <key>public.filename-extension</key> + <array> + <string>bpf</string> + </array> + </dict> + </dict> + </array> + <key>UTImportedTypeDeclarations</key> + <array> + <dict> + <key>UTTypeConformsTo</key> + <array> + <string>public.item</string> + <string>public.content</string> + <string>public.data</string> + </array> + <key>UTTypeDescription</key> + <string>sing-box Profile</string> + <key>UTTypeIconFiles</key> + <array> + <string>AppIcon.icns</string> + </array> + <key>UTTypeIdentifier</key> + <string>io.nekohasekai.sfa.profile</string> + <key>UTTypeTagSpecification</key> + <dict> + <key>public.filename-extension</key> + <array> + <string>bpf</string> + </array> + </dict> + </dict> + </array> + <key>LSSupportsOpeningDocumentsInPlace</key> + <true/> + <key>UISupportsDocumentBrowser</key> + <false/> </dict> </plist> diff --git a/SFI/MainView.swift b/SFI/MainView.swift index e848f661fed0f724624c82e8ffd80924e2f6acc4..e66a2ee7175027e12294a65679f56755bad1ff9d 100644 --- a/SFI/MainView.swift +++ b/SFI/MainView.swift @@ -10,7 +10,9 @@ @State private var selection = NavigationPage.dashboard @State private var extensionProfile: ExtensionProfile? @State private var profileLoading = true @State private var logClient: LogClient! + @State private var importProfile: LibboxProfileContent? @State private var importRemoteProfile: LibboxImportRemoteProfile? + @State private var alert: Alert? var body: some View { viewBuilder { @@ -25,17 +27,19 @@ } else { ContentView() } } -import Libbox + .alertBinding($alert) +struct MainView: View { struct MainView: View { if newValue == .active { Task.detached { await loadProfile() } } - }) + } .environment(\.selection, $selection) .environment(\.extensionProfile, $extensionProfile) .environment(\.logClient, $logClient) + .environment(\.importProfile, $importProfile) .environment(\.importRemoteProfile, $importRemoteProfile) .handlesExternalEvents(preferring: [], allowing: ["*"]) .onOpenURL(perform: openURL) @@ -45,13 +49,29 @@ private func openURL(url: URL) { if url.host == "import-remote-profile" { var error: NSError? importRemoteProfile = LibboxParseRemoteProfileImportLink(url.absoluteString, &error) + if let error { + alert = Alert(error) import SwiftUI +import Library import Libbox +import SwiftUI + if selection != .profiles { + selection = .profiles + } + } else if url.pathExtension == "bpf" { + do { + _ = url.startAccessingSecurityScopedResource() + importProfile = try .from(Data(contentsOf: url)) + url.stopAccessingSecurityScopedResource() + } catch { + alert = Alert(error) return } if selection != .profiles { selection = .profiles } + } else { + alert = Alert(errorMessage: "Handled unknown URL \(url.absoluteString)") } } diff --git a/SFM/Info.plist b/SFM/Info.plist index 5e088a4735e093907c8b94982fea9d97572134dd..6d1e2bd7cca0987baff1fc90e2e8bb8935073752 100644 --- a/SFM/Info.plist +++ b/SFM/Info.plist @@ -32,5 +32,77 @@Any </dict> </dict> <plist version="1.0"> +<plist version="1.0"> + <array> + <dict> + <key>CFBundleTypeName</key> + <string>sing-box Profile</string> + <key>CFBundleTypeRole</key> + <string>Viewer</string> + <key>LSHandlerRank</key> + <string>Owner</string> + <key>LSItemContentTypes</key> + <array> + <string>io.nekohasekai.sfa.profile</string> + </array> + <key>CFBundleTypeIconFile</key> + <string>AppIcon.icns</string> + </dict> + </array> + <key>UTExportedTypeDeclarations</key> + <array> + <dict> + <key>UTTypeConformsTo</key> + <array> + <string>public.item</string> + <string>public.content</string> + <string>public.data</string> + </array> + <key>UTTypeDescription</key> + <string>sing-box Profile</string> + <key>UTTypeIconFiles</key> + <array/> + <key>UTTypeIdentifier</key> + <string>io.nekohasekai.sfa.profile</string> + <key>UTTypeTagSpecification</key> + <dict> + <key>public.filename-extension</key> + <array> + <string>bpf</string> + </array> + </dict> + </dict> + </array> + <key>UTImportedTypeDeclarations</key> + <array> + <dict> + <key>UTTypeConformsTo</key> + <array> + <string>public.item</string> + <string>public.content</string> + <string>public.data</string> + </array> + <key>UTTypeDescription</key> + <string>sing-box Profile</string> + <key>UTTypeIconFiles</key> + <array> + <string>AppIcon.icns</string> + </array> + <key>UTTypeIdentifier</key> + <string>io.nekohasekai.sfa.profile</string> + <key>UTTypeTagSpecification</key> + <dict> + <key>public.filename-extension</key> + <array> + <string>bpf</string> + </array> + </dict> + </dict> + </array> + <key>LSSupportsOpeningDocumentsInPlace</key> + <true/> + <key>UISupportsDocumentBrowser</key> + <false/> +<plist version="1.0"> <?xml version="1.0" encoding="UTF-8"?> </plist> diff --git a/SFM.System/Info.plist b/SFM.System/Info.plist index 43d3931a44eebf3c0f7a41dbf2609777c5c6350c..4bada1df2c49e20fac95df3aae9510c7e374e8a4 100644 --- a/SFM.System/Info.plist +++ b/SFM.System/Info.plist @@ -32,5 +32,77 @@Any </dict> </dict> <plist version="1.0"> +<plist version="1.0"> + <array> + <dict> + <key>CFBundleTypeName</key> + <string>sing-box Profile</string> + <key>CFBundleTypeRole</key> + <string>Viewer</string> + <key>LSHandlerRank</key> + <string>Owner</string> + <key>LSItemContentTypes</key> + <array> + <string>io.nekohasekai.sfa.profile</string> + </array> + <key>CFBundleTypeIconFile</key> + <string>AppIcon.icns</string> + </dict> + </array> + <key>UTExportedTypeDeclarations</key> + <array> + <dict> + <key>UTTypeConformsTo</key> + <array> + <string>public.item</string> + <string>public.content</string> + <string>public.data</string> + </array> + <key>UTTypeDescription</key> + <string>sing-box Profile</string> + <key>UTTypeIconFiles</key> + <array/> + <key>UTTypeIdentifier</key> + <string>io.nekohasekai.sfa.profile</string> + <key>UTTypeTagSpecification</key> + <dict> + <key>public.filename-extension</key> + <array> + <string>bpf</string> + </array> + </dict> + </dict> + </array> + <key>UTImportedTypeDeclarations</key> + <array> + <dict> + <key>UTTypeConformsTo</key> + <array> + <string>public.item</string> + <string>public.content</string> + <string>public.data</string> + </array> + <key>UTTypeDescription</key> + <string>sing-box Profile</string> + <key>UTTypeIconFiles</key> + <array> + <string>AppIcon.icns</string> + </array> + <key>UTTypeIdentifier</key> + <string>io.nekohasekai.sfa.profile</string> + <key>UTTypeTagSpecification</key> + <dict> + <key>public.filename-extension</key> + <array> + <string>bpf</string> + </array> + </dict> + </dict> + </array> + <key>LSSupportsOpeningDocumentsInPlace</key> + <true/> + <key>UISupportsDocumentBrowser</key> + <false/> +<plist version="1.0"> <?xml version="1.0" encoding="UTF-8"?> </plist> diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/tv - Top Shelf - Wide - 2320 x 720 pt.png b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/tv - Top Shelf - Wide - 2320 x 720 pt.png index 656bff3d9e9b094a4f02525f8ce84116753b4127..f09550ae8e2c1b2e891a63169c3cb0359f03d440 100644 Binary files a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/tv - Top Shelf - Wide - 2320 x 720 pt.png and b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/tv - Top Shelf - Wide - 2320 x 720 pt.png differ diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/tv - Top Shelf - Wide - 2320 x 720 [email protected] b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/tv - Top Shelf - Wide - 2320 x 720 [email protected] index a9ec8458779fc42a437927fdabffdd0d1e875d5e..2dc838014cd79ef12c2aefba8310800e2fc29dd9 100644 Binary files a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/tv - Top Shelf - Wide - 2320 x 720 [email protected] and b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/tv - Top Shelf - Wide - 2320 x 720 [email protected] differ diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/1920 x 720 pt.png b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/1920 x 720 pt.png index e958e94e42c8c1ee25e061b0b5fced7f52d606a2..084f2befee567f6721d0b4ce85ad623d8b13d907 100644 Binary files a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/1920 x 720 pt.png and b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/1920 x 720 pt.png differ diff --git a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/1920 x 720 [email protected] b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/1920 x 720 [email protected] index 34a7d28c596533fa32c7650a0af0e0fc3b7f3205..ee565e1576c7c08dba777214e9f2023d33f559fc 100644 Binary files a/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/1920 x 720 [email protected] and b/SFT/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/1920 x 720 [email protected] differ diff --git a/SFT/ContentView.swift b/SFT/ContentView.swift index 420bf6c1e98d4da662ec6ebe3412418f10ae0532..750338aef322c779d2f162c9e49ad3ccef84a7c1 100644 --- a/SFT/ContentView.swift +++ b/SFT/ContentView.swift @@ -33,7 +33,7 @@ .tag(page) .tabItem { page.label } } import SwiftUI -import Library + var body: some View { if !selection.wrappedValue.visible(extensionProfile) { selection.wrappedValue = NavigationPage.dashboard } diff --git a/SFT/MainView.swift b/SFT/MainView.swift index 6095b6d8769163ef6551425e75998d43bb2d4801..73df2cb4ad69b0973d7e00581bfb84b5a9025c3a 100644 --- a/SFT/MainView.swift +++ b/SFT/MainView.swift @@ -25,14 +25,14 @@ } else { ContentView() } } -import Libbox struct MainView: View { +import Libbox if newValue == .active { Task.detached { await loadProfile() } } - }) + } .environment(\.selection, $selection) .environment(\.extensionProfile, $extensionProfile) .environment(\.logClient, $logClient) diff --git a/sing-box.xcodeproj/project.pbxproj b/sing-box.xcodeproj/project.pbxproj index 723517d73682fc94894c5eefbae3ef93a8d1a6ff..a03431905110610bee21a0f82548e46842b8e358 100644 --- a/sing-box.xcodeproj/project.pbxproj +++ b/sing-box.xcodeproj/project.pbxproj @@ -86,6 +86,8 @@ 3AC03B9D2A72BF3500B7946F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3AC03B9C2A72BF3500B7946F /* Assets.xcassets */; }; 3AC194492A50013F00BD8CB9 /* IntentsExtension.appex in Embed ExtensionKit Extensions */ = {isa = PBXBuildFile; fileRef = 3A77016D2A4E6B34008F031F /* IntentsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 3AC1944F2A50247300BD8CB9 /* ApplicationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC1944E2A50247300BD8CB9 /* ApplicationDelegate.swift */; }; 3AC5EC082A6417470077AF34 /* DeviceCensorship.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC5EC072A6417470077AF34 /* DeviceCensorship.swift */; }; + 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 */; }; 3AD0953D2A70EB310052764E /* Profile+Share.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD0953C2A70EB310052764E /* Profile+Share.swift */; }; 3ADBB4252A7389640041D44F /* ProfileServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ADBB4242A7389640041D44F /* ProfileServer.swift */; }; @@ -468,6 +470,9 @@ 3AC03B9C2A72BF3500B7946F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 3AC1944E2A50247300BD8CB9 /* ApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationDelegate.swift; sourceTree = "<group>"; }; 3AC194512A50303300BD8CB9 /* ApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationDelegate.swift; sourceTree = "<group>"; }; 3A4EAD212A4FEB3C005435B3 /* ApplicationLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4EAD202A4FEB3C005435B3 /* ApplicationLibrary.swift */; }; + archiveVersion = 1; + 3AC729EF2A75D9D000FE8EC1 /* Profile+Transferable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Profile+Transferable.swift"; sourceTree = "<group>"; }; + 3AEECC1E2A6DFA79006A0E0C /* Library.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; archiveVersion = 1; 3AC8CF9A2A736C750002AF3C /* ImportProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportProfileView.swift; sourceTree = "<group>"; }; 3AD0953C2A70EB310052764E /* Profile+Share.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Profile+Share.swift"; sourceTree = "<group>"; }; @@ -832,6 +837,8 @@ 3A57DF362A4D5D2600690BC5 /* Profile+Date.swift */, 3A57DF412A4D927A00690BC5 /* Profile+Hashable.swift */, 3AD0953C2A70EB310052764E /* Profile+Share.swift */, // !$*UTF8*$! + 3AE4D0BE2A6E2DDC009FEA9E /* Library.framework in Embed Frameworks */, +// !$*UTF8*$! 3A99B42E2A752ABB0010D4B0 /* NavigationDestinationCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A99B42D2A752ABB0010D4B0 /* NavigationDestinationCompat.swift */; }; path = Database; sourceTree = "<group>"; @@ -937,6 +944,8 @@ 3AB1220A2A70FD500087CD55 /* Alert.swift */, 3A99B4292A7526990010D4B0 /* NavigationStackCompat.swift */, 3A99B42B2A75288C0010D4B0 /* ViewCompat.swift */, 3A4FB1682A7358C9007012B9 /* ApplicationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4FB1672A7358C9007012B9 /* ApplicationDelegate.swift */; }; + }; + 3AEECC1E2A6DFA79006A0E0C /* Library.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; }; ); path = Abstract; @@ -1380,6 +1389,7 @@ 3A4EAD2C2A4FEB77005435B3 /* EditProfileWindowView.swift in Sources */, 3A1CF2FA2A50F0BD000A8289 /* OutboundGroupItem.swift in Sources */, 3A99B42E2A752ABB0010D4B0 /* NavigationDestinationCompat.swift in Sources */, 3A4EAD332A4FEB7F005435B3 /* LogClient.swift in Sources */, + 3AC729F22A76088E00FE8EC1 /* ShareButton.swift in Sources */, 3A4EAD262A4FEB65005435B3 /* ExtensionStatusView.swift in Sources */, 3A4EAD252A4FEB65005435B3 /* StartStopButton.swift in Sources */, 3A4EAD2B2A4FEB6D005435B3 /* ViewBuilder.swift in Sources */, @@ -1433,6 +1443,7 @@ files = ( 3AE4D0B52A6E2BAC009FEA9E /* ExtensionProvider.swift in Sources */, 3A2223562A6E1BDE00C50B23 /* Variant.swift in Sources */, 3AEC214A2A45AA5600A63465 /* Profile+RW.swift in Sources */, + 3AC729F02A75D9D000FE8EC1 /* Profile+Transferable.swift in Sources */, 3A57DF422A4D927A00690BC5 /* Profile+Hashable.swift in Sources */, 3A7E90352A46756300D53052 /* SharedPreferences.swift in Sources */, 3AD0953D2A70EB310052764E /* Profile+Share.swift in Sources */, diff --git a/sing-box.xcodeproj/xcuserdata/sekai.xcuserdatad/xcschemes/xcschememanagement.plist b/sing-box.xcodeproj/xcuserdata/sekai.xcuserdatad/xcschemes/xcschememanagement.plist index a36df2a00cf4839bfe9a6898a66e3a9c2247b44a..6fd3a9efa8d711cfc6e5fde46023aa1ae5ed976d 100644 --- a/sing-box.xcodeproj/xcuserdata/sekai.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/sing-box.xcodeproj/xcuserdata/sekai.xcuserdatad/xcschemes/xcschememanagement.plist @@ -69,7 +69,7 @@ <key>MacLibrary.xcscheme_^#shared#^_</key> <dict> <key>orderHint</key> - <integer>4</integer> + <integer>2</integer> </dict> <key>MessageExtension.xcscheme_^#shared#^_</key> <dict> @@ -132,7 +132,7 @@ SFM.System.xcscheme_^#shared#^_ <dict> <key>orderHint</key> <plist version="1.0"> -<?xml version="1.0" encoding="UTF-8"?> +<plist version="1.0"> </dict> <key>SFM.xcscheme</key> <dict> @@ -142,7 +142,7 @@ <key>SFT.xcscheme_^#shared#^_</key> <dict> <key>orderHint</key> - <integer>2</integer> + <integer>5</integer> </dict> <key>SystemExtension.xcscheme_^#shared#^_</key> <dict>