Liu Song’s Projects


~/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>