From 07159b0ea62bc5f3def168401b208d671797f500 Mon Sep 17 00:00:00 2001 From: Fabian Thies Date: Sat, 4 Feb 2023 13:07:04 +0100 Subject: [PATCH] [ADD] Error log view --- AltStore.xcodeproj/project.pbxproj | 12 ++ AltStore/Helper/DateFormatterHelper.swift | 16 ++ .../View Components/ModalNavigationLink.swift | 33 ++++ .../FilePreviewView.swift | 48 +++++ AltStore/Views/Settings/ErrorLogView.swift | 169 ++++++++++++++++++ AltStore/Views/Settings/SettingsView.swift | 12 +- 6 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 AltStore/View Components/ModalNavigationLink.swift create mode 100644 AltStore/View Extensions/UIView Representables/FilePreviewView.swift create mode 100644 AltStore/Views/Settings/ErrorLogView.swift diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index f04d4866..b6666ba3 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -37,6 +37,9 @@ 1F180F94298E7A2500D1C98B /* Source+Trusted.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F180F93298E7A2500D1C98B /* Source+Trusted.swift */; }; 1F2EF787297C4D40002FD839 /* LicensesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2EF786297C4D40002FD839 /* LicensesView.swift */; }; 1F44634529744E570070E514 /* HintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F44634429744E570070E514 /* HintView.swift */; }; + 1F545E83298D79E400589F68 /* ErrorLogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F545E82298D79E400589F68 /* ErrorLogView.swift */; }; + 1F545E85298D84CF00589F68 /* FilePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F545E84298D84CF00589F68 /* FilePreviewView.swift */; }; + 1F545E87298D86D800589F68 /* ModalNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F545E86298D86D800589F68 /* ModalNavigationLink.swift */; }; 1F5DF9D82974426300DDAA47 /* AppScreenshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F5DF9D72974426300DDAA47 /* AppScreenshot.swift */; }; 1F6284D5295209DA0060AAD8 /* AppAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6284D4295209DA0060AAD8 /* AppAction.swift */; }; 1F6284D7295218980060AAD8 /* DocumentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F6284D6295218980060AAD8 /* DocumentPicker.swift */; }; @@ -582,6 +585,9 @@ 1F180F93298E7A2500D1C98B /* Source+Trusted.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Source+Trusted.swift"; sourceTree = ""; }; 1F2EF786297C4D40002FD839 /* LicensesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicensesView.swift; sourceTree = ""; }; 1F44634429744E570070E514 /* HintView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HintView.swift; sourceTree = ""; }; + 1F545E82298D79E400589F68 /* ErrorLogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLogView.swift; sourceTree = ""; }; + 1F545E84298D84CF00589F68 /* FilePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewView.swift; sourceTree = ""; }; + 1F545E86298D86D800589F68 /* ModalNavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalNavigationLink.swift; sourceTree = ""; }; 1F5DF9D72974426300DDAA47 /* AppScreenshot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppScreenshot.swift; sourceTree = ""; }; 1F6284D4295209DA0060AAD8 /* AppAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAction.swift; sourceTree = ""; }; 1F6284D6295218980060AAD8 /* DocumentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentPicker.swift; sourceTree = ""; }; @@ -1109,6 +1115,7 @@ 1FB96FCE292BBBC9007E68D1 /* SiriShortcutSetupView.swift */, 1F66F5B92938CA5700A910CA /* VisualEffectView.swift */, 1F6284D6295218980060AAD8 /* DocumentPicker.swift */, + 1F545E84298D84CF00589F68 /* FilePreviewView.swift */, ); path = "UIView Representables"; sourceTree = ""; @@ -1193,6 +1200,7 @@ 1FAFC5C02927E13C00B8D837 /* SettingsView.swift */, 1F0DD83E29367F6C007608A4 /* ConnectAppleIDView.swift */, 1F2EF786297C4D40002FD839 /* LicensesView.swift */, + 1F545E82298D79E400589F68 /* ErrorLogView.swift */, ); path = Settings; sourceTree = ""; @@ -1218,6 +1226,7 @@ 1FB96FBD292A20E5007E68D1 /* ObservableScrollView.swift */, 1F6E08E529280F4B005059C0 /* RatingStars.swift */, 1F0DD8422936B0F9007608A4 /* RoundedTextField.swift */, + 1F545E86298D86D800589F68 /* ModalNavigationLink.swift */, ); path = "View Components"; sourceTree = ""; @@ -2745,6 +2754,7 @@ BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */, 1F6E08DA292806E0005059C0 /* AppRowView.swift in Sources */, 1FB96FC3292A6D7E007E68D1 /* DateFormatterHelper.swift in Sources */, + 1F545E87298D86D800589F68 /* ModalNavigationLink.swift in Sources */, 1F943C6D2927F90400ABE095 /* NewsViewModel.swift in Sources */, 1FB96FF3292D0539007E68D1 /* PillButtonProgressViewStyle.swift in Sources */, 1F6E08E829282174005059C0 /* ConfirmAddSourceView.swift in Sources */, @@ -2812,6 +2822,7 @@ D5F2F6A92720B7C20081CCF5 /* PatchViewController.swift in Sources */, B39F16132918D7C5002E9404 /* Consts.swift in Sources */, 1F0DD8212933B749007608A4 /* AppPermissionsGrid.swift in Sources */, + 1F545E83298D79E400589F68 /* ErrorLogView.swift in Sources */, 1F0DD8452936B3FE007608A4 /* FilledButtonStyle.swift in Sources */, BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */, BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */, @@ -2848,6 +2859,7 @@ BFF00D342501BDCF00746320 /* IntentHandler.swift in Sources */, 1F0DD81C2932D2FF007608A4 /* AppScreenshotsScrollView.swift in Sources */, BFDBBD80246CB84F004ED2F3 /* RemoveAppBackupOperation.swift in Sources */, + 1F545E85298D84CF00589F68 /* FilePreviewView.swift in Sources */, 1FB96FCF292BBBCA007E68D1 /* SiriShortcutSetupView.swift in Sources */, 1F2EF787297C4D40002FD839 /* LicensesView.swift in Sources */, BFF0B6942321CB85007A79E1 /* AuthenticationViewController.swift in Sources */, diff --git a/AltStore/Helper/DateFormatterHelper.swift b/AltStore/Helper/DateFormatterHelper.swift index 8478bea9..b099e464 100644 --- a/AltStore/Helper/DateFormatterHelper.swift +++ b/AltStore/Helper/DateFormatterHelper.swift @@ -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) + } } diff --git a/AltStore/View Components/ModalNavigationLink.swift b/AltStore/View Components/ModalNavigationLink.swift new file mode 100644 index 00000000..e8a9d621 --- /dev/null +++ b/AltStore/View Components/ModalNavigationLink.swift @@ -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: 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() +// } +//} diff --git a/AltStore/View Extensions/UIView Representables/FilePreviewView.swift b/AltStore/View Extensions/UIView Representables/FilePreviewView.swift new file mode 100644 index 00000000..0e618ae1 --- /dev/null +++ b/AltStore/View Extensions/UIView Representables/FilePreviewView.swift @@ -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 + } + } +} diff --git a/AltStore/Views/Settings/ErrorLogView.swift b/AltStore/Views/Settings/ErrorLogView.swift new file mode 100644 index 00000000..568a9e10 --- /dev/null +++ b/AltStore/Views/Settings/ErrorLogView.swift @@ -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 + + 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() + } + } +} diff --git a/AltStore/Views/Settings/SettingsView.swift b/AltStore/Views/Settings/SettingsView.swift index f7998a26..88143292 100644 --- a/AltStore/Views/Settings/SettingsView.swift +++ b/AltStore/Views/Settings/SettingsView.swift @@ -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 {