mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
[ADD] Error log view
This commit is contained in:
@@ -27,6 +27,14 @@ struct DateFormatterHelper {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .medium
|
||||
dateFormatter.timeStyle = .none
|
||||
dateFormatter.doesRelativeDateFormatting = true
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
private static let timeFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .none
|
||||
dateFormatter.timeStyle = .short
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
@@ -53,4 +61,12 @@ struct DateFormatterHelper {
|
||||
static func string(forRelativeDate date: Date, to referenceDate: Date = Date()) -> String {
|
||||
self.relativeDateFormatter.localizedString(for: date, relativeTo: referenceDate)
|
||||
}
|
||||
|
||||
static func string(for date: Date) -> String {
|
||||
self.mediumDateFormatter.string(from: date)
|
||||
}
|
||||
|
||||
static func timeString(for date: Date) -> String {
|
||||
self.timeFormatter.string(from: date)
|
||||
}
|
||||
}
|
||||
|
||||
33
AltStore/View Components/ModalNavigationLink.swift
Normal file
33
AltStore/View Components/ModalNavigationLink.swift
Normal file
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// ModalNavigationLink.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 03.02.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ModalNavigationLink<Label: View, Modal: View>: View {
|
||||
let modal: () -> Modal
|
||||
let label: () -> Label
|
||||
|
||||
@State var isPresentingModal: Bool = false
|
||||
|
||||
var body: some View {
|
||||
SwiftUI.Button {
|
||||
self.isPresentingModal = true
|
||||
} label: {
|
||||
self.label()
|
||||
}
|
||||
.sheet(isPresented: self.$isPresentingModal) {
|
||||
self.modal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//struct ModalNavigationLink_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// ModalNavigationLink()
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// FilePreviewView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 03.02.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import QuickLook
|
||||
|
||||
struct FilePreviewView: UIViewControllerRepresentable {
|
||||
let urls: [URL]
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(urls: self.urls)
|
||||
}
|
||||
|
||||
func makeUIViewController(context: Context) -> some UIViewController {
|
||||
let previewController = QLPreviewController()
|
||||
previewController.dataSource = context.coordinator
|
||||
return UINavigationController(rootViewController: previewController)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
|
||||
context.coordinator.urls = self.urls
|
||||
}
|
||||
}
|
||||
|
||||
extension FilePreviewView {
|
||||
|
||||
class Coordinator: QLPreviewControllerDataSource {
|
||||
var urls: [URL]
|
||||
|
||||
init(urls: [URL]) {
|
||||
self.urls = urls
|
||||
}
|
||||
|
||||
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
|
||||
urls.count
|
||||
}
|
||||
|
||||
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
|
||||
urls[index] as QLPreviewItem
|
||||
}
|
||||
}
|
||||
}
|
||||
169
AltStore/Views/Settings/ErrorLogView.swift
Normal file
169
AltStore/Views/Settings/ErrorLogView.swift
Normal file
@@ -0,0 +1,169 @@
|
||||
//
|
||||
// ErrorLogView.swift
|
||||
// SideStore
|
||||
//
|
||||
// Created by Fabian Thies on 03.02.23.
|
||||
// Copyright © 2023 SideStore. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AltStoreCore
|
||||
import ExpandableText
|
||||
|
||||
struct ErrorLogView: View {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
@SwiftUI.FetchRequest(sortDescriptors: [
|
||||
NSSortDescriptor(keyPath: \LoggedError.date, ascending: false)
|
||||
])
|
||||
var loggedErrors: FetchedResults<LoggedError>
|
||||
|
||||
var groupedLoggedErrors: [Date: [LoggedError]] {
|
||||
Dictionary(grouping: loggedErrors, by: { Calendar.current.startOfDay(for: $0.date) })
|
||||
}
|
||||
|
||||
@State var currentFaqUrl: URL?
|
||||
@State var isShowingMinimuxerLog: Bool = false
|
||||
@State var isShowingDeleteConfirmation: Bool = false
|
||||
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(groupedLoggedErrors.keys.sorted(by: { $0 > $1 }), id: \.self) { date in
|
||||
Section {
|
||||
let errors = groupedLoggedErrors[date] ?? []
|
||||
ForEach(errors, id: \.date) { error in
|
||||
VStack(spacing: 8) {
|
||||
HStack(alignment: .top) {
|
||||
Group {
|
||||
if let storeApp = error.storeApp {
|
||||
AppIconView(iconUrl: storeApp.iconURL, size: 50)
|
||||
} else {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 50*0.234, style: .continuous)
|
||||
.foregroundColor(Color(UIColor.secondarySystemFill))
|
||||
|
||||
Image(systemSymbol: .exclamationmarkCircle)
|
||||
.imageScale(.large)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
.frame(width: 50, height: 50)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(error.localizedFailure ?? "Operation Failed")
|
||||
.bold()
|
||||
|
||||
Group {
|
||||
switch error.domain {
|
||||
case AltServerErrorDomain: Text("SideServer Error \(error.code)")
|
||||
case OperationError.domain: Text("SideStore Error \(error.code)")
|
||||
default: Text(error.error.localizedErrorCode)
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(DateFormatterHelper.timeString(for: error.date))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
let nsError = error.error as NSError
|
||||
let errorDescription = [nsError.localizedDescription, nsError.localizedRecoverySuggestion].compactMap { $0 }.joined(separator: "\n\n")
|
||||
|
||||
Menu {
|
||||
SwiftUI.Button {
|
||||
UIPasteboard.general.string = errorDescription
|
||||
} label: {
|
||||
Label("Copy Error Message", systemSymbol: .docOnDoc)
|
||||
}
|
||||
|
||||
SwiftUI.Button {
|
||||
UIPasteboard.general.string = error.error.localizedErrorCode
|
||||
} label: {
|
||||
Label("Copy Error Code", systemSymbol: .docOnDoc)
|
||||
}
|
||||
|
||||
SwiftUI.Button {
|
||||
self.searchFAQ(for: error)
|
||||
} label: {
|
||||
Label("Search FAQ", systemSymbol: .magnifyingglass)
|
||||
}
|
||||
|
||||
} label: {
|
||||
Text(errorDescription)
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text(DateFormatterHelper.string(for: date))
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitle("Error Log")
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
ModalNavigationLink {
|
||||
FilePreviewView(urls: [
|
||||
FileManager.default.documentsDirectory.appendingPathComponent("minimuxer.log")
|
||||
])
|
||||
.ignoresSafeArea()
|
||||
} label: {
|
||||
Image(systemSymbol: .ladybug)
|
||||
}
|
||||
|
||||
|
||||
SwiftUI.Button {
|
||||
self.isShowingDeleteConfirmation = true
|
||||
} label: {
|
||||
Image(systemSymbol: .trash)
|
||||
}
|
||||
.actionSheet(isPresented: self.$isShowingDeleteConfirmation) {
|
||||
ActionSheet(
|
||||
title: Text("Are you sure you want to clear the error log?"),
|
||||
buttons: [
|
||||
.destructive(Text("Clear Error Log"), action: self.clearLoggedErrors),
|
||||
.cancel()
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(item: self.$currentFaqUrl) { url in
|
||||
SafariView(url: url)
|
||||
}
|
||||
}
|
||||
|
||||
func searchFAQ(for error: LoggedError) {
|
||||
let baseURL = URL(string: "https://faq.altstore.io/getting-started/troubleshooting-guide")!
|
||||
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)!
|
||||
|
||||
let query = [error.domain, "\(error.code)"].joined(separator: "+")
|
||||
components.queryItems = [URLQueryItem(name: "q", value: query)]
|
||||
|
||||
self.currentFaqUrl = components.url ?? baseURL
|
||||
}
|
||||
|
||||
func clearLoggedErrors() {
|
||||
DatabaseManager.shared.purgeLoggedErrors { result in
|
||||
if case let .failure(error) = result {
|
||||
NotificationManager.shared.reportError(error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ErrorLogView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
ErrorLogView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,6 +154,12 @@ struct SettingsView: View {
|
||||
SwiftUI.Button(action: resetImageCache) {
|
||||
Text(L10n.SettingsView.resetImageCache)
|
||||
}
|
||||
|
||||
NavigationLink {
|
||||
ErrorLogView()
|
||||
} label: {
|
||||
Text("Show Error Log")
|
||||
}
|
||||
} header: {
|
||||
Text(L10n.SettingsView.debug)
|
||||
}
|
||||
@@ -193,7 +199,11 @@ struct SettingsView: View {
|
||||
|
||||
|
||||
func connectAppleID() {
|
||||
AppManager.shared.authenticate(presentingViewController: nil) { (result) in
|
||||
guard let rootViewController = UIApplication.shared.keyWindow?.rootViewController else {
|
||||
return
|
||||
}
|
||||
|
||||
AppManager.shared.authenticate(presentingViewController: rootViewController) { (result) in
|
||||
DispatchQueue.main.async {
|
||||
switch result
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user