Refactor Alerts

This commit is contained in:
世界
2025-12-01 18:22:06 +08:00
parent 8e764c56eb
commit 4ddfcdb324
40 changed files with 324 additions and 224 deletions
@@ -1,47 +0,0 @@
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!
}
}
}
@@ -0,0 +1,156 @@
import SwiftUI
public struct AlertState: Equatable {
public var title: String
public var message: String
public var primaryButton: ButtonState?
public var secondaryButton: ButtonState?
public var onDismiss: (() -> Void)?
public struct ButtonState: Equatable {
public var label: String
public var role: ButtonRole?
public var action: (() -> Void)?
public init(label: String, role: ButtonRole? = nil, action: (() -> Void)? = nil) {
self.label = label
self.role = role
self.action = action
}
public static func == (lhs: ButtonState, rhs: ButtonState) -> Bool {
lhs.label == rhs.label && lhs.role == rhs.role
}
public static func `default`(_ label: String, action: (() -> Void)? = nil) -> ButtonState {
ButtonState(label: label, action: action)
}
public static func cancel(_ label: String = String(localized: "Cancel"), action: (() -> Void)? = nil) -> ButtonState {
ButtonState(label: label, role: .cancel, action: action)
}
public static func destructive(_ label: String, action: (() -> Void)? = nil) -> ButtonState {
ButtonState(label: label, role: .destructive, action: action)
}
}
public init(error: Error, dismiss: (() -> Void)? = nil) {
self.init(errorMessage: error.localizedDescription, dismiss: dismiss)
}
public init(errorMessage: String, dismiss: (() -> Void)? = nil) {
title = String(localized: "Error")
message = errorMessage
primaryButton = .default(String(localized: "Ok"), action: dismiss)
secondaryButton = nil
onDismiss = nil
}
public init(title: String, message: String, dismissButton: ButtonState? = nil) {
self.title = title
self.message = message
primaryButton = dismissButton ?? .default(String(localized: "Ok"))
secondaryButton = nil
onDismiss = nil
}
public init(title: String, message: String, primaryButton: ButtonState, secondaryButton: ButtonState) {
self.title = title
self.message = message
self.primaryButton = primaryButton
self.secondaryButton = secondaryButton
onDismiss = nil
}
public init(title: String, message: String, primaryButton: ButtonState, secondaryButton: ButtonState, onDismiss: @escaping () -> Void) {
self.title = title
self.message = message
self.primaryButton = primaryButton
self.secondaryButton = secondaryButton
self.onDismiss = onDismiss
}
public static func == (lhs: AlertState, rhs: AlertState) -> Bool {
lhs.title == rhs.title && lhs.message == rhs.message &&
lhs.primaryButton == rhs.primaryButton && lhs.secondaryButton == rhs.secondaryButton
}
}
public extension View {
@ViewBuilder
func alert(_ binding: Binding<AlertState?>) -> some View {
alert(
binding.wrappedValue?.title ?? "",
isPresented: Binding(
get: { binding.wrappedValue != nil },
set: { newValue, _ in
if !newValue {
binding.wrappedValue?.onDismiss?()
binding.wrappedValue = nil
}
}
),
presenting: binding.wrappedValue
) { alertState in
if let secondary = alertState.secondaryButton {
Button(role: alertState.primaryButton?.role) {
alertState.primaryButton?.action?()
} label: {
Text(alertState.primaryButton?.label ?? "Ok")
}
Button(role: secondary.role) {
secondary.action?()
} label: {
Text(secondary.label)
}
} else if let primary = alertState.primaryButton {
Button(role: primary.role) {
primary.action?()
} label: {
Text(primary.label)
}
}
} message: { alertState in
Text(alertState.message)
}
}
@ViewBuilder
func alert(_ binding: Binding<AlertState?>, isLoading: Binding<Bool>) -> some View {
alert(
binding.wrappedValue?.title ?? "",
isPresented: Binding(
get: { binding.wrappedValue != nil },
set: { newValue, _ in
if !newValue, !isLoading.wrappedValue {
binding.wrappedValue?.onDismiss?()
binding.wrappedValue = nil
}
}
),
presenting: binding.wrappedValue
) { alertState in
if let secondary = alertState.secondaryButton {
Button(role: alertState.primaryButton?.role) {
alertState.primaryButton?.action?()
} label: {
Text(alertState.primaryButton?.label ?? "Ok")
}
Button(role: secondary.role) {
secondary.action?()
} label: {
Text(secondary.label)
}
} else if let primary = alertState.primaryButton {
Button(role: primary.role) {
primary.action?()
} label: {
Text(primary.label)
}
}
} message: { alertState in
Text(alertState.message)
}
}
}
@@ -2,20 +2,20 @@ import SwiftUI
@MainActor @MainActor
open class BaseViewModel: ObservableObject { open class BaseViewModel: ObservableObject {
@Published public var alert: Alert? @Published public var alert: AlertState?
@Published public var isLoading = false @Published public var isLoading = false
public init() {} public init() {}
public func showError(_ error: Error) { public func showError(_ error: Error) {
alert = Alert(error) alert = AlertState(error: error)
} }
public func execute(_ operation: () async throws -> Void) async { public func execute(_ operation: () async throws -> Void) async {
do { do {
try await operation() try await operation()
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
@@ -24,7 +24,7 @@ open class BaseViewModel: ObservableObject {
try await operation() try await operation()
} catch { } catch {
await MainActor.run { await MainActor.run {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
} }
@@ -9,11 +9,11 @@ import SwiftUI
@MainActor @MainActor
public struct ProfileShareButton<Label>: View where Label: View { public struct ProfileShareButton<Label>: View where Label: View {
private let alert: Binding<Alert?> private let alert: Binding<AlertState?>
private let profile: Profile private let profile: Profile
private let label: () -> Label private let label: () -> Label
public init(_ alert: Binding<Alert?>, _ profile: Profile, label: @escaping () -> Label) { public init(_ alert: Binding<AlertState?>, _ profile: Profile, label: @escaping () -> Label) {
self.alert = alert self.alert = alert
self.profile = profile self.profile = profile
self.label = label self.label = label
@@ -44,13 +44,13 @@ public struct ShareButtonCompat<Label>: View where Label: View {
private let label: () -> Label private let label: () -> Label
private let itemURL: () throws -> URL private let itemURL: () throws -> URL
@Binding private var alert: Alert? @Binding private var alert: AlertState?
#if os(macOS) #if os(macOS)
@State private var sharePresented = false @State private var sharePresented = false
#endif #endif
public init(_ alert: Binding<Alert?>, @ViewBuilder label: @escaping () -> Label, itemURL: @escaping () throws -> URL) { public init(_ alert: Binding<AlertState?>, @ViewBuilder label: @escaping () -> Label, itemURL: @escaping () throws -> URL) {
_alert = alert _alert = alert
self.label = label self.label = label
self.itemURL = itemURL self.itemURL = itemURL
@@ -83,7 +83,7 @@ public struct ShareButtonCompat<Label>: View where Label: View {
} }
} catch { } catch {
await MainActor.run { await MainActor.run {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
} }
@@ -109,10 +109,10 @@ public struct ShareButtonCompat<Label>: View where Label: View {
#if os(macOS) #if os(macOS)
private struct SharingServicePicker: NSViewRepresentable { private struct SharingServicePicker: NSViewRepresentable {
@Binding private var isPresented: Bool @Binding private var isPresented: Bool
@Binding private var alert: Alert? @Binding private var alert: AlertState?
private let item: () throws -> URL private let item: () throws -> URL
init(_ isPresented: Binding<Bool>, _ alert: Binding<Alert?>, _ item: @escaping () throws -> URL) { init(_ isPresented: Binding<Bool>, _ alert: Binding<AlertState?>, _ item: @escaping () throws -> URL) {
_isPresented = isPresented _isPresented = isPresented
_alert = alert _alert = alert
self.item = item self.item = item
@@ -132,7 +132,7 @@ public struct ShareButtonCompat<Label>: View where Label: View {
picker.show(relativeTo: .zero, of: nsView, preferredEdge: .minY) picker.show(relativeTo: .zero, of: nsView, preferredEdge: .minY)
} }
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
} }
@@ -54,7 +54,7 @@ public struct ConnectionListView: View {
#if os(macOS) #if os(macOS)
.searchable(text: $viewModel.searchText) .searchable(text: $viewModel.searchText)
#endif #endif
.alertBinding($viewModel.alert) .alert($viewModel.alert)
.onAppear { .onAppear {
viewModel.connect() viewModel.connect()
} }
@@ -28,7 +28,7 @@ public class ConnectionListViewModel: BaseViewModel {
private var saveStateFilterTask: Task<Void, Never>? private var saveStateFilterTask: Task<Void, Never>?
private var saveSortTask: Task<Void, Never>? private var saveSortTask: Task<Void, Never>?
public override init() { override public init() {
connectionStateFilter = .active connectionStateFilter = .active
connectionSort = .byDate connectionSort = .byDate
super.init() super.init()
@@ -70,7 +70,7 @@ public class ConnectionListViewModel: BaseViewModel {
do { do {
try LibboxNewStandaloneCommandClient()!.closeConnections() try LibboxNewStandaloneCommandClient()!.closeConnections()
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
@@ -18,7 +18,7 @@ public struct ConnectionView: View {
LibboxFormatDuration(Int64((closedAt.timeIntervalSince1970 - createdAt.timeIntervalSince1970) * 1000)) LibboxFormatDuration(Int64((closedAt.timeIntervalSince1970 - createdAt.timeIntervalSince1970) * 1000))
} }
@State private var alert: Alert? @State private var alert: AlertState?
public var body: some View { public var body: some View {
FormNavigationLink { FormNavigationLink {
@@ -86,7 +86,7 @@ public struct ConnectionView: View {
#if !os(tvOS) #if !os(tvOS)
.buttonStyle(.borderless) .buttonStyle(.borderless)
#endif #endif
.alertBinding($alert) .alert($alert)
.contextMenu { .contextMenu {
if connection.closedAt == nil { if connection.closedAt == nil {
Button("Close", role: .destructive) { Button("Close", role: .destructive) {
@@ -114,7 +114,7 @@ public struct ConnectionView: View {
try await LibboxNewStandaloneCommandClient()!.closeConnection(connection.id) try await LibboxNewStandaloneCommandClient()!.closeConnection(connection.id)
} catch { } catch {
await MainActor.run { await MainActor.run {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
} }
@@ -34,7 +34,7 @@ import SwiftUI
} }
} else { } else {
content.onAppear { content.onAppear {
guard !ApplicationLibrary.inPreview else { guard !ApplicationLibrary.inPreview, profile.status.isConnected else {
return return
} }
Task { Task {
@@ -117,7 +117,7 @@ import SwiftUI
updateButtonVisibility() updateButtonVisibility()
} }
#endif #endif
.alertBinding($coordinator.alert) .alert($coordinator.alert)
} }
@ViewBuilder private var overviewPage: some View { @ViewBuilder private var overviewPage: some View {
@@ -5,7 +5,7 @@ import SwiftUI
public struct ClashModeCard: View { public struct ClashModeCard: View {
@EnvironmentObject private var commandClient: CommandClient @EnvironmentObject private var commandClient: CommandClient
@State private var clashMode: String = "" @State private var clashMode: String = ""
@State private var alert: Alert? @State private var alert: AlertState?
public init() {} public init() {}
@@ -32,7 +32,7 @@ public struct ClashModeCard: View {
.onChangeCompat(of: commandClient.clashMode) { newValue in .onChangeCompat(of: commandClient.clashMode) { newValue in
clashMode = newValue clashMode = newValue
} }
.alertBinding($alert) .alert($alert)
} }
} }
@@ -45,7 +45,7 @@ public struct ClashModeCard: View {
try LibboxNewStandaloneCommandClient()!.setClashMode(newMode) try LibboxNewStandaloneCommandClient()!.setClashMode(newMode)
} catch { } catch {
await MainActor.run { await MainActor.run {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
} }
@@ -51,7 +51,7 @@ public struct ProfileCard: View {
} }
} }
#endif #endif
.alertBinding($viewModel.alert) .alert($viewModel.alert)
} }
private var headerView: some View { private var headerView: some View {
@@ -254,7 +254,7 @@ extension ProfileCard {
@Published var showManageProfiles = false @Published var showManageProfiles = false
@Published var showQRCode = false @Published var showQRCode = false
@Published var isUpdating = false @Published var isUpdating = false
@Published var alert: Alert? @Published var alert: AlertState?
@Published var profileToEdit: Profile? @Published var profileToEdit: Profile?
func updateProfile(_ profile: Profile, environments: ExtensionEnvironments) async { func updateProfile(_ profile: Profile, environments: ExtensionEnvironments) async {
@@ -264,9 +264,9 @@ extension ProfileCard {
try await profile.updateRemoteProfile() try await profile.updateRemoteProfile()
environments.profileUpdate.send() environments.profileUpdate.send()
} catch { } catch {
alert = Alert( alert = AlertState(
title: Text("Update Failed"), title: String(localized: "Update Failed"),
message: Text(error.localizedDescription) message: error.localizedDescription
) )
} }
} }
@@ -368,7 +368,7 @@ extension ProfileCard {
} }
} }
.disabled(viewModel.isUpdating) .disabled(viewModel.isUpdating)
.alertBinding($viewModel.alert, $viewModel.isLoading) .alert($viewModel.alert, isLoading: $viewModel.isLoading)
.onReceive(environments.profileUpdate) { _ in .onReceive(environments.profileUpdate) { _ in
Task { Task {
await viewModel.doReload() await viewModel.doReload()
@@ -9,7 +9,7 @@ public struct ExtensionStatusView: View {
@EnvironmentObject private var commandClient: CommandClient @EnvironmentObject private var commandClient: CommandClient
@State private var columnCount: Int = 4 @State private var columnCount: Int = 4
@State private var alert: Alert? @State private var alert: AlertState?
private let infoFont = Font.system(.caption, design: .monospaced) private let infoFont = Font.system(.caption, design: .monospaced)
@@ -91,7 +91,7 @@ public struct ExtensionStatusView: View {
.frame(alignment: .topLeading) .frame(alignment: .topLeading)
.padding([.top, .leading, .trailing]) .padding([.top, .leading, .trailing])
} }
.alertBinding($alert) .alert($alert)
} }
private func updateColumnCount(_ width: Double) { private func updateColumnCount(_ width: Double) {
@@ -3,7 +3,7 @@ import SwiftUI
@MainActor @MainActor
public struct InstallProfileButton: View { public struct InstallProfileButton: View {
@State private var alert: Alert? @State private var alert: AlertState?
private let callback: () async -> Void private let callback: () async -> Void
public init(_ callback: @escaping (() async -> Void)) { public init(_ callback: @escaping (() async -> Void)) {
@@ -18,7 +18,7 @@ public struct InstallProfileButton: View {
} label: { } label: {
Label("Install Network Extension", systemImage: "lock.doc.fill") Label("Install Network Extension", systemImage: "lock.doc.fill")
} }
.alertBinding($alert) .alert($alert)
} }
private func installProfile() async { private func installProfile() async {
@@ -26,7 +26,7 @@ public struct InstallProfileButton: View {
try await ExtensionProfile.install() try await ExtensionProfile.install()
await callback() await callback()
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
} }
@@ -5,7 +5,7 @@
@MainActor @MainActor
public struct InstallSystemExtensionButton: View { public struct InstallSystemExtensionButton: View {
@State private var alert: Alert? @State private var alert: AlertState?
private let callback: () async -> Void private let callback: () async -> Void
public init(_ callback: @escaping () async -> Void) { public init(_ callback: @escaping () async -> Void) {
self.callback = callback self.callback = callback
@@ -19,19 +19,19 @@
} label: { } label: {
Label("Install System Extension", systemImage: "lock.doc.fill") Label("Install System Extension", systemImage: "lock.doc.fill")
} }
.alertBinding($alert) .alert($alert)
} }
private func installSystemExtension() async { private func installSystemExtension() async {
do { do {
if let result = try await SystemExtension.install() { if let result = try await SystemExtension.install() {
if result == .willCompleteAfterReboot { if result == .willCompleteAfterReboot {
alert = Alert(errorMessage: String(localized: "Need Reboot")) alert = AlertState(errorMessage: String(localized: "Need Reboot"))
} }
} }
await callback() await callback()
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
} }
@@ -39,7 +39,7 @@ public struct StartStopButton: View {
private struct ToggleConnectionButton: View { private struct ToggleConnectionButton: View {
@EnvironmentObject private var environments: ExtensionEnvironments @EnvironmentObject private var environments: ExtensionEnvironments
@EnvironmentObject private var profile: ExtensionProfile @EnvironmentObject private var profile: ExtensionProfile
@State private var alert: Alert? @State private var alert: AlertState?
@State private var currentTime = Date() @State private var currentTime = Date()
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
@@ -105,7 +105,7 @@ public struct StartStopButton: View {
.modifier(PrimaryTintModifier()) .modifier(PrimaryTintModifier())
#endif #endif
.disabled(!profile.status.isEnabled) .disabled(!profile.status.isEnabled)
.alertBinding($alert) .alert($alert)
.onReceive(timer) { _ in .onReceive(timer) { _ in
currentTime = Date() currentTime = Date()
} }
@@ -146,7 +146,7 @@ public struct StartStopButton: View {
} }
} catch { } catch {
await MainActor.run { await MainActor.run {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
} }
@@ -48,15 +48,15 @@ public struct DashboardView: View {
private func handleImportProfile() { private func handleImportProfile() {
if let profile = importProfile.wrappedValue { if let profile = importProfile.wrappedValue {
importProfile.wrappedValue = nil importProfile.wrappedValue = nil
coordinator.alert = Alert( coordinator.alert = AlertState(
title: Text("Import Profile"), title: String(localized: "Import Profile"),
message: Text("Are you sure to import profile \(profile.name)?"), message: String(localized: "Are you sure to import profile \(profile.name)?"),
primaryButton: .default(Text("Import")) { primaryButton: .default(String(localized: "Import")) {
Task { Task {
do { do {
try await profile.importProfile() try await profile.importProfile()
} catch { } catch {
coordinator.alert = Alert(error) coordinator.alert = AlertState(error: error)
return return
} }
environments.profileUpdate.send() environments.profileUpdate.send()
@@ -70,10 +70,10 @@ public struct DashboardView: View {
private func handleImportRemoteProfile() { private func handleImportRemoteProfile() {
if let remoteProfile = importRemoteProfile.wrappedValue { if let remoteProfile = importRemoteProfile.wrappedValue {
importRemoteProfile.wrappedValue = nil importRemoteProfile.wrappedValue = nil
coordinator.alert = Alert( coordinator.alert = AlertState(
title: Text("Import Remote Profile"), title: String(localized: "Import Remote Profile"),
message: Text("Are you sure to import remote profile \(remoteProfile.name)? You will connect to \(remoteProfile.host) to download the configuration."), message: String(localized: "Are you sure to import remote profile \(remoteProfile.name)? You will connect to \(remoteProfile.host) to download the configuration."),
primaryButton: .default(Text("Import")) { primaryButton: .default(String(localized: "Import")) {
importRemoteProfileRequest = .init(name: remoteProfile.name, url: remoteProfile.url) importRemoteProfileRequest = .init(name: remoteProfile.name, url: remoteProfile.url)
}, },
secondaryButton: .cancel() secondaryButton: .cancel()
@@ -117,7 +117,7 @@ public struct DashboardView: View {
} else if let profile = environments.extensionProfile { } else if let profile = environments.extensionProfile {
activeDashboardView activeDashboardView
.environmentObject(profile) .environmentObject(profile)
.alertBinding($coordinator.alert) .alert($coordinator.alert)
.onChangeCompat(of: profile.status) { status in .onChangeCompat(of: profile.status) { status in
coordinator.handleStatusChange(status, profile: profile) coordinator.handleStatusChange(status, profile: profile)
} }
@@ -20,7 +20,7 @@ public final class DashboardViewModel: BaseViewModel {
public var onEmptyProfilesChange: ((Bool) -> Void)? public var onEmptyProfilesChange: ((Bool) -> Void)?
private var openURL: ((URL) -> Void)? private var openURL: ((URL) -> Void)?
public override init() { override public init() {
super.init() super.init()
isLoading = true isLoading = true
} }
@@ -65,7 +65,7 @@ public final class DashboardViewModel: BaseViewModel {
await SharedPreferences.selectedProfileID.set(selectedProfileID) await SharedPreferences.selectedProfileID.set(selectedProfileID)
} }
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
return return
} }
} }
@@ -78,7 +78,7 @@ public final class DashboardViewModel: BaseViewModel {
systemProxyAvailable = status.available systemProxyAvailable = status.available
systemProxyEnabled = status.enabled systemProxyEnabled = status.enabled
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
@@ -114,7 +114,7 @@ public final class DashboardViewModel: BaseViewModel {
} }
} catch { } catch {
await MainActor.run { await MainActor.run {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
} }
@@ -123,34 +123,29 @@ public final class DashboardViewModel: BaseViewModel {
guard reports.hasNext() else { return } guard reports.hasNext() else { return }
let report = reports.next()! let report = reports.next()!
let continueChain: () -> Void = { [weak self] in
_ = Task.detached {
try? await Task.sleep(nanoseconds: 300 * NSEC_PER_MSEC)
await self?.loopShowDeprecateNotes(reports)
}
}
if report.migrationLink.isEmpty { if report.migrationLink.isEmpty {
alert = Alert( alert = AlertState(
title: Text("Deprecated Warning"), title: String(localized: "Deprecated Warning"),
message: Text(report.message()), message: report.message(),
dismissButton: .cancel(Text("Ok")) { dismissButton: .cancel(String(localized: "Ok"))
Task.detached { [weak self] in
try await Task.sleep(nanoseconds: 300 * NSEC_PER_MSEC)
await self?.loopShowDeprecateNotes(reports)
}
}
) )
alert?.onDismiss = continueChain
} else { } else {
alert = Alert( alert = AlertState(
title: Text("Deprecated Warning"), title: String(localized: "Deprecated Warning"),
message: Text(report.message()), message: report.message(),
primaryButton: .default(Text("Documentation")) { primaryButton: .default(String(localized: "Documentation")) {
self.openURL?(URL(string: report.migrationLink)!) self.openURL?(URL(string: report.migrationLink)!)
Task.detached { [weak self] in
try await Task.sleep(nanoseconds: 300 * NSEC_PER_MSEC)
await self?.loopShowDeprecateNotes(reports)
}
}, },
secondaryButton: .cancel(Text("Ok")) { secondaryButton: .cancel(String(localized: "Ok")),
Task.detached { [weak self] in onDismiss: continueChain
try await Task.sleep(nanoseconds: 300 * NSEC_PER_MSEC)
await self?.loopShowDeprecateNotes(reports)
}
}
) )
} }
} }
@@ -165,10 +160,10 @@ public final class DashboardViewModel: BaseViewModel {
#if os(macOS) #if os(macOS)
if myError.domain == "Library.FullDiskAccessPermissionRequired" { if myError.domain == "Library.FullDiskAccessPermissionRequired" {
await MainActor.run { await MainActor.run {
alert = Alert( alert = AlertState(
title: Text("Full Disk Access permission is required"), title: String(localized: "Full Disk Access permission is required"),
message: Text("Please grant the permission for **SFMExtension**, then we can continue."), message: String(localized: "Please grant the permission for **SFMExtension**, then we can continue."),
primaryButton: .default(Text("Authorize"), action: openFDASettings), primaryButton: .default(String(localized: "Authorize"), action: openFDASettings),
secondaryButton: .cancel() secondaryButton: .cancel()
) )
} }
@@ -176,7 +171,7 @@ public final class DashboardViewModel: BaseViewModel {
} }
#endif #endif
await MainActor.run { await MainActor.run {
alert = Alert(title: Text("Service Error"), message: Text(myError.localizedDescription)) alert = AlertState(title: String(localized: "Service Error"), message: myError.localizedDescription)
} }
} }
} }
@@ -44,7 +44,7 @@ public struct OverviewView: View {
.onChangeCompat(of: cardConfigurationVersion) { _ in .onChangeCompat(of: cardConfigurationVersion) { _ in
Task { await configuration.reload() } Task { await configuration.reload() }
} }
.alertBinding($coordinator.alert) .alert($coordinator.alert)
.disabled(!ApplicationLibrary.inPreview && (!profile.status.isSwitchable || coordinator.reasserting)) .disabled(!ApplicationLibrary.inPreview && (!profile.status.isSwitchable || coordinator.reasserting))
} }
@@ -15,7 +15,7 @@ public final class OverviewViewModel: BaseViewModel {
do { do {
try await serviceReload() try await serviceReload()
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
reasserting = false reasserting = false
@@ -46,7 +46,7 @@ public final class OverviewViewModel: BaseViewModel {
await MainActor.run { reasserting = false } await MainActor.run { reasserting = false }
} }
} catch { } catch {
await MainActor.run { alert = Alert(error) } await MainActor.run { alert = AlertState(error: error) }
} }
} }
} }
@@ -23,7 +23,7 @@ public struct GroupListView: View {
} }
} }
.environmentObject(viewModel) .environmentObject(viewModel)
.alertBinding($viewModel.alert) .alert($viewModel.alert)
.onAppear { .onAppear {
viewModel.connect() viewModel.connect()
} }
@@ -8,7 +8,7 @@ public class GroupListViewModel: BaseViewModel {
private var pendingSelections: [String: String] = [:] private var pendingSelections: [String: String] = [:]
public override init() { override public init() {
super.init() super.init()
isLoading = true isLoading = true
} }
@@ -89,7 +89,7 @@ public class GroupListViewModel: BaseViewModel {
try await LibboxNewStandaloneCommandClient()!.selectOutbound(groupTag, outboundTag: outboundTag) try await LibboxNewStandaloneCommandClient()!.selectOutbound(groupTag, outboundTag: outboundTag)
} catch { } catch {
await MainActor.run { await MainActor.run {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
} }
@@ -108,7 +108,7 @@ public class GroupListViewModel: BaseViewModel {
try await LibboxNewStandaloneCommandClient()!.setGroupExpand(tag, isExpand: isExpand) try await LibboxNewStandaloneCommandClient()!.setGroupExpand(tag, isExpand: isExpand)
} catch { } catch {
await MainActor.run { await MainActor.run {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
} }
@@ -124,7 +124,7 @@ public class GroupListViewModel: BaseViewModel {
try await LibboxNewStandaloneCommandClient()!.urlTest(tag) try await LibboxNewStandaloneCommandClient()!.urlTest(tag)
} catch { } catch {
await MainActor.run { await MainActor.run {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
} }
+4 -4
View File
@@ -49,7 +49,7 @@ private struct LogViewContent: View {
} }
} }
} }
.alertBinding($viewModel.alert) .alert($viewModel.alert)
.background( .background(
LogExportView( LogExportView(
showFileExporter: $viewModel.showFileExporter, showFileExporter: $viewModel.showFileExporter,
@@ -242,7 +242,7 @@ private struct LogViewContent: View {
private struct LogExportView: View { private struct LogExportView: View {
@Binding var showFileExporter: Bool @Binding var showFileExporter: Bool
@Binding var logFileURL: URL? @Binding var logFileURL: URL?
@Binding var alert: Alert? @Binding var alert: AlertState?
@State private var showShareSheet = false @State private var showShareSheet = false
let cleanup: () -> Void let cleanup: () -> Void
@@ -257,7 +257,7 @@ private struct LogViewContent: View {
cleanup() cleanup()
logFileURL = nil logFileURL = nil
if case let .failure(error) = result { if case let .failure(error) = result {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
.sheet(isPresented: $showShareSheet) { .sheet(isPresented: $showShareSheet) {
@@ -320,7 +320,7 @@ private struct LogViewContent: View {
#elseif os(macOS) #elseif os(macOS)
private struct ShareView: NSViewRepresentable { private struct ShareView: NSViewRepresentable {
let items: [Any] let items: [Any]
@Binding var alert: Alert? @Binding var alert: AlertState?
func makeNSView(context _: Context) -> NSView { func makeNSView(context _: Context) -> NSView {
let view = NSView() let view = NSView()
@@ -16,7 +16,7 @@ public class LogViewModel: ObservableObject {
@Published public var searchText = "" @Published public var searchText = ""
@Published public var isSearching = false @Published public var isSearching = false
@Published public var filteredLogs: [LogEntry] = [] @Published public var filteredLogs: [LogEntry] = []
@Published public var alert: Alert? @Published public var alert: AlertState?
@Published public var showFileExporter = false @Published public var showFileExporter = false
@Published public var logFileURL: URL? @Published public var logFileURL: URL?
@@ -146,7 +146,7 @@ public class LogViewModel: ObservableObject {
try text.write(to: fileURL, atomically: true, encoding: .utf8) try text.write(to: fileURL, atomically: true, encoding: .utf8)
logFileURL = fileURL logFileURL = fileURL
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
#endif #endif
@@ -36,7 +36,7 @@
} }
} }
} }
.alertBinding($viewModel.alert) .alert($viewModel.alert)
.navigationTitle(navigationTitle) .navigationTitle(navigationTitle)
#if os(macOS) #if os(macOS)
.toolbar { .toolbar {
@@ -33,7 +33,7 @@
do { do {
try await loadContentBackground() try await loadContentBackground()
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
} }
isLoading = false isLoading = false
} }
@@ -59,7 +59,7 @@
do { do {
try await saveContentBackground(profile) try await saveContentBackground(profile)
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
return return
} }
isChanged = false isChanged = false
@@ -95,7 +95,7 @@ public struct EditProfileView: View {
viewModel.markAsChanged() viewModel.markAsChanged()
} }
.disabled(viewModel.isLoading) .disabled(viewModel.isLoading)
.alertBinding($viewModel.alert) .alert($viewModel.alert)
} }
#else #else
private var iOSBody: some View { private var iOSBody: some View {
@@ -122,7 +122,7 @@ public struct EditProfileView: View {
} }
} }
#endif #endif
.alertBinding($viewModel.alert) .alert($viewModel.alert)
.navigationTitle("Edit Profile") .navigationTitle("Edit Profile")
} }
#endif #endif
@@ -21,7 +21,7 @@ public final class EditProfileViewModel: BaseViewModel {
try await profile.updateRemoteProfile() try await profile.updateRemoteProfile()
environments.profileUpdate.send() environments.profileUpdate.send()
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
@@ -29,7 +29,7 @@ public final class EditProfileViewModel: BaseViewModel {
do { do {
try await ProfileManager.delete(profile) try await ProfileManager.delete(profile)
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
return return
} }
environments.profileUpdate.send() environments.profileUpdate.send()
@@ -46,7 +46,7 @@ public final class EditProfileViewModel: BaseViewModel {
#endif #endif
try await profile.onProfileUpdated() try await profile.onProfileUpdated()
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
return return
} }
isChanged = false isChanged = false
@@ -59,7 +59,7 @@
} }
} }
.focusSection() .focusSection()
.alertBinding($viewModel.alert) .alert($viewModel.alert)
.navigationTitle("Import Profile") .navigationTitle("Import Profile")
} }
} }
@@ -37,7 +37,7 @@
case let .failed(error): case let .failed(error):
DispatchQueue.main.async { [self] in DispatchQueue.main.async { [self] in
reset() reset()
alert = Alert(error) alert = AlertState(error: error)
} }
default: break default: break
} }
@@ -46,7 +46,7 @@
do { do {
try await loopMessages(environments: environments, dismiss: dismiss) try await loopMessages(environments: environments, dismiss: dismiss)
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
reset() reset()
} }
} }
@@ -115,7 +115,7 @@
try socket.write(request.encode()) try socket.write(request.encode())
isImporting = true isImporting = true
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
reset() reset()
} }
} }
@@ -158,7 +158,7 @@ public struct NewProfileView: View {
} }
} }
.disabled(viewModel.isSaving) .disabled(viewModel.isSaving)
.alertBinding($viewModel.alert) .alert($viewModel.alert)
.fileImporter( .fileImporter(
isPresented: $viewModel.pickerPresented, isPresented: $viewModel.pickerPresented,
allowedContentTypes: [.json], allowedContentTypes: [.json],
@@ -170,7 +170,7 @@ public struct NewProfileView: View {
viewModel.fileURL = urls[0] viewModel.fileURL = urls[0]
} }
} catch { } catch {
viewModel.alert = Alert(error) viewModel.alert = AlertState(error: error)
return return
} }
} }
@@ -180,7 +180,7 @@ public struct NewProfileView: View {
formContent formContent
.navigationTitle("New Profile") .navigationTitle("New Profile")
.disabled(viewModel.isSaving) .disabled(viewModel.isSaving)
.alertBinding($viewModel.alert) .alert($viewModel.alert)
#if os(iOS) #if os(iOS)
.fileImporter( .fileImporter(
isPresented: $viewModel.pickerPresented, isPresented: $viewModel.pickerPresented,
@@ -193,7 +193,7 @@ public struct NewProfileView: View {
viewModel.fileURL = urls[0] viewModel.fileURL = urls[0]
} }
} catch { } catch {
viewModel.alert = Alert(error) viewModel.alert = AlertState(error: error)
return return
} }
} }
@@ -45,17 +45,17 @@ public final class NewProfileViewModel: BaseViewModel {
defer { isSaving = false } defer { isSaving = false }
guard !profileName.isEmpty else { guard !profileName.isEmpty else {
alert = Alert(errorMessage: String(localized: "Missing profile name")) alert = AlertState(errorMessage: String(localized: "Missing profile name"))
return return
} }
if profileType == .icloud, remotePath.isEmpty { if profileType == .icloud, remotePath.isEmpty {
alert = Alert(errorMessage: String(localized: "Missing path")) alert = AlertState(errorMessage: String(localized: "Missing path"))
return return
} }
if profileType == .remote, remotePath.isEmpty { if profileType == .remote, remotePath.isEmpty {
alert = Alert(errorMessage: String(localized: "Missing URL")) alert = AlertState(errorMessage: String(localized: "Missing URL"))
return return
} }
@@ -63,7 +63,7 @@ public final class NewProfileViewModel: BaseViewModel {
do { do {
createdProfile = try await createProfileBackground() createdProfile = try await createProfileBackground()
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
return return
} }
@@ -88,7 +88,7 @@ public struct ProfileView: View {
} }
} }
.disabled(viewModel.isUpdating) .disabled(viewModel.isUpdating)
.alertBinding($viewModel.alert, $viewModel.isLoading) .alert($viewModel.alert, isLoading: $viewModel.isLoading)
.onAppear { .onAppear {
if let profile = importProfile.wrappedValue { if let profile = importProfile.wrappedValue {
importProfile.wrappedValue = nil importProfile.wrappedValue = nil
@@ -16,7 +16,7 @@ public class ProfileViewModel: BaseViewModel {
private weak var environments: ExtensionEnvironments? private weak var environments: ExtensionEnvironments?
public override init() { override public init() {
super.init() super.init()
isLoading = true isLoading = true
} }
@@ -26,15 +26,15 @@ public class ProfileViewModel: BaseViewModel {
} }
public func createImportProfileDialog(_ profile: LibboxProfileContent) { public func createImportProfileDialog(_ profile: LibboxProfileContent) {
alert = Alert( alert = AlertState(
title: Text("Import Profile"), title: String(localized: "Import Profile"),
message: Text("Are you sure to import profile \(profile.name)?"), message: String(localized: "Are you sure to import profile \(profile.name)?"),
primaryButton: .default(Text("Import")) { primaryButton: .default(String(localized: "Import")) {
Task { Task {
do { do {
try await profile.importProfile() try await profile.importProfile()
} catch { } catch {
self.alert = Alert(error) self.alert = AlertState(error: error)
return return
} }
await self.doReload() await self.doReload()
@@ -47,10 +47,10 @@ public class ProfileViewModel: BaseViewModel {
public func createImportRemoteProfileDialog(_ newValue: LibboxImportRemoteProfile) { public func createImportRemoteProfileDialog(_ newValue: LibboxImportRemoteProfile) {
importRemoteProfileRequest = .init(name: newValue.name, url: newValue.url) importRemoteProfileRequest = .init(name: newValue.name, url: newValue.url)
alert = Alert( alert = AlertState(
title: Text("Import Remote Profile"), title: String(localized: "Import Remote Profile"),
message: Text("Are you sure to import remote profile \(newValue.name)? You will connect to \(newValue.host) to download the configuration."), message: String(localized: "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(String(localized: "Import")) {
self.importRemoteProfilePresented = true self.importRemoteProfilePresented = true
}, },
secondaryButton: .cancel() secondaryButton: .cancel()
@@ -70,7 +70,7 @@ public class ProfileViewModel: BaseViewModel {
do { do {
profileList = try await ProfileManager.list().map { ProfilePreview($0) } profileList = try await ProfileManager.list().map { ProfilePreview($0) }
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
return return
} }
} }
@@ -87,7 +87,7 @@ public class ProfileViewModel: BaseViewModel {
_ = try await profile.updateRemoteProfile() _ = try await profile.updateRemoteProfile()
} catch { } catch {
await MainActor.run { await MainActor.run {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
} }
@@ -98,7 +98,7 @@ public class ProfileViewModel: BaseViewModel {
environments?.profileUpdate.send() environments?.profileUpdate.send()
environments?.emptyProfiles = profileList.isEmpty environments?.emptyProfiles = profileList.isEmpty
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
@@ -113,7 +113,7 @@ public class ProfileViewModel: BaseViewModel {
try await ProfileManager.update(profileList.map(\.origin)) try await ProfileManager.update(profileList.map(\.origin))
environments?.profileUpdate.send() environments?.profileUpdate.send()
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
} }
@@ -129,7 +129,7 @@ public class ProfileViewModel: BaseViewModel {
environments?.emptyProfiles = profileList.isEmpty environments?.emptyProfiles = profileList.isEmpty
environments?.profileUpdate.send() environments?.profileUpdate.send()
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
} }
@@ -13,7 +13,7 @@ public struct AppView: View {
@Environment(\.showMenuBarExtra) private var showMenuBarExtra @Environment(\.showMenuBarExtra) private var showMenuBarExtra
@State private var menuBarExtraInBackground = false @State private var menuBarExtraInBackground = false
@State private var alert: Alert? @State private var alert: AlertState?
public init() {} public init() {}
public var body: some View { public var body: some View {
@@ -72,7 +72,7 @@ public struct AppView: View {
} }
} }
} }
.alertBinding($alert) .alert($alert)
.navigationTitle("App") .navigationTitle("App")
#if os(iOS) #if os(iOS)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
@@ -101,7 +101,7 @@ public struct AppView: View {
try SMAppService.mainApp.unregister() try SMAppService.mainApp.unregister()
} }
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
@@ -110,21 +110,19 @@ public struct AppView: View {
if let result = try await SystemExtension.install(forceUpdate: true) { if let result = try await SystemExtension.install(forceUpdate: true) {
switch result { switch result {
case .completed: case .completed:
alert = Alert( alert = AlertState(
title: Text("Update"), title: String(localized: "Update"),
message: Text("System Extension updated."), message: String(localized: "System Extension updated.")
dismissButton: .default(Text("Ok")) {}
) )
case .willCompleteAfterReboot: case .willCompleteAfterReboot:
alert = Alert( alert = AlertState(
title: Text("Update"), title: String(localized: "Update"),
message: Text("Reboot required."), message: String(localized: "Reboot required.")
dismissButton: .default(Text("Ok")) {}
) )
} }
} }
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
@@ -133,21 +131,19 @@ public struct AppView: View {
if let result = try await SystemExtension.uninstall() { if let result = try await SystemExtension.uninstall() {
switch result { switch result {
case .completed: case .completed:
alert = Alert( alert = AlertState(
title: Text("Uninstall"), title: String(localized: "Uninstall"),
message: Text("System Extension removed."), message: String(localized: "System Extension removed.")
dismissButton: .default(Text("Ok")) {}
) )
case .willCompleteAfterReboot: case .willCompleteAfterReboot:
alert = Alert( alert = AlertState(
title: Text("Uninstall"), title: String(localized: "Uninstall"),
message: Text("Reboot required."), message: String(localized: "Reboot required.")
dismissButton: .default(Text("Ok")) {}
) )
} }
} }
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
@@ -55,7 +55,7 @@ public struct ServiceLogView: View {
} }
} }
} }
.alertBinding($viewModel.alert) .alert($viewModel.alert)
.navigationTitle("Service Log") .navigationTitle("Service Log")
#if os(tvOS) #if os(tvOS)
.focusable() .focusable()
@@ -6,7 +6,7 @@ import SwiftUI
final class ServiceLogViewModel: ObservableObject { final class ServiceLogViewModel: ObservableObject {
@Published var isLoading = true @Published var isLoading = true
@Published var content = "" @Published var content = ""
@Published var alert: Alert? @Published var alert: AlertState?
var isEmpty: Bool { var isEmpty: Bool {
content.isEmpty content.isEmpty
+1 -1
View File
@@ -76,7 +76,7 @@ struct EditProfileContentWindow: View {
} message: { } message: {
Text("Do you want to save the changes you made?") Text("Do you want to save the changes you made?")
} }
.alertBinding($viewModel.alert) .alert($viewModel.alert)
.navigationTitle(navigationTitle) .navigationTitle(navigationTitle)
.toolbar { .toolbar {
ToolbarItemGroup(placement: .navigation) { ToolbarItemGroup(placement: .navigation) {
+1 -1
View File
@@ -32,7 +32,7 @@ public struct MainView: View {
.onAppear { .onAppear {
viewModel.onAppear(environments: environments) viewModel.onAppear(environments: environments)
} }
.alertBinding($viewModel.alert) .alert($viewModel.alert)
.toolbar { .toolbar {
ToolbarItem(placement: .navigation) { ToolbarItem(placement: .navigation) {
StartStopButton() StartStopButton()
+7 -7
View File
@@ -9,7 +9,7 @@ public class MainViewModel: ObservableObject {
@Published public var selection = NavigationPage.dashboard @Published public var selection = NavigationPage.dashboard
@Published public var importProfile: LibboxProfileContent? @Published public var importProfile: LibboxProfileContent?
@Published public var importRemoteProfile: LibboxImportRemoteProfile? @Published public var importRemoteProfile: LibboxImportRemoteProfile?
@Published public var alert: Alert? @Published public var alert: AlertState?
public init() {} public init() {}
@@ -53,7 +53,7 @@ public class MainViewModel: ObservableObject {
await importURLProfile(url) await importURLProfile(url)
} }
} else { } else {
alert = Alert(errorMessage: String(localized: "Handled unknown URL \(url.absoluteString)")) alert = AlertState(errorMessage: String(localized: "Handled unknown URL \(url.absoluteString)"))
} }
} }
@@ -63,7 +63,7 @@ public class MainViewModel: ObservableObject {
importProfile = try await .from(readURL(url)) importProfile = try await .from(readURL(url))
url.stopAccessingSecurityScopedResource() url.stopAccessingSecurityScopedResource()
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
return return
} }
if selection != .dashboard { if selection != .dashboard {
@@ -78,10 +78,10 @@ public class MainViewModel: ObservableObject {
private func checkApplicationPath() { private func checkApplicationPath() {
let directoryName = URL(filePath: Bundle.main.bundlePath).deletingLastPathComponent().pathComponents.last let directoryName = URL(filePath: Bundle.main.bundlePath).deletingLastPathComponent().pathComponents.last
if directoryName != "Applications" { if directoryName != "Applications" {
alert = Alert( alert = AlertState(
title: Text("Wrong application location"), title: String(localized: "Wrong application location"),
message: Text("This app needs to be placed under the Applications folder to work."), message: String(localized: "This app needs to be placed under the Applications folder to work."),
dismissButton: .default(Text("Ok")) { dismissButton: .default(String(localized: "Ok")) {
NSWorkspace.shared.selectFile(Bundle.main.bundlePath, inFileViewerRootedAtPath: "") NSWorkspace.shared.selectFile(Bundle.main.bundlePath, inFileViewerRootedAtPath: "")
NSApp.terminate(nil) NSApp.terminate(nil)
} }
+7 -7
View File
@@ -72,7 +72,7 @@ public struct MenuView: View {
private struct StatusSwitch: View { private struct StatusSwitch: View {
@ObservedObject private var profile: ExtensionProfile @ObservedObject private var profile: ExtensionProfile
@State private var alert: Alert? @State private var alert: AlertState?
init(_ profile: ExtensionProfile) { init(_ profile: ExtensionProfile) {
self.profile = profile self.profile = profile
@@ -88,7 +88,7 @@ public struct MenuView: View {
})) {} })) {}
.toggleStyle(.switch) .toggleStyle(.switch)
.disabled(!profile.status.isEnabled) .disabled(!profile.status.isEnabled)
.alertBinding($alert) .alert($alert)
} }
private func switchProfile(_ isEnabled: Bool) async { private func switchProfile(_ isEnabled: Bool) async {
@@ -99,7 +99,7 @@ public struct MenuView: View {
try await profile.stop() try await profile.stop()
} }
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
return return
} }
} }
@@ -117,7 +117,7 @@ public struct MenuView: View {
@State private var profileList: [ProfilePreview] = [] @State private var profileList: [ProfilePreview] = []
@State private var selectedProfileID: Int64 = 0 @State private var selectedProfileID: Int64 = 0
@State private var reasserting = false @State private var reasserting = false
@State private var alert: Alert? @State private var alert: AlertState?
private var selectedProfileIDLocal: Binding<Int64> { private var selectedProfileIDLocal: Binding<Int64> {
$selectedProfileID.withSetter { newValue in $selectedProfileID.withSetter { newValue in
@@ -161,7 +161,7 @@ public struct MenuView: View {
selectedProfileID = await SharedPreferences.selectedProfileID.get() selectedProfileID = await SharedPreferences.selectedProfileID.get()
} }
} }
.alertBinding($alert) .alert($alert)
} }
private func doReload() async { private func doReload() async {
@@ -171,7 +171,7 @@ public struct MenuView: View {
do { do {
profileList = try await ProfileManager.list().map { ProfilePreview($0) } profileList = try await ProfileManager.list().map { ProfilePreview($0) }
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
return return
} }
if profileList.isEmpty { if profileList.isEmpty {
@@ -194,7 +194,7 @@ public struct MenuView: View {
do { do {
try await serviceReload() try await serviceReload()
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
} }
} }
reasserting = false reasserting = false
+5 -5
View File
@@ -11,7 +11,7 @@ struct MainView: View {
@State private var selection = NavigationPage.dashboard @State private var selection = NavigationPage.dashboard
@State private var importProfile: LibboxProfileContent? @State private var importProfile: LibboxProfileContent?
@State private var importRemoteProfile: LibboxImportRemoteProfile? @State private var importRemoteProfile: LibboxImportRemoteProfile?
@State private var alert: Alert? @State private var alert: AlertState?
@State private var showGroups = false @State private var showGroups = false
@State private var showConnections = false @State private var showConnections = false
@State private var buttonState = ButtonVisibilityState() @State private var buttonState = ButtonVisibilityState()
@@ -122,7 +122,7 @@ struct MainView: View {
.onAppear { .onAppear {
environments.postReload() environments.postReload()
} }
.alertBinding($alert) .alert($alert)
.onChangeCompat(of: scenePhase) { newValue in .onChangeCompat(of: scenePhase) { newValue in
if newValue == .active { if newValue == .active {
environments.postReload() environments.postReload()
@@ -185,7 +185,7 @@ struct MainView: View {
var error: NSError? var error: NSError?
importRemoteProfile = LibboxParseRemoteProfileImportLink(url.absoluteString, &error) importRemoteProfile = LibboxParseRemoteProfileImportLink(url.absoluteString, &error)
if let error { if let error {
alert = Alert(error) alert = AlertState(error: error)
return return
} }
if selection != .dashboard { if selection != .dashboard {
@@ -197,14 +197,14 @@ struct MainView: View {
importProfile = try .from(Data(contentsOf: url)) importProfile = try .from(Data(contentsOf: url))
url.stopAccessingSecurityScopedResource() url.stopAccessingSecurityScopedResource()
} catch { } catch {
alert = Alert(error) alert = AlertState(error: error)
return return
} }
if selection != .dashboard { if selection != .dashboard {
selection = .dashboard selection = .dashboard
} }
} else { } else {
alert = Alert(errorMessage: String(localized: "Handled unknown URL \(url.absoluteString)")) alert = AlertState(errorMessage: String(localized: "Handled unknown URL \(url.absoluteString)"))
} }
} }
} }