Prepare airdrop support

This commit is contained in:
世界
2023-07-26 20:38:51 +08:00
parent 486a685bb5
commit 2bd462c789
52 changed files with 488 additions and 455 deletions
@@ -0,0 +1,47 @@
import Foundation
import SwiftUI
public extension Alert {
init(_ error: Error, _ dismissAction: (() -> Void)? = nil) {
self.init(
errorMessage: error.localizedDescription,
dismissAction
)
}
init(errorMessage: String, _ dismissAction: (() -> Void)? = nil) {
self.init(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("Ok")) {
dismissAction?()
}
)
}
}
public extension View {
func alertBinding(_ binding: Binding<Alert?>) -> some View {
alert(isPresented: Binding(get: {
binding.wrappedValue != nil
}, set: { newValue, _ in
if !newValue {
binding.wrappedValue = nil
}
})) {
binding.wrappedValue!
}
}
func alertBinding(_ binding: Binding<Alert?>, _ isLoading: Binding<Bool>) -> some View {
alert(isPresented: Binding(get: {
binding.wrappedValue != nil
}, set: { newValue, _ in
if !newValue, !isLoading.wrappedValue {
binding.wrappedValue = nil
}
})) {
binding.wrappedValue!
}
}
}
@@ -16,9 +16,7 @@ public struct ActiveDashboardView: View {
@State private var selectedProfileID: Int64!
@State private var reasserting = false
@State private var observer: Any?
@State private var errorPresented = false
@State private var errorMessage = ""
@State private var alert: Alert?
public init() {}
@@ -78,43 +76,37 @@ public struct ActiveDashboardView: View {
}
}
}
.alert(isPresented: $errorPresented) {
Alert(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("Ok"))
)
}
.alertBinding($alert)
#if os(iOS)
.onChange(of: scenePhase, perform: { newValue in
if newValue == .active {
Task.detached {
await doReload()
}
}
})
.onChange(of: selection.wrappedValue, perform: { newValue in
if newValue == .dashboard {
Task.detached {
await doReload()
}
}
})
#elseif os(macOS)
.onAppear {
if observer == nil {
observer = NotificationCenter.default.addObserver(forName: ActiveDashboardView.NotificationUpdateSelectedProfile, object: nil, queue: nil, using: { _ in
.onChange(of: scenePhase, perform: { newValue in
if newValue == .active {
Task.detached {
await doReload()
}
})
}
})
.onChange(of: selection.wrappedValue, perform: { newValue in
if newValue == .dashboard {
Task.detached {
await doReload()
}
}
})
#elseif os(macOS)
.onAppear {
if observer == nil {
observer = NotificationCenter.default.addObserver(forName: ActiveDashboardView.NotificationUpdateSelectedProfile, object: nil, queue: nil, using: { _ in
Task.detached {
await doReload()
}
})
}
}
}
.onDisappear {
if let observer {
NotificationCenter.default.removeObserver(observer)
.onDisappear {
if let observer {
NotificationCenter.default.removeObserver(observer)
}
}
}
#endif
}
@@ -132,8 +124,7 @@ public struct ActiveDashboardView: View {
do {
profileList = try ProfileManager.list()
} catch {
errorMessage = error.localizedDescription
errorPresented = true
alert = Alert(error)
return
}
if profileList.isEmpty {
@@ -158,8 +149,7 @@ public struct ActiveDashboardView: View {
do {
try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)?.serviceReload()
} catch {
errorMessage = error.localizedDescription
errorPresented = true
alert = Alert(error)
}
}
reasserting = false
@@ -7,8 +7,7 @@ public struct ExtensionStatusView: View {
@State private var message: LibboxStatusMessage?
@State private var connectTask: Task<Void, Error>?
@State private var columnCount: Int = 4
@State private var errorPresented = false
@State private var errorMessage = ""
@State private var alert: Alert?
private let infoFont = Font.system(.caption, design: .monospaced)
@@ -67,13 +66,7 @@ public struct ExtensionStatusView: View {
}
commandClient = nil
}
.alert(isPresented: $errorPresented) {
Alert(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("Ok"))
)
}
.alertBinding($alert)
}
private func doReload() {
@@ -125,8 +118,7 @@ public struct ExtensionStatusView: View {
do {
try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)?.closeConnections()
} catch {
errorMessage = error.localizedDescription
errorPresented = true
alert = Alert(error)
}
}
@@ -4,8 +4,7 @@ import SwiftUI
public struct InstallProfileButton: View {
@Environment(\.extensionProfile) private var extensionProfile
@State private var errorPresented = false
@State private var errorMessage = ""
@State private var alert: Alert?
public init() {}
@@ -15,21 +14,14 @@ public struct InstallProfileButton: View {
await installProfile()
}
}
.alert(isPresented: $errorPresented) {
Alert(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("Ok"))
)
}
.alertBinding($alert)
}
private func installProfile() async {
do {
try await ExtensionProfile.install()
} catch {
errorMessage = error.localizedDescription
errorPresented = true
alert = Alert(error)
}
}
}
@@ -4,8 +4,7 @@
import SwiftUI
public struct InstallSystemExtensionButton: View {
@State private var errorPresented = false
@State private var errorMessage = ""
@State private var alert: Alert?
private let callback: () -> Void
public init(_ callback: @escaping () -> Void) {
self.callback = callback
@@ -17,27 +16,19 @@
await installSystemExtension()
}
}
.alert(isPresented: $errorPresented) {
Alert(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("Ok"))
)
}
.alertBinding($alert)
}
private func installSystemExtension() async {
do {
if let result = try await SystemExtension.install() {
if result == .willCompleteAfterReboot {
errorMessage = "Need reboot"
errorPresented = true
alert = Alert(errorMessage: "Need Reboot")
}
}
callback()
} catch {
errorMessage = error.localizedDescription
errorPresented = true
alert = Alert(error)
}
}
}
@@ -41,8 +41,7 @@ public struct StartStopButton: View {
private struct Button0: View {
@Environment(\.logClient) private var logClient
@ObservedObject private var profile: ExtensionProfile
@State private var errorPresented = false
@State private var errorMessage = ""
@State private var alert: Alert?
init(_ profile: ExtensionProfile) {
self.profile = profile
@@ -75,13 +74,7 @@ public struct StartStopButton: View {
#endif
}
.disabled(!profile.status.isEnabled)
.alert(isPresented: $errorPresented) {
Alert(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("Ok"))
)
}
.alertBinding($alert)
}
private func switchProfile(_ isEnabled: Bool) async {
@@ -93,8 +86,7 @@ public struct StartStopButton: View {
profile.stop()
}
} catch {
errorMessage = error.localizedDescription
errorPresented = true
alert = Alert(error)
return
}
}
@@ -1,4 +1,5 @@
import Foundation
import Libbox
import Library
import SwiftUI
@@ -54,4 +55,17 @@ public extension EnvironmentValues {
self[logClientKey.self] = newValue
}
}
private struct importRemoteProfileKey: EnvironmentKey {
static var defaultValue: Binding<LibboxImportRemoteProfile?> = .constant(nil)
}
var importRemoteProfile: Binding<LibboxImportRemoteProfile?> {
get {
self[importRemoteProfileKey.self]
}
set {
self[importRemoteProfileKey.self] = newValue
}
}
}
@@ -3,16 +3,13 @@ import Library
import SwiftUI
public struct GroupView: View {
private var expland: Binding<Bool>
@Binding private var expland: Bool
@State private var group: OutboundGroup
@State private var geometryWidth: CGFloat = 300
@State private var errorPresented = false
@State private var errorMessage = ""
public init(_ group: OutboundGroup, _ expland: Binding<Bool>) {
self.group = group
self.expland = expland
_expland = expland
}
private var title: some View {
@@ -28,9 +25,9 @@ public struct GroupView: View {
.background(Color.gray.opacity(0.5))
.cornerRadius(4)
Button {
expland.wrappedValue = !expland.wrappedValue
expland = !expland
} label: {
if expland.wrappedValue {
if expland {
Image(systemName: "arrow.down.to.line")
} else {
Image(systemName: "arrow.up.to.line")
@@ -51,18 +48,11 @@ public struct GroupView: View {
#endif
Spacer(minLength: 6)
}
.alert(isPresented: $errorPresented) {
Alert(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("Ok"))
)
}
}
public var body: some View {
Section {
if expland.wrappedValue {
if expland {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()),
count: explandColumnCount()))
{
@@ -123,12 +113,7 @@ public struct GroupView: View {
}
private func doURLTest() {
do {
try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)!.urlTest(group.tag)
} catch {
errorMessage = error.localizedDescription
errorPresented = true
}
try? LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)!.urlTest(group.tag)
}
}
@@ -26,10 +26,7 @@ public struct EditProfileContentView: View {
@State private var profile: Profile!
@State private var profileContent: String = ""
@State private var isChanged = false
@State private var errorPresented = false
@State private var errorMessage = ""
@State private var fatalError = false
@State private var alert: Alert?
public var body: some View {
viewBuilder {
@@ -60,17 +57,7 @@ public struct EditProfileContentView: View {
}
}
}
.alert(isPresented: $errorPresented) {
Alert(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("Ok"), action: {
if fatalError {
dismiss()
}
})
)
}
.alertBinding($alert)
.navigationTitle(navigationTitle)
#if os(macOS)
.toolbar {
@@ -115,9 +102,7 @@ public struct EditProfileContentView: View {
do {
try loadContent0()
} catch {
errorMessage = error.localizedDescription
fatalError = true
errorPresented = true
alert = Alert(error, dismiss.callAsFunction)
}
}
@@ -140,8 +125,7 @@ public struct EditProfileContentView: View {
do {
try profile.write(profileContent)
} catch {
errorMessage = error.localizedDescription
errorPresented = true
alert = Alert(error)
return
}
isChanged = false
@@ -10,8 +10,7 @@ public struct EditProfileView: View {
@State private var isLoading = false
@State private var isChanged = false
@State private var errorPresented = false
@State private var errorMessage = ""
@State private var alert: Alert?
public init() {}
@@ -69,6 +68,17 @@ public struct EditProfileView: View {
}
.disabled(isLoading)
}
if #available(iOS 16.0, *) {
ShareLink(item: profile.shareLink) {
Text("Share")
}
} else {
Button("Share") {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
windowScene.keyWindow?.rootViewController?.present(UIActivityViewController(activityItems: [profile.shareLink], applicationActivities: nil), animated: true, completion: nil)
}
}
}
}
#endif
}
@@ -132,13 +142,7 @@ public struct EditProfileView: View {
}
}
#endif
.alert(isPresented: $errorPresented) {
Alert(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("Ok"))
)
}
.alertBinding($alert)
.navigationTitle("Edit Profile")
}
@@ -150,8 +154,7 @@ public struct EditProfileView: View {
try await Task.sleep(nanoseconds: UInt64(100 * Double(NSEC_PER_MSEC)))
try profile.updateRemoteProfile()
} catch {
errorMessage = error.localizedDescription
errorPresented = true
alert = Alert(error)
}
}
@@ -159,8 +162,7 @@ public struct EditProfileView: View {
do {
_ = try ProfileManager.update(profile)
} catch {
errorMessage = error.localizedDescription
errorPresented = true
alert = Alert(error)
return
}
isChanged = false
@@ -1,4 +1,3 @@
import Library
import SwiftUI
@@ -16,8 +15,7 @@ import SwiftUI
@State private var isLoading = true
@State private var profile: Profile!
@State private var errorPresented = false
@State private var errorMessage = ""
@State private var alert: Alert?
public var body: some View {
viewBuilder {
@@ -27,19 +25,11 @@ import SwiftUI
await doReload()
}
}
.alert(isPresented: $errorPresented) {
Alert(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("Ok"), action: {
dismiss()
})
)
}
} else {
EditProfileView().environmentObject(profile!)
}
}
.alertBinding($alert)
.onExitCommand {
dismiss()
}
@@ -47,20 +37,17 @@ import SwiftUI
private func doReload() async {
guard let profileID else {
errorMessage = "Context destroyed"
errorPresented = true
alert = Alert(errorMessage: "Context destroyed")
return
}
do {
profile = try ProfileManager.get(profileID)
} catch {
errorMessage = error.localizedDescription
errorPresented = true
alert = Alert(error)
return
}
if profile == nil {
errorMessage = "Profile deleted"
errorPresented = true
alert = Alert(errorMessage: "Profile deleted")
return
}
isLoading = false
@@ -17,12 +17,21 @@ public struct NewProfileView: View {
@State private var fileURL: URL!
@State private var remotePath = ""
@State private var pickerPresented = false
@State private var errorPresented = false
@State private var errorMessage = ""
@State private var alert: Alert?
public struct ImportRequest: Codable, Hashable {
public let name: String
public let url: String
}
private let callback: (() -> Void)?
public init(_ callback: (() -> Void)? = nil) {
public init(_ importRequest: ImportRequest? = nil, _ callback: (() -> Void)? = nil) {
self.callback = callback
if let importRequest {
_profileName = .init(initialValue: importRequest.name)
_profileType = .init(initialValue: .remote)
_remotePath = .init(initialValue: importRequest.url)
}
}
public var body: some View {
@@ -88,13 +97,7 @@ public struct NewProfileView: View {
}
}
.navigationTitle("New Profile")
.alert(isPresented: $errorPresented) {
Alert(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("Ok"))
)
}
.alertBinding($alert)
.fileImporter(
isPresented: $pickerPresented,
allowedContentTypes: [.json],
@@ -106,8 +109,7 @@ public struct NewProfileView: View {
fileURL = urls[0]
}
} catch {
errorMessage = error.localizedDescription
errorPresented = true
alert = Alert(error)
return
}
}
@@ -118,26 +120,22 @@ public struct NewProfileView: View {
isSaving = false
}
if profileName.isEmpty {
errorMessage = "Missing profile name"
errorPresented = true
alert = Alert(errorMessage: "Missing profile name")
return
}
if remotePath.isEmpty {
if profileType == .icloud {
errorMessage = "Missing path"
errorPresented = true
alert = Alert(errorMessage: "Missing path")
return
} else if profileType == .remote {
errorMessage = "Missing URL"
errorPresented = true
alert = Alert(errorMessage: "Missing URL")
return
}
}
do {
try createProfile0()
} catch {
errorMessage = error.localizedDescription
errorPresented = true
alert = Alert(error)
return
}
await MainActor.run {
@@ -173,13 +171,11 @@ public struct NewProfileView: View {
let profileConfig = profileConfigDirectory.appendingPathComponent("config_\(nextProfileID).json")
if fileImport {
guard let fileURL else {
errorMessage = "Missing file"
errorPresented = true
alert = Alert(errorMessage: "Missing file")
return
}
if !fileURL.startAccessingSecurityScopedResource() {
errorMessage = "Missing access to selected file"
errorPresented = true
alert = Alert(errorMessage: "Missing access to selected file")
return
}
defer {
@@ -1,16 +1,19 @@
import Foundation
import Libbox
import Library
import SwiftUI
public struct ProfileView: View {
public static let notificationName = Notification.Name("\(FilePath.packageName).update-profile")
@Environment(\.importRemoteProfile) private var importRemoteProfile
@State private var importRemoteProfileRequest: NewProfileView.ImportRequest?
@State private var importRemoteProfilePresented = false
@State private var isLoading = true
@State private var isUpdating = false
@State private var errorPresented = false
@State private var errorMessage = ""
@State private var alert: Alert?
@State private var profileList: [Profile] = []
#if os(iOS)
@@ -33,36 +36,51 @@ public struct ProfileView: View {
}
} else {
#if os(iOS)
FormView {
NavigationLink {
NewProfileView {
Task.detached {
doReload()
ZStack {
if let importRemoteProfileRequest {
NavigationLink(
destination: NewProfileView(importRemoteProfileRequest) {
Task.detached {
doReload()
}
},
isActive: $importRemoteProfilePresented,
label: {
EmptyView()
}
}
} label: {
Text("New Profile").foregroundColor(.accentColor)
)
}
.disabled(editMode.isEditing)
if profileList.isEmpty {
Text("Empty Profiles")
} else {
List {
ForEach(profileList, id: \.mustID) { profile in
viewBuilder {
if editMode.isEditing == true {
Text(profile.name)
} else {
NavigationLink {
EditProfileView().environmentObject(profile)
} label: {
FormView {
NavigationLink {
NewProfileView {
Task.detached {
doReload()
}
}
} label: {
Text("New Profile").foregroundColor(.accentColor)
}
.disabled(editMode.isEditing)
if profileList.isEmpty {
Text("Empty Profiles")
} else {
List {
ForEach(profileList, id: \.mustID) { profile in
viewBuilder {
if editMode.isEditing == true {
Text(profile.name)
} else {
NavigationLink {
EditProfileView().environmentObject(profile)
} label: {
Text(profile.name)
}
}
}
}
.onMove(perform: moveProfile)
.onDelete(perform: deleteProfile)
}
.onMove(perform: moveProfile)
.onDelete(perform: deleteProfile)
}
}
}
@@ -92,6 +110,9 @@ public struct ProfileView: View {
}, 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)
@@ -119,8 +140,13 @@ public struct ProfileView: View {
}
.disabled(isUpdating)
.navigationTitle("Profiles")
#if os(macOS)
.onAppear {
.alertBinding($alert, $isLoading)
.onAppear {
if let remoteProfile = importRemoteProfile.wrappedValue {
importRemoteProfile.wrappedValue = nil
createImportRemoteProfileDialog(remoteProfile)
}
#if os(macOS)
if observer == nil {
observer = NotificationCenter.default.addObserver(forName: ProfileView.notificationName, object: nil, queue: .main) { _ in
Task.detached {
@@ -128,40 +154,63 @@ public struct ProfileView: View {
}
}
}
#endif
}
.onChange(of: importRemoteProfile.wrappedValue) { newValue in
if let newValue {
importRemoteProfile.wrappedValue = nil
createImportRemoteProfileDialog(newValue)
}
.onDisappear {
if let observer {
NotificationCenter.default.removeObserver(observer)
}
observer = nil
}
#if os(macOS)
.onDisappear {
if let observer {
NotificationCenter.default.removeObserver(observer)
}
.toolbar {
ToolbarItem {
Button(action: {
openWindow(id: NewProfileView.windowID)
}, label: {
Label("New Profile", systemImage: "plus.square.fill")
})
}
observer = nil
}
.toolbar {
ToolbarItem {
Button(action: {
openWindow(id: NewProfileView.windowID)
}, label: {
Label("New Profile", systemImage: "plus.square.fill")
})
}
}
#elseif os(iOS)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton().disabled(profileList.isEmpty)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton().disabled(profileList.isEmpty)
}
.environment(\.editMode, $editMode)
}
.environment(\.editMode, $editMode)
#endif
}
private func createImportRemoteProfileDialog(_ newValue: LibboxImportRemoteProfile) {
importRemoteProfileRequest = .init(name: newValue.name, url: newValue.url)
alert = Alert(
title: Text("Import Remote Profile"),
message: Text("Are you sure to import remote configuration \(newValue.name)? You will connect to \(newValue.host) to download the configuration."),
primaryButton: .default(Text("Import")) {
#if os(iOS)
importRemoteProfilePresented = true
#elseif os(macOS)
openWindow(id: NewProfileView.windowID, value: importRemoteProfileRequest!)
#endif
},
secondaryButton: .cancel()
)
}
private func deleteSelectedProfiles(_ profileID: [Int64]) {
do {
if try ProfileManager.delete(by: profileID) > 0 {
isLoading = true
}
} catch {
errorMessage = error.localizedDescription
errorPresented = true
alert = Alert(error)
}
}
@@ -178,8 +227,7 @@ public struct ProfileView: View {
do {
profileList = try ProfileManager.list()
} catch {
errorMessage = error.localizedDescription
errorPresented = true
alert = Alert(error)
return
}
}
@@ -189,8 +237,7 @@ public struct ProfileView: View {
do {
_ = try profile.updateRemoteProfile()
} catch {
errorMessage = error.localizedDescription
errorPresented = true
alert = Alert(error)
}
isUpdating = false
}
@@ -200,8 +247,7 @@ public struct ProfileView: View {
do {
_ = try ProfileManager.delete(profile)
} catch {
errorMessage = error.localizedDescription
errorPresented = true
alert = Alert(error)
return
}
isLoading = true
@@ -216,8 +262,7 @@ public struct ProfileView: View {
do {
try ProfileManager.update(profileList)
} catch {
errorMessage = error.localizedDescription
errorPresented = true
alert = Alert(error)
return
}
}
@@ -231,8 +276,7 @@ public struct ProfileView: View {
do {
_ = try ProfileManager.delete(profileToDelete)
} catch {
errorMessage = error.localizedDescription
errorPresented = true
alert = Alert(error)
}
}
}
@@ -24,9 +24,7 @@ public struct SettingView: View {
@State private var version = ""
@State private var dataSize = ""
@State private var taiwanFlagAvailable = false
@State private var errorPresented = false
@State private var errorMessage = ""
@State private var alert: Alert?
public init() {}
@@ -82,13 +80,11 @@ public struct SettingView: View {
do {
if let result = try await SystemExtension.install(forceUpdate: true) {
if result == .willCompleteAfterReboot {
errorMessage = "Need reboot"
errorPresented = true
alert = Alert(errorMessage: "Need reboot")
}
}
} catch {
errorMessage = error.localizedDescription
errorPresented = true
alert = Alert(error)
}
}
}
@@ -134,13 +130,7 @@ public struct SettingView: View {
}
}
.navigationTitle("Settings")
.alert(isPresented: $errorPresented) {
Alert(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("Ok"))
)
}
.alertBinding($alert)
}
#if os(macOS)
@@ -156,8 +146,7 @@ public struct SettingView: View {
try SMAppService.mainApp.unregister()
}
} catch {
errorMessage = error.localizedDescription
errorPresented = true
alert = Alert(error)
}
}
#endif