Merge pull request #54 from neoarz/custom-error-message

Custom error message
This commit is contained in:
Stephen 2025-03-31 20:27:10 -04:00 • committed by GitHub
commit 2f08e258c0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 257 additions and 57 deletions

View file

@ -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)
}
}
}

View file

@ -14,9 +14,8 @@ struct ConsoleLogsView: View {
@State private var autoScroll = true
@State private var scrollView: ScrollViewProxy? = nil
// Alert handlin
@State private var showingExportAlert = false
@State private var showingCopyAlert = false
// Alert handling
@State private var showingCustomAlert = false
@State private var alertMessage = ""
@State private var alertTitle = ""
@State private var isError = false
@ -119,13 +118,13 @@ struct ConsoleLogsView: View {
alertTitle = "Logs Exported"
alertMessage = "Logs have been saved to Files app in StikJIT folder."
isError = false
showingExportAlert = true
showingCustomAlert = true
} catch {
// Set error alert variables and show the alert
alertTitle = "Export Failed"
alertMessage = "Failed to save logs: \(error.localizedDescription)"
isError = true
showingExportAlert = true
showingCustomAlert = true
}
}) {
HStack {
@ -166,7 +165,7 @@ struct ConsoleLogsView: View {
alertTitle = "Logs Copied"
alertMessage = "Logs have been copied to clipboard."
isError = false
showingCopyAlert = true
showingCustomAlert = true
}) {
HStack {
Text("Copy Logs")
@ -220,16 +219,22 @@ struct ConsoleLogsView: View {
}
}
.navigationViewStyle(StackNavigationViewStyle())
.alert(alertTitle, isPresented: $showingExportAlert) {
Button("OK", role: .cancel) { }
} message: {
Text(alertMessage)
}
.alert(alertTitle, isPresented: $showingCopyAlert) {
Button("OK", role: .cancel) { }
} message: {
Text(alertMessage)
}
.overlay(
ZStack {
if showingCustomAlert {
CustomErrorView(
title: alertTitle,
message: alertMessage,
onDismiss: {
showingCustomAlert = false
},
showButton: true,
primaryButtonText: "OK",
messageType: isError ? .error : .success
)
}
}
)
}
// Creates an NSAttributedString that combines timestamp, type, and message

View file

@ -0,0 +1,155 @@
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 messageType: MessageType = .error
enum MessageType {
case error
case success
case info
}
var body: some View {
ZStack {
// Background overlay
Color.black.opacity(0.6)
.edgesIgnoringSafeArea(.all)
.opacity(opacity)
.onTapGesture {
if showButton {
dismissWithAnimation()
}
}
// Card itself
VStack(spacing: 12) {
// Icon based on messageType
switch messageType {
case .error:
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 32))
.foregroundColor(.red.opacity(0.9))
.padding(.top, 8)
case .success:
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 32))
.foregroundColor(.green.opacity(0.9))
.padding(.top, 8)
case .info:
Image(systemName: "info.circle.fill")
.font(.system(size: 32))
.foregroundColor(.blue.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
Text(message)
.font(.system(size: 15, design: .rounded))
.foregroundColor(.white.opacity(0.9))
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
.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 {
withAnimation(.spring(response: 0.25, dampingFraction: 0.7)) {
opacity = 1
scale = 1
}
}
}
private func dismissWithAnimation() {
withAnimation(.easeOut(duration: 0.15)) {
opacity = 0
scale = 0.8
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
onDismiss()
}
}
}