Add profile sharing
@@ -0,0 +1,97 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
#if canImport(UIKit)
|
||||||
|
import UIKit
|
||||||
|
#elseif canImport(AppKit)
|
||||||
|
import AppKit
|
||||||
|
#endif
|
||||||
|
|
||||||
|
public struct ShareButton<Label>: View where Label: View {
|
||||||
|
private let items: () throws -> [Any]
|
||||||
|
private let label: Label
|
||||||
|
@Binding private var alert: Alert?
|
||||||
|
|
||||||
|
public init(_ alert: Binding<Alert?>, @ViewBuilder label: () -> Label, items: @escaping () throws -> [Any]) {
|
||||||
|
_alert = alert
|
||||||
|
self.items = items
|
||||||
|
self.label = label()
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(AppKit)
|
||||||
|
@State private var sharePresented = false
|
||||||
|
#endif
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
Button {
|
||||||
|
#if os(iOS)
|
||||||
|
do {
|
||||||
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
|
||||||
|
try windowScene.keyWindow?.rootViewController?.present(UIActivityViewController(activityItems: items(), applicationActivities: nil), animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
alert = Alert(error)
|
||||||
|
}
|
||||||
|
#elseif canImport(AppKit)
|
||||||
|
sharePresented = true
|
||||||
|
#endif
|
||||||
|
} label: {
|
||||||
|
label
|
||||||
|
}
|
||||||
|
#if canImport(AppKit)
|
||||||
|
.background(SharingServicePicker($sharePresented, $alert, items))
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private func shareItems() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(AppKit)
|
||||||
|
private struct SharingServicePicker: NSViewRepresentable {
|
||||||
|
@Binding private var isPresented: Bool
|
||||||
|
@Binding private var alert: Alert?
|
||||||
|
private let items: () throws -> [Any]
|
||||||
|
|
||||||
|
init(_ isPresented: Binding<Bool>, _ alert: Binding<Alert?>, _ items: @escaping () throws -> [Any]) {
|
||||||
|
_isPresented = isPresented
|
||||||
|
_alert = alert
|
||||||
|
self.items = items
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeNSView(context _: Context) -> NSView {
|
||||||
|
let view = NSView()
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNSView(_ nsView: NSView, context: Context) {
|
||||||
|
if isPresented {
|
||||||
|
do {
|
||||||
|
let picker = try NSSharingServicePicker(items: items())
|
||||||
|
picker.delegate = context.coordinator
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
picker.show(relativeTo: .zero, of: nsView, preferredEdge: .minY)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
alert = Alert(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
Coordinator(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coordinator: NSObject, NSSharingServicePickerDelegate {
|
||||||
|
private let parent: SharingServicePicker
|
||||||
|
|
||||||
|
init(_ parent: SharingServicePicker) {
|
||||||
|
self.parent = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, didChoose _: NSSharingService?) {
|
||||||
|
sharingServicePicker.delegate = nil
|
||||||
|
parent.isPresented = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -79,7 +79,7 @@ public struct ActiveDashboardView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alertBinding($alert)
|
.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
|
#endif
|
||||||
}
|
}
|
||||||
.onChange(of: profile.name, perform: { _ in
|
.onChangeCompat(of: profile.name) {
|
||||||
isChanged = true
|
isChanged = true
|
||||||
})
|
}
|
||||||
.onChange(of: profile.remoteURL, perform: { _ in
|
.onChangeCompat(of: profile.remoteURL) {
|
||||||
isChanged = true
|
isChanged = true
|
||||||
})
|
}
|
||||||
.onChange(of: profile.autoUpdate, perform: { _ in
|
.onChangeCompat(of: profile.autoUpdate) {
|
||||||
isChanged = true
|
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 MainActor.run {
|
await performCallback()
|
||||||
NotificationCenter.default.post(name: ProfileView.notificationName, object: nil)
|
}
|
||||||
|
|
||||||
|
private func performCallback() async {
|
||||||
|
if let updateCallback {
|
||||||
|
updateCallback()
|
||||||
|
} else {
|
||||||
|
await MainActor.run {
|
||||||
|
NotificationCenter.default.post(name: ProfileView.notificationName, object: nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import SwiftUI
|
|||||||
public struct ProfileView: View {
|
public 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") }
|
||||||
|
}
|
||||||
@@ -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)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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!)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 885 KiB After Width: | Height: | Size: 368 KiB |
|
Before Width: | Height: | Size: 3.3 MiB After Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 725 KiB After Width: | Height: | Size: 383 KiB |
|
Before Width: | Height: | Size: 2.7 MiB After Width: | Height: | Size: 2.2 MiB |
@@ -32,7 +32,7 @@ struct ContentView: View {
|
|||||||
.tag(page)
|
.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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||