mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-16 04:20:30 +00:00
Merge pull request #54 from neoarz/custom-error-message
Custom error message
This commit is contained in:
commit
2f08e258c0
3 changed files with 257 additions and 57 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
155
StikJIT/Views/Custom Error View.swift
Normal file
155
StikJIT/Views/Custom Error View.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue