diff --git a/StikJIT/StikJITApp.swift b/StikJIT/StikJITApp.swift index f250755..f5de8c9 100644 --- a/StikJIT/StikJITApp.swift +++ b/StikJIT/StikJITApp.swift @@ -232,18 +232,25 @@ struct HeartbeatApp: App { alert_string = "[Download DDI Error]: " + result show_alert = true } - } - } } - - } - .alert(alert_title, isPresented: $show_alert) { - Button("OK", role: .cancel) { } - } message: { - Text(alert_title) } + .overlay( + ZStack { + if show_alert { + CustomErrorView( + title: alert_title, + message: alert_string, + onDismiss: { + show_alert = false + }, + showButton: true, + primaryButtonText: "OK" + ) + } + } + ) } } } @@ -430,16 +437,15 @@ func startHeartbeatInBackground() { print("Error: \(message ?? "") (Code: \(result))") DispatchQueue.main.async { - if let mainWindow = UIApplication.shared.windows.last { - let alert = UIAlertController(title: "Heartbeat Error", message: "\(message ?? "") (\(result))", preferredStyle: .alert) - - let tryAgainAction = UIAlertAction(title: "Try Again", style: .default) { _ in + showAlert( + title: "Heartbeat Error", + message: "Failed to connect to Heartbeat (\(result))", + showOk: false, + showTryAgain: true + ) { shouldTryAgain in + if shouldTryAgain { startHeartbeatInBackground() } - - alert.addAction(tryAgainAction) - - mainWindow.rootViewController?.present(alert, animated: true, completion: nil) } } } @@ -499,34 +505,68 @@ struct LoadingView: View { public func showAlert(title: String, message: String, showOk: Bool, showTryAgain: Bool = false, completion: @escaping (Bool) -> Void) { DispatchQueue.main.async { - if let mainWindow = UIApplication.shared.windows.last { - let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - - if showOk, !showTryAgain { - let okAction = UIAlertAction(title: "OK", style: .default) { _ in - completion(true) - } - - alert.addAction(okAction) - } else if !showTryAgain { - completion(false) - } - - if showTryAgain { - let tryAgainAction = UIAlertAction(title: "Try Again", style: .default) { _ in - completion(true) - } - - alert.addAction(tryAgainAction) - - let okAction = UIAlertAction(title: "OK", style: .cancel) { _ in + let rootViewController = UIApplication.shared.windows.last?.rootViewController + + if showTryAgain { + // Configure with "Try Again" button only (no Cancel button) + let customErrorView = CustomErrorView( + title: title, + message: message, + onDismiss: { + // Called when tapped outside completion(false) + }, + showButton: true, + primaryButtonText: "Try Again", + onPrimaryButtonTap: { + // Try Again was tapped + completion(true) } - - alert.addAction(okAction) - } + ) - mainWindow.rootViewController?.present(alert, animated: true, completion: nil) + let hostingController = UIHostingController(rootView: customErrorView) + hostingController.modalPresentationStyle = .overFullScreen + hostingController.modalTransitionStyle = .crossDissolve + hostingController.view.backgroundColor = .clear + rootViewController?.present(hostingController, animated: true) + } else if showOk { + // Configure with just "OK" button + let customErrorView = CustomErrorView( + title: title, + message: message, + onDismiss: { + // Called when tapped outside + completion(true) + }, + showButton: true, + primaryButtonText: "OK", + onPrimaryButtonTap: { + // OK was tapped + completion(true) + } + ) + + let hostingController = UIHostingController(rootView: customErrorView) + hostingController.modalPresentationStyle = .overFullScreen + hostingController.modalTransitionStyle = .crossDissolve + hostingController.view.backgroundColor = .clear + rootViewController?.present(hostingController, animated: true) + } else { + // No buttons case + let customErrorView = CustomErrorView( + title: title, + message: message, + onDismiss: { + completion(false) + }, + showButton: false + ) + + let hostingController = UIHostingController(rootView: customErrorView) + hostingController.modalPresentationStyle = .overFullScreen + hostingController.modalTransitionStyle = .crossDissolve + hostingController.view.backgroundColor = .clear + rootViewController?.present(hostingController, animated: true) } } } diff --git a/StikJIT/Views/Custom Error View.swift b/StikJIT/Views/Custom Error View.swift new file mode 100644 index 0000000..df648d4 --- /dev/null +++ b/StikJIT/Views/Custom Error View.swift @@ -0,0 +1,144 @@ +import SwiftUI + +struct CustomErrorView: View { + var title: String + var message: String + var onDismiss: () -> Void + var showButton: Bool = true + @State private var opacity: Double = 0 + @State private var scale: CGFloat = 0.8 + var primaryButtonText: String = "OK" + var secondaryButtonText: String = "Cancel" + var onPrimaryButtonTap: (() -> Void)? = nil + var onSecondaryButtonTap: (() -> Void)? = nil + var showSecondaryButton: Bool = false + + var body: some View { + ZStack { + // Background overlay + Color.black.opacity(0.6) + .edgesIgnoringSafeArea(.all) + .opacity(opacity) + .onTapGesture { + if showButton { + dismissWithAnimation() + } + } + + // Error card - made more compact + VStack(spacing: 12) { + // Error icon - smaller + Image(systemName: "exclamationmark.triangle.fill") + .font(.system(size: 32)) + .foregroundColor(.red.opacity(0.9)) + .padding(.top, 8) + + // Title - slightly smaller + Text(title) + .font(.system(size: 18, weight: .bold, design: .rounded)) + .foregroundColor(.white) + .multilineTextAlignment(.center) + + // Divider + Rectangle() + .frame(height: 1) + .foregroundColor(.white.opacity(0.2)) + .padding(.horizontal, 12) + + // Message - slightly smaller + Text(message) + .font(.system(size: 15, design: .rounded)) + .foregroundColor(.white.opacity(0.9)) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .padding(.horizontal, 12) + + // Restart instructions when button is hidden + if !showButton { + Text("Please connect to WireGuard VPN and restart the app.") + .font(.system(size: 15, weight: .medium, design: .rounded)) + .foregroundColor(.white.opacity(0.9)) + .multilineTextAlignment(.center) + .padding(.top, 4) + .padding(.horizontal, 12) + } + + // Dismiss button (only shown when showButton is true) + if showButton { + VStack(spacing: 6) { + Button(action: { + dismissWithAnimation() + onPrimaryButtonTap?() // Call primary action if provided + }) { + Text(primaryButtonText) + .font(.system(size: 16, weight: .semibold, design: .rounded)) + .foregroundColor(.black) + .frame(height: 38) + .frame(maxWidth: .infinity) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(Color.white) + ) + } + + if showSecondaryButton { + Button(action: { + dismissWithAnimation() + onSecondaryButtonTap?() // Call secondary action if provided + }) { + Text(secondaryButtonText) + .font(.system(size: 16, weight: .medium, design: .rounded)) + .foregroundColor(.white) + .frame(height: 38) + .frame(maxWidth: .infinity) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(Color.gray.opacity(0.3)) + ) + } + } + } + .padding(.horizontal, 12) + .padding(.bottom, 12) + .padding(.top, 6) + } else { + Spacer() + .frame(height: 12) + } + } + // Made the card smaller overall + .frame(width: min(UIScreen.main.bounds.width - 80, 300)) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(Color(UIColor.systemGray6).opacity(0.95)) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(Color.white.opacity(0.2), lineWidth: 1) + ) + ) + .shadow(color: Color.black.opacity(0.25), radius: 16, x: 0, y: 8) + .scaleEffect(scale) + .opacity(opacity) + } + .onAppear { + // Made animation faster (0.25s instead of 0.4s) + withAnimation(.spring(response: 0.25, dampingFraction: 0.7)) { + opacity = 1 + scale = 1 + } + } + } + + private func dismissWithAnimation() { + // Made dismiss animation faster (0.15s instead of 0.2s) + withAnimation(.easeOut(duration: 0.15)) { + opacity = 0 + scale = 0.8 + } + + // Delay the actual dismissal to allow the animation to complete + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { + onDismiss() + } + } +} \ No newline at end of file