Add profile sharing
@@ -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
|
||||
@@ -79,7 +79,7 @@ public struct ActiveDashboardView: View {
|
||||
}
|
||||
}
|
||||
.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,22 +90,22 @@ public struct ActiveDashboardView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
#if os(iOS) || os(tvOS)
|
||||
.onChange(of: scenePhase, perform: { newValue in
|
||||
.onChangeCompat(of: scenePhase) { newValue in
|
||||
if newValue == .active {
|
||||
Task.detached {
|
||||
await doReload()
|
||||
}
|
||||
}
|
||||
})
|
||||
.onChange(of: selection.wrappedValue, perform: { newValue in
|
||||
}
|
||||
.onChangeCompat(of: selection.wrappedValue) { newValue in
|
||||
if newValue == .dashboard {
|
||||
Task.detached {
|
||||
await doReload()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
#elseif os(macOS)
|
||||
.onAppear {
|
||||
if observer == nil {
|
||||
|
||||
@@ -30,7 +30,7 @@ public struct DashboardView: View {
|
||||
#endif
|
||||
}
|
||||
#if os(macOS)
|
||||
.onChange(of: controlActiveState, perform: { newValue in
|
||||
.onChangeCompat(of: controlActiveState) { newValue in
|
||||
if newValue != .inactive {
|
||||
if Variant.useSystemExtension {
|
||||
if !isLoading {
|
||||
@@ -38,7 +38,7 @@ public struct DashboardView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
#endif
|
||||
.navigationTitle("Dashboard")
|
||||
}
|
||||
|
||||
@@ -68,4 +68,17 @@ public extension EnvironmentValues {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
#elseif os(macOS)
|
||||
.padding()
|
||||
#endif
|
||||
.onChange(of: profileContent) { _ in
|
||||
.onChangeCompat(of: profileContent) {
|
||||
isChanged = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,16 @@ public struct EditProfileView: View {
|
||||
@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?
|
||||
|
||||
public init() {}
|
||||
private let updateCallback: (() -> Void)?
|
||||
public init(_ updateCallback: (() -> Void)? = nil) {
|
||||
self.updateCallback = updateCallback
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
FormView {
|
||||
@@ -63,6 +66,11 @@ public struct EditProfileView: View {
|
||||
} label: {
|
||||
Text("View Content").foregroundColor(.accentColor)
|
||||
}
|
||||
ShareButton($alert) {
|
||||
Text("Share")
|
||||
} items: {
|
||||
try [profile.toContent().generateShareFile()]
|
||||
}
|
||||
#endif
|
||||
Button("Update") {
|
||||
isLoading = true
|
||||
@@ -71,32 +79,24 @@ public struct EditProfileView: View {
|
||||
}
|
||||
}
|
||||
.disabled(isLoading)
|
||||
#if os(iOS)
|
||||
if #available(iOS 16.0, *) {
|
||||
ShareLink(item: profile.shareLink) {
|
||||
Text("Share")
|
||||
}
|
||||
} else {
|
||||
Button("Share") {
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
|
||||
windowScene.keyWindow?.rootViewController?.present(UIActivityViewController(activityItems: [profile.shareLink], applicationActivities: nil), animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
Button("Delete", role: .destructive) {
|
||||
Task.detached {
|
||||
await deleteProfile()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.onChange(of: profile.name, perform: { _ in
|
||||
.onChangeCompat(of: profile.name) {
|
||||
isChanged = true
|
||||
})
|
||||
.onChange(of: profile.remoteURL, perform: { _ in
|
||||
}
|
||||
.onChangeCompat(of: profile.remoteURL) {
|
||||
isChanged = true
|
||||
})
|
||||
.onChange(of: profile.autoUpdate, perform: { _ in
|
||||
}
|
||||
.onChangeCompat(of: profile.autoUpdate) {
|
||||
isChanged = true
|
||||
})
|
||||
}
|
||||
.disabled(isLoading)
|
||||
#if os(macOS)
|
||||
.toolbar {
|
||||
@@ -164,6 +164,17 @@ public struct EditProfileView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteProfile() async {
|
||||
do {
|
||||
try ProfileManager.delete(profile)
|
||||
} catch {
|
||||
alert = Alert(error)
|
||||
return
|
||||
}
|
||||
await performCallback()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private func saveProfile() async {
|
||||
do {
|
||||
_ = try ProfileManager.update(profile)
|
||||
@@ -173,8 +184,16 @@ public struct EditProfileView: View {
|
||||
}
|
||||
isChanged = false
|
||||
isLoading = false
|
||||
await MainActor.run {
|
||||
NotificationCenter.default.post(name: ProfileView.notificationName, object: nil)
|
||||
await performCallback()
|
||||
}
|
||||
|
||||
private func performCallback() async {
|
||||
if let updateCallback {
|
||||
updateCallback()
|
||||
} else {
|
||||
await MainActor.run {
|
||||
NotificationCenter.default.post(name: ProfileView.notificationName, object: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import SwiftUI
|
||||
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,28 +87,7 @@ public struct ProfileView: View {
|
||||
if editMode.isEditing == true {
|
||||
Text(profile.name)
|
||||
} else {
|
||||
NavigationLink {
|
||||
EditProfileView().environmentObject(profile)
|
||||
} label: {
|
||||
Text(profile.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
.contextMenu {
|
||||
if profile.type == .remote {
|
||||
Button {
|
||||
isUpdating = true
|
||||
Task.detached {
|
||||
updateProfile(profile)
|
||||
}
|
||||
} label: {
|
||||
Label("Update", systemImage: "arrow.clockwise")
|
||||
}
|
||||
}
|
||||
Button(role: .destructive) {
|
||||
deleteProfile(profile)
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash.fill")
|
||||
ProfileItem(self, profile)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,44 +104,7 @@ public struct ProfileView: View {
|
||||
FormView {
|
||||
List {
|
||||
ForEach(profileList, id: \.mustID) { profile in
|
||||
|
||||
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: {
|
||||
isUpdating = true
|
||||
Task.detached {
|
||||
updateProfile(profile)
|
||||
}
|
||||
}, label: {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
})
|
||||
ShareLink(item: profile.shareLink) {
|
||||
Image(systemName: "square.and.arrow.up.fill")
|
||||
}
|
||||
}
|
||||
Button(action: {
|
||||
openWindow(id: EditProfileWindowView.windowID, value: profile.mustID)
|
||||
}, label: {
|
||||
Image(systemName: "pencil")
|
||||
})
|
||||
Button(action: {
|
||||
deleteProfile(profile)
|
||||
}, label: {
|
||||
Image(systemName: "trash.fill")
|
||||
})
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
ProfileItem(self, profile)
|
||||
}
|
||||
.onMove(perform: moveProfile)
|
||||
.onDelete(perform: deleteProfile)
|
||||
@@ -175,6 +118,10 @@ public struct ProfileView: View {
|
||||
.navigationTitle("Profiles")
|
||||
.alertBinding($alert, $isLoading)
|
||||
.onAppear {
|
||||
if let profile = importProfile.wrappedValue {
|
||||
importProfile.wrappedValue = nil
|
||||
createImportProfileDialog(profile)
|
||||
}
|
||||
if let remoteProfile = importRemoteProfile.wrappedValue {
|
||||
importRemoteProfile.wrappedValue = nil
|
||||
createImportRemoteProfileDialog(remoteProfile)
|
||||
@@ -189,7 +136,13 @@ public struct ProfileView: View {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.onChange(of: importRemoteProfile.wrappedValue) { newValue in
|
||||
.onChangeCompat(of: importProfile.wrappedValue) { newValue in
|
||||
if let newValue {
|
||||
importProfile.wrappedValue = nil
|
||||
createImportProfileDialog(newValue)
|
||||
}
|
||||
}
|
||||
.onChangeCompat(of: importRemoteProfile.wrappedValue) { newValue in
|
||||
if let newValue {
|
||||
importRemoteProfile.wrappedValue = nil
|
||||
createImportRemoteProfileDialog(newValue)
|
||||
@@ -221,11 +174,30 @@ public struct ProfileView: View {
|
||||
#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
|
||||
@@ -283,7 +255,7 @@ public struct ProfileView: View {
|
||||
alert = Alert(error)
|
||||
return
|
||||
}
|
||||
isLoading = true
|
||||
doReload()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,4 +285,103 @@ public struct ProfileView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ public struct SettingView: View {
|
||||
#if os(macOS)
|
||||
Section("MacOS") {
|
||||
Toggle("Start At Login", isOn: $startAtLogin)
|
||||
.onChange(of: startAtLogin) { newValue in
|
||||
.onChangeCompat(of: startAtLogin) { newValue in
|
||||
Task.detached {
|
||||
updateLoginItems(newValue)
|
||||
}
|
||||
@@ -57,7 +57,7 @@ public struct SettingView: View {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
@@ -10,8 +10,9 @@ public struct MainView: View {
|
||||
@State private var extensionProfile: ExtensionProfile?
|
||||
@State private var profileLoading = true
|
||||
@State private var logClient: LogClient!
|
||||
@State private var alert: Alert?
|
||||
@State private var importProfile: LibboxProfileContent?
|
||||
@State private var importRemoteProfile: LibboxImportRemoteProfile?
|
||||
@State private var alert: Alert?
|
||||
|
||||
public init() {}
|
||||
public var body: some View {
|
||||
@@ -48,23 +49,25 @@ public struct MainView: View {
|
||||
StartStopButton()
|
||||
}
|
||||
}
|
||||
.onChange(of: controlActiveState, perform: { newValue in
|
||||
.onChangeComat(of: controlActiveState) { newValue in
|
||||
if newValue != .inactive {
|
||||
Task {
|
||||
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)
|
||||
@@ -80,6 +83,20 @@ public struct MainView: View {
|
||||
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)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ public struct MenuView: View {
|
||||
}
|
||||
}
|
||||
.pickerStyle(.inline)
|
||||
.onChange(of: selectedProfileID) { _ in
|
||||
.onChangeCompat(of: selectedProfileID) {
|
||||
reasserting = true
|
||||
Task.detached {
|
||||
await switchProfile(selectedProfileID!)
|
||||
|
||||
@@ -26,7 +26,7 @@ public struct SidebarView: View {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ struct ContentView: View {
|
||||
.tag(page)
|
||||
.tabItem { page.label }
|
||||
}
|
||||
}.onChange(of: extensionProfile.status) { _ in
|
||||
}.onChangeCompat(of: extensionProfile.status) {
|
||||
if !selection.wrappedValue.visible(extensionProfile) {
|
||||
selection.wrappedValue = NavigationPage.dashboard
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
<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>
|
||||
|
||||
@@ -10,7 +10,9 @@ struct MainView: View {
|
||||
@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,16 +27,18 @@ struct MainView: View {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
.onChange(of: scenePhase, perform: { newValue in
|
||||
.alertBinding($alert)
|
||||
.onChangeCompat(of: scenePhase) { newValue in
|
||||
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)
|
||||
@@ -44,12 +48,27 @@ struct MainView: View {
|
||||
if url.host == "import-remote-profile" {
|
||||
var error: NSError?
|
||||
importRemoteProfile = LibboxParseRemoteProfileImportLink(url.absoluteString, &error)
|
||||
if error != nil {
|
||||
if let error {
|
||||
alert = Alert(error)
|
||||
return
|
||||
}
|
||||
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)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,5 +31,76 @@
|
||||
<string>Any</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<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>
|
||||
|
||||
@@ -31,5 +31,76 @@
|
||||
<string>Any</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<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>
|
||||
|
||||
|
Before Width: | Height: | Size: 885 KiB After Width: | Height: | Size: 368 KiB |
|
Before Width: | Height: | Size: 3.3 MiB After Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 725 KiB After Width: | Height: | Size: 383 KiB |
|
Before Width: | Height: | Size: 2.7 MiB After Width: | Height: | Size: 2.2 MiB |
@@ -32,7 +32,7 @@ struct ContentView: View {
|
||||
.tag(page)
|
||||
.tabItem { page.label }
|
||||
}
|
||||
}.onChange(of: extensionProfile.status) { _ in
|
||||
}.onChangeCompat(of: extensionProfile.status) {
|
||||
if !selection.wrappedValue.visible(extensionProfile) {
|
||||
selection.wrappedValue = NavigationPage.dashboard
|
||||
}
|
||||
|
||||
@@ -25,13 +25,13 @@ struct MainView: View {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
.onChange(of: scenePhase, perform: { newValue in
|
||||
.onChangeCompat(of: scenePhase) { newValue in
|
||||
if newValue == .active {
|
||||
Task.detached {
|
||||
await loadProfile()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
.environment(\.selection, $selection)
|
||||
.environment(\.extensionProfile, $extensionProfile)
|
||||
.environment(\.logClient, $logClient)
|
||||
|
||||
@@ -86,6 +86,8 @@
|
||||
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,8 @@
|
||||
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>"; };
|
||||
3AC5EC072A6417470077AF34 /* DeviceCensorship.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceCensorship.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -830,6 +834,7 @@
|
||||
3A57DF362A4D5D2600690BC5 /* Profile+Date.swift */,
|
||||
3A57DF412A4D927A00690BC5 /* Profile+Hashable.swift */,
|
||||
3AD0953C2A70EB310052764E /* Profile+Share.swift */,
|
||||
3AC729EF2A75D9D000FE8EC1 /* Profile+Transferable.swift */,
|
||||
);
|
||||
path = Database;
|
||||
sourceTree = "<group>";
|
||||
@@ -935,6 +940,7 @@
|
||||
3A99B4292A7526990010D4B0 /* NavigationStackCompat.swift */,
|
||||
3A99B42B2A75288C0010D4B0 /* ViewCompat.swift */,
|
||||
3A99B42D2A752ABB0010D4B0 /* NavigationDestinationCompat.swift */,
|
||||
3AC729F12A76088E00FE8EC1 /* ShareButton.swift */,
|
||||
);
|
||||
path = Abstract;
|
||||
sourceTree = "<group>";
|
||||
@@ -1377,6 +1383,7 @@
|
||||
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 */,
|
||||
@@ -1430,6 +1437,7 @@
|
||||
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 */,
|
||||
|
||||
@@ -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>
|
||||
@@ -131,7 +131,7 @@
|
||||
<key>SFM.System.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>5</integer>
|
||||
<integer>4</integer>
|
||||
</dict>
|
||||
<key>SFM.xcscheme</key>
|
||||
<dict>
|
||||
@@ -141,7 +141,7 @@
|
||||
<key>SFT.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
<integer>5</integer>
|
||||
</dict>
|
||||
<key>SystemExtension.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
||||