Add profile sharing

This commit is contained in:
世界
2023-07-30 14:13:57 +08:00
parent 27ffee06e2
commit 2ca6c515b9
25 changed files with 675 additions and 123 deletions
@@ -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) .alertBinding($alert)
.onChange(of: profile.status, perform: { newValue in .onChangeCompat(of: profile.status) { newValue in
if newValue == .disconnecting || newValue == .connected { if newValue == .disconnecting || newValue == .connected {
Task.detached { Task.detached {
if let serviceError = try? String(contentsOf: ExtensionProvider.errorFile) { if let serviceError = try? String(contentsOf: ExtensionProvider.errorFile) {
@@ -90,22 +90,22 @@ public struct ActiveDashboardView: View {
} }
} }
} }
}) }
#if os(iOS) || os(tvOS) #if os(iOS) || os(tvOS)
.onChange(of: scenePhase, perform: { newValue in .onChangeCompat(of: scenePhase) { newValue in
if newValue == .active { if newValue == .active {
Task.detached { Task.detached {
await doReload() await doReload()
} }
} }
}) }
.onChange(of: selection.wrappedValue, perform: { newValue in .onChangeCompat(of: selection.wrappedValue) { newValue in
if newValue == .dashboard { if newValue == .dashboard {
Task.detached { Task.detached {
await doReload() await doReload()
} }
} }
}) }
#elseif os(macOS) #elseif os(macOS)
.onAppear { .onAppear {
if observer == nil { if observer == nil {
@@ -30,7 +30,7 @@ public struct DashboardView: View {
#endif #endif
} }
#if os(macOS) #if os(macOS)
.onChange(of: controlActiveState, perform: { newValue in .onChangeCompat(of: controlActiveState) { newValue in
if newValue != .inactive { if newValue != .inactive {
if Variant.useSystemExtension { if Variant.useSystemExtension {
if !isLoading { if !isLoading {
@@ -38,7 +38,7 @@ public struct DashboardView: View {
} }
} }
} }
}) }
#endif #endif
.navigationTitle("Dashboard") .navigationTitle("Dashboard")
} }
@@ -68,4 +68,17 @@ public extension EnvironmentValues {
self[importRemoteProfileKey.self] = newValue 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) #elseif os(macOS)
.padding() .padding()
#endif #endif
.onChange(of: profileContent) { _ in .onChangeCompat(of: profileContent) {
isChanged = true isChanged = true
} }
} }
@@ -6,13 +6,16 @@ public struct EditProfileView: View {
@Environment(\.openWindow) private var openWindow @Environment(\.openWindow) private var openWindow
#endif #endif
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var profile: Profile @EnvironmentObject private var profile: Profile
@State private var isLoading = false @State private var isLoading = false
@State private var isChanged = false @State private var isChanged = false
@State private var alert: Alert? @State private var alert: Alert?
private let updateCallback: (() -> Void)?
public init() {} public init(_ updateCallback: (() -> Void)? = nil) {
self.updateCallback = updateCallback
}
public var body: some View { public var body: some View {
FormView { FormView {
@@ -63,6 +66,11 @@ public struct EditProfileView: View {
} label: { } label: {
Text("View Content").foregroundColor(.accentColor) Text("View Content").foregroundColor(.accentColor)
} }
ShareButton($alert) {
Text("Share")
} items: {
try [profile.toContent().generateShareFile()]
}
#endif #endif
Button("Update") { Button("Update") {
isLoading = true isLoading = true
@@ -71,32 +79,24 @@ public struct EditProfileView: View {
} }
} }
.disabled(isLoading) .disabled(isLoading)
#if os(iOS) Button("Delete", role: .destructive) {
if #available(iOS 16.0, *) { Task.detached {
ShareLink(item: profile.shareLink) { await deleteProfile()
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)
} }
} }
} }
#endif #endif
} }
.onChangeCompat(of: profile.name) {
isChanged = true
} }
#endif .onChangeCompat(of: profile.remoteURL) {
isChanged = true
} }
.onChange(of: profile.name, perform: { _ in .onChangeCompat(of: profile.autoUpdate) {
isChanged = true isChanged = true
}) }
.onChange(of: profile.remoteURL, perform: { _ in
isChanged = true
})
.onChange(of: profile.autoUpdate, perform: { _ in
isChanged = true
})
.disabled(isLoading) .disabled(isLoading)
#if os(macOS) #if os(macOS)
.toolbar { .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 { private func saveProfile() async {
do { do {
_ = try ProfileManager.update(profile) _ = try ProfileManager.update(profile)
@@ -173,8 +184,16 @@ public struct EditProfileView: View {
} }
isChanged = false isChanged = false
isLoading = false isLoading = false
await performCallback()
}
private func performCallback() async {
if let updateCallback {
updateCallback()
} else {
await MainActor.run { await MainActor.run {
NotificationCenter.default.post(name: ProfileView.notificationName, object: nil) NotificationCenter.default.post(name: ProfileView.notificationName, object: nil)
} }
} }
}
} }
@@ -7,6 +7,7 @@ import SwiftUI
public struct ProfileView: View { public struct ProfileView: View {
public static let notificationName = Notification.Name("\(FilePath.packageName).update-profile") public static let notificationName = Notification.Name("\(FilePath.packageName).update-profile")
@Environment(\.importProfile) private var importProfile
@Environment(\.importRemoteProfile) private var importRemoteProfile @Environment(\.importRemoteProfile) private var importRemoteProfile
@State private var importRemoteProfileRequest: NewProfileView.ImportRequest? @State private var importRemoteProfileRequest: NewProfileView.ImportRequest?
@State private var importRemoteProfilePresented = false @State private var importRemoteProfilePresented = false
@@ -86,28 +87,7 @@ public struct ProfileView: View {
if editMode.isEditing == true { if editMode.isEditing == true {
Text(profile.name) Text(profile.name)
} else { } else {
NavigationLink { ProfileItem(self, profile)
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")
} }
} }
} }
@@ -124,44 +104,7 @@ public struct ProfileView: View {
FormView { FormView {
List { List {
ForEach(profileList, id: \.mustID) { profile in ForEach(profileList, id: \.mustID) { profile in
ProfileItem(self, profile)
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)
} }
.onMove(perform: moveProfile) .onMove(perform: moveProfile)
.onDelete(perform: deleteProfile) .onDelete(perform: deleteProfile)
@@ -175,6 +118,10 @@ public struct ProfileView: View {
.navigationTitle("Profiles") .navigationTitle("Profiles")
.alertBinding($alert, $isLoading) .alertBinding($alert, $isLoading)
.onAppear { .onAppear {
if let profile = importProfile.wrappedValue {
importProfile.wrappedValue = nil
createImportProfileDialog(profile)
}
if let remoteProfile = importRemoteProfile.wrappedValue { if let remoteProfile = importRemoteProfile.wrappedValue {
importRemoteProfile.wrappedValue = nil importRemoteProfile.wrappedValue = nil
createImportRemoteProfileDialog(remoteProfile) createImportRemoteProfileDialog(remoteProfile)
@@ -189,7 +136,13 @@ public struct ProfileView: View {
} }
#endif #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 { if let newValue {
importRemoteProfile.wrappedValue = nil importRemoteProfile.wrappedValue = nil
createImportRemoteProfileDialog(newValue) createImportRemoteProfileDialog(newValue)
@@ -221,11 +174,30 @@ public struct ProfileView: View {
#endif #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) { private func createImportRemoteProfileDialog(_ newValue: LibboxImportRemoteProfile) {
importRemoteProfileRequest = .init(name: newValue.name, url: newValue.url) importRemoteProfileRequest = .init(name: newValue.name, url: newValue.url)
alert = Alert( alert = Alert(
title: Text("Import Remote Profile"), 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")) { primaryButton: .default(Text("Import")) {
#if os(iOS) || os(tvOS) #if os(iOS) || os(tvOS)
importRemoteProfilePresented = true importRemoteProfilePresented = true
@@ -283,7 +255,7 @@ public struct ProfileView: View {
alert = Alert(error) alert = Alert(error)
return 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) #if os(macOS)
Section("MacOS") { Section("MacOS") {
Toggle("Start At Login", isOn: $startAtLogin) Toggle("Start At Login", isOn: $startAtLogin)
.onChange(of: startAtLogin) { newValue in .onChangeCompat(of: startAtLogin) { newValue in
Task.detached { Task.detached {
updateLoginItems(newValue) updateLoginItems(newValue)
} }
@@ -57,7 +57,7 @@ public struct SettingView: View {
} }
if showMenuBarExtra.wrappedValue { if showMenuBarExtra.wrappedValue {
Toggle("Keep Menu Bar in Background", isOn: $keepMenuBarInBackground) Toggle("Keep Menu Bar in Background", isOn: $keepMenuBarInBackground)
.onChange(of: keepMenuBarInBackground) { newValue in .onChangeCompat(of: keepMenuBarInBackground) { newValue in
Task.detached { Task.detached {
SharedPreferences.menuBarExtraInBackground = newValue 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") }
}
+22 -5
View File
@@ -10,8 +10,9 @@ public struct MainView: View {
@State private var extensionProfile: ExtensionProfile? @State private var extensionProfile: ExtensionProfile?
@State private var profileLoading = true @State private var profileLoading = true
@State private var logClient: LogClient! @State private var logClient: LogClient!
@State private var alert: Alert? @State private var importProfile: LibboxProfileContent?
@State private var importRemoteProfile: LibboxImportRemoteProfile? @State private var importRemoteProfile: LibboxImportRemoteProfile?
@State private var alert: Alert?
public init() {} public init() {}
public var body: some View { public var body: some View {
@@ -48,23 +49,25 @@ public struct MainView: View {
StartStopButton() StartStopButton()
} }
} }
.onChange(of: controlActiveState, perform: { newValue in .onChangeComat(of: controlActiveState) { newValue in
if newValue != .inactive { if newValue != .inactive {
Task { Task {
await loadProfile() await loadProfile()
connectLog() connectLog()
} }
} }
}) }
.onChange(of: selection, perform: { value in .onChangeCompat(of: selection) { value in
if value == .logs { if value == .logs {
connectLog() connectLog()
} }
}) }
.formStyle(.grouped) .formStyle(.grouped)
.environment(\.selection, $selection) .environment(\.selection, $selection)
.environment(\.extensionProfile, $extensionProfile) .environment(\.extensionProfile, $extensionProfile)
.environment(\.logClient, $logClient) .environment(\.logClient, $logClient)
.environment(\.importProfile, $importProfile)
.environment(\.importRemoteProfile, $importRemoteProfile) .environment(\.importRemoteProfile, $importRemoteProfile)
.handlesExternalEvents(preferring: [], allowing: ["*"]) .handlesExternalEvents(preferring: [], allowing: ["*"])
.onOpenURL(perform: openURL) .onOpenURL(perform: openURL)
@@ -80,6 +83,20 @@ public struct MainView: View {
if selection != .profiles { if selection != .profiles {
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)")
} }
} }
+1 -1
View File
@@ -137,7 +137,7 @@ public struct MenuView: View {
} }
} }
.pickerStyle(.inline) .pickerStyle(.inline)
.onChange(of: selectedProfileID) { _ in .onChangeCompat(of: selectedProfileID) {
reasserting = true reasserting = true
Task.detached { Task.detached {
await switchProfile(selectedProfileID!) await switchProfile(selectedProfileID!)
+1 -1
View File
@@ -26,7 +26,7 @@ public struct SidebarView: View {
it.visible(extensionProfile) it.visible(extensionProfile)
}, selection: selection) { it in }, selection: selection) { it in
it.label it.label
}.onChange(of: extensionProfile.status) { _ in }.onChangeCompat(of: extensionProfile.status) {
if !selection.wrappedValue.visible(extensionProfile) { if !selection.wrappedValue.visible(extensionProfile) {
selection.wrappedValue = NavigationPage.dashboard selection.wrappedValue = NavigationPage.dashboard
} }
+1 -1
View File
@@ -31,7 +31,7 @@ struct ContentView: View {
.tag(page) .tag(page)
.tabItem { page.label } .tabItem { page.label }
} }
}.onChange(of: extensionProfile.status) { _ in }.onChangeCompat(of: extensionProfile.status) {
if !selection.wrappedValue.visible(extensionProfile) { if !selection.wrappedValue.visible(extensionProfile) {
selection.wrappedValue = NavigationPage.dashboard selection.wrappedValue = NavigationPage.dashboard
} }
+81 -10
View File
@@ -2,16 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>NSApplicationServices</key>
<dict>
<key>Advertises</key>
<array>
<dict>
<key>NSApplicationServiceIdentifier</key>
<string>sing-box:profile</string>
</dict>
</array>
</dict>
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>io.nekohasekai.sfa.update_profiles</string> <string>io.nekohasekai.sfa.update_profiles</string>
@@ -33,6 +23,16 @@
</array> </array>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>NSApplicationServices</key>
<dict>
<key>Advertises</key>
<array>
<dict>
<key>NSApplicationServiceIdentifier</key>
<string>sing-box:profile</string>
</dict>
</array>
</dict>
<key>NSUbiquitousContainers</key> <key>NSUbiquitousContainers</key>
<dict> <dict>
<key>iCloud.io.nekohasekai.sfa</key> <key>iCloud.io.nekohasekai.sfa</key>
@@ -49,5 +49,76 @@
<array> <array>
<string>fetch</string> <string>fetch</string>
</array> </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> </dict>
</plist> </plist>
+22 -3
View File
@@ -10,7 +10,9 @@ struct MainView: View {
@State private var extensionProfile: ExtensionProfile? @State private var extensionProfile: ExtensionProfile?
@State private var profileLoading = true @State private var profileLoading = true
@State private var logClient: LogClient! @State private var logClient: LogClient!
@State private var importProfile: LibboxProfileContent?
@State private var importRemoteProfile: LibboxImportRemoteProfile? @State private var importRemoteProfile: LibboxImportRemoteProfile?
@State private var alert: Alert?
var body: some View { var body: some View {
viewBuilder { viewBuilder {
@@ -25,16 +27,18 @@ struct MainView: View {
ContentView() ContentView()
} }
} }
.onChange(of: scenePhase, perform: { newValue in .alertBinding($alert)
.onChangeCompat(of: scenePhase) { newValue in
if newValue == .active { if newValue == .active {
Task.detached { Task.detached {
await loadProfile() await loadProfile()
} }
} }
}) }
.environment(\.selection, $selection) .environment(\.selection, $selection)
.environment(\.extensionProfile, $extensionProfile) .environment(\.extensionProfile, $extensionProfile)
.environment(\.logClient, $logClient) .environment(\.logClient, $logClient)
.environment(\.importProfile, $importProfile)
.environment(\.importRemoteProfile, $importRemoteProfile) .environment(\.importRemoteProfile, $importRemoteProfile)
.handlesExternalEvents(preferring: [], allowing: ["*"]) .handlesExternalEvents(preferring: [], allowing: ["*"])
.onOpenURL(perform: openURL) .onOpenURL(perform: openURL)
@@ -44,12 +48,27 @@ struct MainView: View {
if url.host == "import-remote-profile" { if url.host == "import-remote-profile" {
var error: NSError? var error: NSError?
importRemoteProfile = LibboxParseRemoteProfileImportLink(url.absoluteString, &error) importRemoteProfile = LibboxParseRemoteProfileImportLink(url.absoluteString, &error)
if error != nil { if let error {
alert = Alert(error)
return return
} }
if selection != .profiles { if selection != .profiles {
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)")
} }
} }
+71
View File
@@ -31,5 +31,76 @@
<string>Any</string> <string>Any</string>
</dict> </dict>
</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> </dict>
</plist> </plist>
+71
View File
@@ -31,5 +31,76 @@
<string>Any</string> <string>Any</string>
</dict> </dict>
</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> </dict>
</plist> </plist>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 885 KiB

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 725 KiB

After

Width:  |  Height:  |  Size: 383 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

+1 -1
View File
@@ -32,7 +32,7 @@ struct ContentView: View {
.tag(page) .tag(page)
.tabItem { page.label } .tabItem { page.label }
} }
}.onChange(of: extensionProfile.status) { _ in }.onChangeCompat(of: extensionProfile.status) {
if !selection.wrappedValue.visible(extensionProfile) { if !selection.wrappedValue.visible(extensionProfile) {
selection.wrappedValue = NavigationPage.dashboard selection.wrappedValue = NavigationPage.dashboard
} }
+2 -2
View File
@@ -25,13 +25,13 @@ struct MainView: View {
ContentView() ContentView()
} }
} }
.onChange(of: scenePhase, perform: { newValue in .onChangeCompat(of: scenePhase) { newValue in
if newValue == .active { if newValue == .active {
Task.detached { Task.detached {
await loadProfile() await loadProfile()
} }
} }
}) }
.environment(\.selection, $selection) .environment(\.selection, $selection)
.environment(\.extensionProfile, $extensionProfile) .environment(\.extensionProfile, $extensionProfile)
.environment(\.logClient, $logClient) .environment(\.logClient, $logClient)
+8
View File
@@ -86,6 +86,8 @@
3AC194492A50013F00BD8CB9 /* IntentsExtension.appex in Embed ExtensionKit Extensions */ = {isa = PBXBuildFile; fileRef = 3A77016D2A4E6B34008F031F /* IntentsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 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 */; }; 3AC1944F2A50247300BD8CB9 /* ApplicationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC1944E2A50247300BD8CB9 /* ApplicationDelegate.swift */; };
3AC5EC082A6417470077AF34 /* DeviceCensorship.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC5EC072A6417470077AF34 /* DeviceCensorship.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 */; }; 3AC8CF9B2A736C750002AF3C /* ImportProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC8CF9A2A736C750002AF3C /* ImportProfileView.swift */; };
3AD0953D2A70EB310052764E /* Profile+Share.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AD0953C2A70EB310052764E /* Profile+Share.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 */; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 3ADBB4242A7389640041D44F /* ProfileServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileServer.swift; sourceTree = "<group>"; };
@@ -830,6 +834,7 @@
3A57DF362A4D5D2600690BC5 /* Profile+Date.swift */, 3A57DF362A4D5D2600690BC5 /* Profile+Date.swift */,
3A57DF412A4D927A00690BC5 /* Profile+Hashable.swift */, 3A57DF412A4D927A00690BC5 /* Profile+Hashable.swift */,
3AD0953C2A70EB310052764E /* Profile+Share.swift */, 3AD0953C2A70EB310052764E /* Profile+Share.swift */,
3AC729EF2A75D9D000FE8EC1 /* Profile+Transferable.swift */,
); );
path = Database; path = Database;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -935,6 +940,7 @@
3A99B4292A7526990010D4B0 /* NavigationStackCompat.swift */, 3A99B4292A7526990010D4B0 /* NavigationStackCompat.swift */,
3A99B42B2A75288C0010D4B0 /* ViewCompat.swift */, 3A99B42B2A75288C0010D4B0 /* ViewCompat.swift */,
3A99B42D2A752ABB0010D4B0 /* NavigationDestinationCompat.swift */, 3A99B42D2A752ABB0010D4B0 /* NavigationDestinationCompat.swift */,
3AC729F12A76088E00FE8EC1 /* ShareButton.swift */,
); );
path = Abstract; path = Abstract;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1377,6 +1383,7 @@
3A1CF2FA2A50F0BD000A8289 /* OutboundGroupItem.swift in Sources */, 3A1CF2FA2A50F0BD000A8289 /* OutboundGroupItem.swift in Sources */,
3A99B42E2A752ABB0010D4B0 /* NavigationDestinationCompat.swift in Sources */, 3A99B42E2A752ABB0010D4B0 /* NavigationDestinationCompat.swift in Sources */,
3A4EAD332A4FEB7F005435B3 /* LogClient.swift in Sources */, 3A4EAD332A4FEB7F005435B3 /* LogClient.swift in Sources */,
3AC729F22A76088E00FE8EC1 /* ShareButton.swift in Sources */,
3A4EAD262A4FEB65005435B3 /* ExtensionStatusView.swift in Sources */, 3A4EAD262A4FEB65005435B3 /* ExtensionStatusView.swift in Sources */,
3A4EAD252A4FEB65005435B3 /* StartStopButton.swift in Sources */, 3A4EAD252A4FEB65005435B3 /* StartStopButton.swift in Sources */,
3A4EAD2B2A4FEB6D005435B3 /* ViewBuilder.swift in Sources */, 3A4EAD2B2A4FEB6D005435B3 /* ViewBuilder.swift in Sources */,
@@ -1430,6 +1437,7 @@
3AE4D0B52A6E2BAC009FEA9E /* ExtensionProvider.swift in Sources */, 3AE4D0B52A6E2BAC009FEA9E /* ExtensionProvider.swift in Sources */,
3A2223562A6E1BDE00C50B23 /* Variant.swift in Sources */, 3A2223562A6E1BDE00C50B23 /* Variant.swift in Sources */,
3AEC214A2A45AA5600A63465 /* Profile+RW.swift in Sources */, 3AEC214A2A45AA5600A63465 /* Profile+RW.swift in Sources */,
3AC729F02A75D9D000FE8EC1 /* Profile+Transferable.swift in Sources */,
3A57DF422A4D927A00690BC5 /* Profile+Hashable.swift in Sources */, 3A57DF422A4D927A00690BC5 /* Profile+Hashable.swift in Sources */,
3A7E90352A46756300D53052 /* SharedPreferences.swift in Sources */, 3A7E90352A46756300D53052 /* SharedPreferences.swift in Sources */,
3AD0953D2A70EB310052764E /* Profile+Share.swift in Sources */, 3AD0953D2A70EB310052764E /* Profile+Share.swift in Sources */,
@@ -69,7 +69,7 @@
<key>MacLibrary.xcscheme_^#shared#^_</key> <key>MacLibrary.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>4</integer> <integer>2</integer>
</dict> </dict>
<key>MessageExtension.xcscheme_^#shared#^_</key> <key>MessageExtension.xcscheme_^#shared#^_</key>
<dict> <dict>
@@ -131,7 +131,7 @@
<key>SFM.System.xcscheme_^#shared#^_</key> <key>SFM.System.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>5</integer> <integer>4</integer>
</dict> </dict>
<key>SFM.xcscheme</key> <key>SFM.xcscheme</key>
<dict> <dict>
@@ -141,7 +141,7 @@
<key>SFT.xcscheme_^#shared#^_</key> <key>SFT.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>2</integer> <integer>5</integer>
</dict> </dict>
<key>SystemExtension.xcscheme_^#shared#^_</key> <key>SystemExtension.xcscheme_^#shared#^_</key>
<dict> <dict>