refactor: improve alert error context and copy action

This commit is contained in:
世界
2026-02-08 18:25:55 +08:00
parent a86dfc26d1
commit 8897e6deeb
32 changed files with 125 additions and 82 deletions
@@ -7,24 +7,24 @@ open class BaseViewModel: ObservableObject {
public init() {}
public func showError(_ error: Error) {
alert = AlertState(error: error)
public func showError(_ error: Error, action: String) {
alert = AlertState(action: action, error: error)
}
public func execute(_ operation: () async throws -> Void) async {
public func execute(_ operation: () async throws -> Void, action: String) async {
do {
try await operation()
} catch {
alert = AlertState(error: error)
alert = AlertState(action: action, error: error)
}
}
public func executeOnBackground(_ operation: @escaping @Sendable () async throws -> Void) async {
public func executeOnBackground(_ operation: @escaping @Sendable () async throws -> Void, action: String) async {
do {
try await operation()
} catch {
await MainActor.run {
alert = AlertState(error: error)
alert = AlertState(action: action, error: error)
}
}
}
@@ -105,7 +105,7 @@ public struct GlobalChecksModifier: ViewModifier {
try await profile.importProfile()
} catch {
await MainActor.run {
alert = AlertState(error: error)
alert = AlertState(action: "import profile", error: error)
}
return
}
@@ -174,7 +174,7 @@ public struct GlobalChecksModifier: ViewModifier {
}
} catch {
await MainActor.run {
alert = AlertState(error: error)
alert = AlertState(action: "check deprecated notes", error: error)
}
}
}
@@ -86,7 +86,7 @@ public struct ShareButtonCompat<Label: View>: View {
}
} catch {
await MainActor.run {
alert = AlertState(error: error)
alert = AlertState(action: "prepare share file", error: error)
}
}
}
@@ -117,7 +117,7 @@ public struct ShareButtonCompat<Label: View>: View {
}
} catch {
await MainActor.run {
alert = AlertState(error: error)
alert = AlertState(action: "prepare share file", error: error)
}
}
}
@@ -90,7 +90,7 @@ public class ConnectionListViewModel: BaseViewModel {
do {
try LibboxNewStandaloneCommandClient()!.closeConnections()
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "close all connections", error: error)
}
}
@@ -118,7 +118,7 @@ public struct ConnectionView: View {
try await LibboxNewStandaloneCommandClient()!.closeConnection(connection.id)
} catch {
await MainActor.run {
alert = AlertState(error: error)
alert = AlertState(action: "close connection", error: error)
}
}
}
@@ -195,7 +195,7 @@ public struct ClashModeCard: View {
try LibboxNewStandaloneCommandClient()!.setClashMode(newMode)
} catch {
await MainActor.run {
alert = AlertState(error: error)
alert = AlertState(action: "set clash mode", error: error)
}
}
}
@@ -134,7 +134,7 @@ public struct ProfileCard: View {
) { result in
viewModel.exportDocument = nil
if case let .failure(error) = result {
viewModel.alert = AlertState(error: error)
viewModel.alert = AlertState(action: "export profile", error: error)
}
}
#endif
@@ -371,7 +371,7 @@ public struct ProfileCard: View {
)
#endif
} catch {
viewModel.alert = AlertState(error: error)
viewModel.alert = AlertState(action: "share profile", error: error)
}
}
}
@@ -394,7 +394,7 @@ public struct ProfileCard: View {
}
} catch {
await MainActor.run {
viewModel.alert = AlertState(error: error)
viewModel.alert = AlertState(action: "export profile", error: error)
}
}
}
@@ -427,7 +427,7 @@ public struct ProfileCard: View {
do {
viewModel.qrsShareData = try await profile.origin.encodedContentDataAsync()
} catch {
viewModel.alert = AlertState(error: error)
viewModel.alert = AlertState(action: "prepare QRS share", error: error)
viewModel.showQRSShare = false
}
}
@@ -536,7 +536,7 @@ extension ProfileCard {
try await profile.updateRemoteProfile()
environments.profileUpdate.send()
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "update remote profile", error: error)
}
}
}
@@ -402,7 +402,7 @@ struct ProfilePickerSheet: View {
try await profile.origin.updateRemoteProfile()
environments.profileUpdate.send()
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "update remote profile", error: error)
}
}
@@ -742,7 +742,7 @@ private struct ProfilePickerRow: View {
) { result in
exportDocument = nil
if case let .failure(error) = result {
alert = AlertState(error: error)
alert = AlertState(action: "export profile", error: error)
}
}
}
@@ -789,7 +789,7 @@ private struct ProfilePickerRow: View {
) { result in
exportDocument = nil
if case let .failure(error) = result {
alert = AlertState(error: error)
alert = AlertState(action: "export profile", error: error)
}
}
}
@@ -919,7 +919,7 @@ private struct ProfilePickerRow: View {
}
} catch {
await MainActor.run {
alert = AlertState(error: error)
alert = AlertState(action: "prepare QRS share", error: error)
showQRSShare = false
}
}
@@ -1015,7 +1015,7 @@ private struct ProfilePickerRow: View {
}
} catch {
await MainActor.run {
alert = AlertState(error: error)
alert = AlertState(action: "export profile", error: error)
}
}
}
@@ -1067,7 +1067,7 @@ private struct ProfilePickerRow: View {
}
} catch {
await MainActor.run {
alert = AlertState(error: error)
alert = AlertState(action: "share profile", error: error)
}
}
}
@@ -1091,7 +1091,7 @@ private struct ProfilePickerRow: View {
}
} catch {
await MainActor.run {
alert = AlertState(error: error)
alert = AlertState(action: "export profile", error: error)
}
}
}
@@ -1282,7 +1282,7 @@ private struct ProfilePickerRow: View {
) { result in
exportDocument = nil
if case let .failure(error) = result {
alert = AlertState(error: error)
alert = AlertState(action: "export profile", error: error)
}
}
}
@@ -1336,7 +1336,7 @@ private struct ProfilePickerRow: View {
}
} catch {
await MainActor.run {
alert = AlertState(error: error)
alert = AlertState(action: "prepare QRS share", error: error)
showQRSShare = false
}
}
@@ -1405,7 +1405,7 @@ private struct ProfilePickerRow: View {
}
} catch {
await MainActor.run {
alert = AlertState(error: error)
alert = AlertState(action: "export profile", error: error)
}
}
}
@@ -26,7 +26,7 @@ public struct InstallProfileButton: View {
try await ExtensionProfile.install()
await callback()
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "install network extension", error: error)
}
}
}
@@ -31,7 +31,7 @@
}
await callback()
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "install system extension", error: error)
}
}
}
@@ -165,7 +165,8 @@ public struct StartStopButton: View {
} catch {
await MainActor.run {
isStarting = false
alert = AlertState(error: error)
let action = isEnabled ? "start service" : "stop service"
alert = AlertState(action: action, error: error)
}
}
}
@@ -69,7 +69,7 @@ public final class DashboardViewModel: BaseViewModel {
await SharedPreferences.selectedProfileID.set(selectedProfileID)
}
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "load profile list", error: error)
return
}
}
@@ -109,7 +109,7 @@ extension ExtensionProfile {
)
}
#endif
return AlertState(error: nsError)
return AlertState(action: "fetch last disconnect error", error: nsError)
}
}
@@ -15,7 +15,7 @@ public final class OverviewViewModel: BaseViewModel {
do {
try await profile.reloadService()
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "reload service", error: error)
}
}
reasserting = false
@@ -32,7 +32,7 @@ public final class OverviewViewModel: BaseViewModel {
await MainActor.run { reasserting = false }
}
} catch {
await MainActor.run { alert = AlertState(error: error) }
await MainActor.run { alert = AlertState(action: "update system proxy settings", error: error) }
}
}
}
@@ -90,7 +90,7 @@ public class GroupListViewModel: BaseViewModel {
try await LibboxNewStandaloneCommandClient()!.selectOutbound(groupTag, outboundTag: outboundTag)
} catch {
await MainActor.run {
alert = AlertState(error: error)
alert = AlertState(action: "select outbound", error: error)
}
}
}
@@ -109,7 +109,7 @@ public class GroupListViewModel: BaseViewModel {
try await LibboxNewStandaloneCommandClient()!.setGroupExpand(tag, isExpand: isExpand)
} catch {
await MainActor.run {
alert = AlertState(error: error)
alert = AlertState(action: "update group expansion", error: error)
}
}
}
@@ -125,7 +125,7 @@ public class GroupListViewModel: BaseViewModel {
try await LibboxNewStandaloneCommandClient()!.urlTest(tag)
} catch {
await MainActor.run {
alert = AlertState(error: error)
alert = AlertState(action: "run URL test", error: error)
}
}
}
+1 -1
View File
@@ -399,7 +399,7 @@ private struct LogContentInnerView: View {
cleanup()
logFileURL = nil
if case let .failure(error) = result {
alert = AlertState(error: error)
alert = AlertState(action: "export log file", error: error)
}
}
.sheet(isPresented: $showShareSheet) {
@@ -135,7 +135,7 @@ public class LogDataModel: ObservableObject {
try text.write(to: fileURL, atomically: true, encoding: .utf8)
logFileURL = fileURL
} catch {
viewModel?.alert = AlertState(error: error)
viewModel?.alert = AlertState(action: "prepare log file", error: error)
}
}
#endif
@@ -85,7 +85,7 @@ public final class EditProfileContentViewModel: BaseViewModel {
do {
try await loadContentBackground()
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "load profile content", error: error)
}
isLoading = false
}
@@ -111,7 +111,7 @@ public final class EditProfileContentViewModel: BaseViewModel {
do {
try await saveContentBackground(profile)
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "save profile content", error: error)
return
}
isChanged = false
@@ -21,7 +21,7 @@ public final class EditProfileViewModel: BaseViewModel {
try await profile.updateRemoteProfile()
environments.profileUpdate.send()
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "update remote profile", error: error)
}
}
@@ -29,7 +29,7 @@ public final class EditProfileViewModel: BaseViewModel {
do {
try await ProfileManager.delete(profile)
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "delete profile", error: error)
return
}
environments.profileUpdate.send()
@@ -46,7 +46,7 @@ public final class EditProfileViewModel: BaseViewModel {
#endif
try await profile.onProfileUpdated()
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "save profile", error: error)
return
}
isChanged = false
@@ -38,7 +38,7 @@
case let .failed(error):
DispatchQueue.main.async { [self] in
reset()
alert = AlertState(error: error)
alert = AlertState(action: "connect to import source", error: error)
}
default: break
}
@@ -47,7 +47,7 @@
do {
try await loopMessages(environments: environments)
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "import profile from device", error: error)
reset()
}
}
@@ -121,7 +121,7 @@
try await socket.write(request.encode())
} catch {
isImporting = false
alert = AlertState(error: error)
alert = AlertState(action: "request profile content from device", error: error)
reset()
}
}
@@ -223,14 +223,14 @@ public struct NewProfileMenuView: View {
environments.profileUpdate.send()
dismiss()
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "import profile", error: error)
}
}
},
secondaryButton: .cancel()
)
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "read imported profile file", error: error)
}
}
}
@@ -250,7 +250,7 @@ public struct NewProfileMenuView: View {
var error: NSError?
let remoteProfile = LibboxParseRemoteProfileImportLink(string, &error)
if let error {
alert = AlertState(error: error)
alert = AlertState(action: "parse QR code profile link", error: error)
return
}
guard let remoteProfile else {
@@ -274,14 +274,14 @@ public struct NewProfileMenuView: View {
environments.profileUpdate.send()
dismiss()
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "import profile", error: error)
}
}
},
secondaryButton: .cancel()
)
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "decode QRS profile data", error: error)
}
}
#endif
@@ -185,7 +185,7 @@ public struct NewProfileView: View {
viewModel.fileURL = urls[0]
}
} catch {
viewModel.alert = AlertState(error: error)
viewModel.alert = AlertState(action: "read imported profile file", error: error)
return
}
}
@@ -213,7 +213,7 @@ public struct NewProfileView: View {
viewModel.fileURL = urls[0]
}
} catch {
viewModel.alert = AlertState(error: error)
viewModel.alert = AlertState(action: "read imported profile file", error: error)
return
}
}
@@ -72,7 +72,7 @@ public final class NewProfileViewModel: BaseViewModel {
do {
createdProfile = try await createProfileBackground()
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "create profile", error: error)
return
}
@@ -160,7 +160,7 @@
)
#endif
default:
alert = AlertState(error: error)
alert = AlertState(action: "scan QR code", error: error)
}
}
}
@@ -204,7 +204,7 @@ public struct CoreView: View {
try await environments.extensionProfile!.stop()
await destroyWorkingDirectory()
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "stop service before destroying working directory", error: error)
}
}
@@ -233,7 +233,7 @@ public struct CoreView: View {
#endif
isLoading = true
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "destroy working directory", error: error)
}
}
@@ -281,7 +281,7 @@ public struct CoreView: View {
try await manager.signalEnumerator(for: .workingSet)
} catch {
await MainActor.run {
alert = AlertState(error: error)
alert = AlertState(action: "notify Files app about working directory changes", error: error)
}
}
}
@@ -300,7 +300,7 @@ public struct CoreView: View {
}
} catch {
await MainActor.run {
alert = AlertState(error: error)
alert = AlertState(action: "open working directory in Files", error: error)
}
}
}
@@ -109,14 +109,14 @@ public struct AppView: View {
try HelperServiceManager.registerRootHelper()
refreshHelperStatus()
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "update helper service", error: error)
}
}
} label: {
Label("Update", systemImage: "arrow.down.doc.fill")
}
FormButton(role: .destructive) {
performHelperAction {
performHelperAction(actionName: "uninstall helper service") {
try HelperServiceManager.unregisterRootHelper()
}
} label: {
@@ -130,7 +130,7 @@ public struct AppView: View {
}
} else {
FormButton {
performHelperAction {
performHelperAction(actionName: "install helper service") {
try HelperServiceManager.registerRootHelper()
}
} label: {
@@ -210,7 +210,7 @@ public struct AppView: View {
try SMAppService.mainApp.unregister()
}
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "update login items", error: error)
}
}
@@ -231,7 +231,7 @@ public struct AppView: View {
}
}
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "update system extension", error: error)
}
}
@@ -252,16 +252,16 @@ public struct AppView: View {
}
}
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "uninstall system extension", error: error)
}
}
private func performHelperAction(_ action: () throws -> Void) {
private func performHelperAction(actionName: String, _ action: () throws -> Void) {
do {
try action()
refreshHelperStatus()
} catch {
alert = AlertState(error: error)
alert = AlertState(action: actionName, error: error)
}
}
@@ -264,7 +264,7 @@ public struct OnDemandRulesView: View {
await updateService()
isLoading = true
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "reset on-demand rules", error: error)
}
}
} label: {
@@ -289,7 +289,7 @@ public struct OnDemandRulesView: View {
let enabled = mode != .disabled
try await profile.updateOnDemand(enabled: enabled, useDefaultRules: mode == .alwaysOn)
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "update on-demand rules", error: error)
}
}
@@ -111,7 +111,7 @@ struct PacketTunnelView: View {
do {
try await profile.restart()
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "restart service", error: error)
}
}
@@ -65,7 +65,7 @@ public struct ProfileOverrideView: View {
do {
try await profile.reloadService()
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "reload service", error: error)
}
}
+40 -5
View File
@@ -1,5 +1,10 @@
import Foundation
import SwiftUI
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
public struct AlertState: Equatable {
public var title: String
@@ -36,15 +41,45 @@ public struct AlertState: Equatable {
}
}
public init(error: Error, dismiss: (() -> Void)? = nil) {
self.init(errorMessage: error.localizedDescription, dismiss: dismiss)
private static func formatErrorMessage(action: String, error: Error) -> String {
let normalizedAction = action.trimmingCharacters(in: .whitespacesAndNewlines)
let normalizedDescription = error.localizedDescription.trimmingCharacters(in: .whitespacesAndNewlines)
let actionText = normalizedAction.isEmpty ? "complete operation" : normalizedAction
if normalizedDescription.isEmpty {
return "Failed to \(actionText)"
}
return "Failed to \(actionText)\n\(normalizedDescription)"
}
public init(errorMessage: String, dismiss: (() -> Void)? = nil) {
private static func copyErrorMessage(_ text: String) {
#if canImport(UIKit)
UIPasteboard.general.string = text
#elseif canImport(AppKit)
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(text, forType: .string)
#endif
}
public init(action: String, error: Error, dismiss: (() -> Void)? = nil) {
self.init(
errorMessage: Self.formatErrorMessage(action: action, error: error),
dismiss: dismiss,
allowsCopy: true
)
}
public init(errorMessage: String, dismiss: (() -> Void)? = nil, allowsCopy: Bool = false) {
title = String(localized: "Error")
message = errorMessage
primaryButton = .default(String(localized: "Ok"), action: dismiss)
secondaryButton = nil
if allowsCopy {
primaryButton = .default(String(localized: "Copy")) {
Self.copyErrorMessage(errorMessage)
}
secondaryButton = .default(String(localized: "Ok"), action: dismiss)
} else {
primaryButton = .default(String(localized: "Ok"), action: dismiss)
secondaryButton = nil
}
onDismiss = nil
}
+7
View File
@@ -148,6 +148,13 @@ open class ExtensionProvider: NEPacketTunnelProvider {
throw ExtensionStartupError("(packet-tunnel) error: setup service: \(setupError.localizedDescription)")
}
let stderrPath = URL(fileURLWithPath: tempPath, isDirectory: true).appendingPathComponent("stderr.log").path
var stderrError: NSError?
LibboxRedirectStderr(stderrPath, &stderrError)
if let stderrError {
throw ExtensionStartupError("(packet-tunnel) redirect stderr error: \(stderrError.localizedDescription)")
}
let ignoreMemoryLimit = (effectiveOptions["ignoreMemoryLimit"] as? NSNumber)?.boolValue ?? false
LibboxSetMemoryLimit(!ignoreMemoryLimit)
+2 -2
View File
@@ -45,7 +45,7 @@ public class MainViewModel: BaseViewModel {
var error: NSError?
importRemoteProfile = LibboxParseRemoteProfileImportLink(url.absoluteString, &error)
if let error {
alert = AlertState(error: error)
alert = AlertState(action: "parse remote profile import link", error: error)
}
} else if url.pathExtension == "bpf" {
Task {
@@ -62,7 +62,7 @@ public class MainViewModel: BaseViewModel {
try await .from(readURL(url))
}
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "import profile from URL", error: error)
}
}
+2 -2
View File
@@ -215,7 +215,7 @@ struct MainView: View {
var error: NSError?
importRemoteProfile = LibboxParseRemoteProfileImportLink(url.absoluteString, &error)
if let error {
alert = AlertState(error: error)
alert = AlertState(action: "parse remote profile import link", error: error)
}
} else if url.pathExtension == "bpf" {
do {
@@ -223,7 +223,7 @@ struct MainView: View {
try .from(Data(contentsOf: url))
}
} catch {
alert = AlertState(error: error)
alert = AlertState(action: "import profile from URL", error: error)
}
} else {
alert = AlertState(errorMessage: String(localized: "Handled unknown URL \(url.absoluteString)"))