import Combine import Foundation import SwiftUI #if canImport(UIKit) import UIKit #elseif canImport(AppKit) import AppKit #endif 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) } } 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)" } private static func copyErrorMessage(_ text: String) { #if canImport(UIKit) && !os(tvOS) UIPasteboard.general.string = text #elseif canImport(AppKit) NSPasteboard.general.clearContents() NSPasteboard.general.setString(text, forType: .string) #endif } private static var supportsErrorCopy: Bool { #if canImport(UIKit) && !os(tvOS) true #elseif canImport(AppKit) true #else false #endif } public init(action: String, error: Error, dismiss: (() -> Void)? = nil) { let errorMessage = Self.formatErrorMessage(action: action, error: error) self.init(errorMessage: errorMessage, dismiss: dismiss) if Self.supportsErrorCopy { primaryButton = .default(String(localized: "Copy")) { Self.copyErrorMessage(errorMessage) } secondaryButton = .default(String(localized: "Ok"), action: dismiss) } } public init(errorMessage: String, dismiss: (() -> Void)? = nil) { title = String(localized: "Error") message = errorMessage if Self.supportsErrorCopy { 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 } 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 { func alert(_ binding: Binding) -> 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) } } } public struct ImportRemoteProfileRequest: Hashable, Identifiable { public var id: String { url } public let name: String public let url: String public init(name: String, url: String) { self.name = name self.url = url } } @MainActor public class ExtensionEnvironments: ObservableObject { @Published public var commandClient = CommandClient([.log, .status, .groups, .clashMode]) public let crashReportManager = CrashReportManager() public let oomReportManager = OOMReportManager() public var totalUnreadReportCount: Int { crashReportManager.unreadCount + oomReportManager.unreadCount } @Published public var extensionProfileLoading = true @Published public var extensionProfile: ExtensionProfile? @Published public var emptyProfiles = false @Published public var pendingImportRemoteProfile: ImportRemoteProfileRequest? public var logSearchText = "" public var connectionSearchText = "" public let profileUpdate = ObjectWillChangePublisher() public let selectedProfileUpdate = ObjectWillChangePublisher() public let openSettings = ObjectWillChangePublisher() private var cancellables = Set() public init() { crashReportManager.objectWillChange .sink { [weak self] _ in self?.objectWillChange.send() } .store(in: &cancellables) oomReportManager.objectWillChange .sink { [weak self] _ in self?.objectWillChange.send() } .store(in: &cancellables) if Variant.screenshotMode { extensionProfileLoading = false extensionProfile = .mock commandClient.setupMockData() } } public func postReload() { Task { await reload() await crashReportManager.refresh() await oomReportManager.refresh() } } public func reload() async { if Variant.screenshotMode { return } if let newProfile = try? await ExtensionProfile.load() { if extensionProfile == nil || extensionProfile?.status == .invalid { newProfile.register() extensionProfile = newProfile extensionProfileLoading = false } } else { extensionProfile = nil extensionProfileLoading = false } } public func connect() { if Variant.screenshotMode { return } guard let profile = extensionProfile else { return } if profile.status.isConnected, !commandClient.isConnected { commandClient.connect() } } }