2022-09-09 17:44:15 -05:00
//
// E r r o r L o g V i e w C o n t r o l l e r . s w i f t
// A l t S t o r e
//
// C r e a t e d b y R i l e y T e s t u t o n 9 / 6 / 2 2 .
// C o p y r i g h t © 2 0 2 2 R i l e y T e s t u t . A l l r i g h t s r e s e r v e d .
//
import UIKit
import SafariServices
import AltStoreCore
import Roxas
import Nuke
2023-01-09 16:17:00 +08:00
import QuickLook
2023-01-04 09:52:12 -05:00
final class ErrorLogViewController : UITableViewController
2022-09-09 17:44:15 -05:00
{
private lazy var dataSource = self . makeDataSource ( )
private var expandedErrorIDs = Set < NSManagedObjectID > ( )
2024-08-06 10:43:52 +09:00
private var isScrolling = false {
didSet {
guard self . isScrolling != oldValue else { return }
self . updateButtonInteractivity ( )
}
}
2022-09-09 17:44:15 -05:00
private lazy var timeFormatter : DateFormatter = {
let dateFormatter = DateFormatter ( )
dateFormatter . dateStyle = . none
dateFormatter . timeStyle = . short
return dateFormatter
} ( )
override var preferredStatusBarStyle : UIStatusBarStyle {
return . lightContent
}
override func viewDidLoad ( )
{
super . viewDidLoad ( )
self . tableView . dataSource = self . dataSource
self . tableView . prefetchDataSource = self . dataSource
}
2023-01-24 13:56:41 -06:00
override func prepare ( for segue : UIStoryboardSegue , sender : Any ? )
{
2024-08-06 10:43:52 +09:00
guard let loggedError = sender as ? LoggedError , segue . identifier = = " showErrorDetails " else { return }
2023-01-24 13:56:41 -06:00
2024-08-06 10:43:52 +09:00
let navigationController = segue . destination as ! UINavigationController
2023-01-24 13:56:41 -06:00
2024-08-06 10:43:52 +09:00
let errorDetailsViewController = navigationController . viewControllers . first as ! ErrorDetailsViewController
errorDetailsViewController . loggedError = loggedError
}
2023-01-24 13:56:41 -06:00
@IBAction private func unwindFromErrorDetails ( _ segue : UIStoryboardSegue )
{
}
2022-09-09 17:44:15 -05:00
}
private extension ErrorLogViewController
{
func makeDataSource ( ) -> RSTFetchedResultsTableViewPrefetchingDataSource < LoggedError , UIImage >
{
let fetchRequest = LoggedError . fetchRequest ( ) as NSFetchRequest < LoggedError >
fetchRequest . sortDescriptors = [ NSSortDescriptor ( keyPath : \ LoggedError . date , ascending : false ) ]
fetchRequest . returnsObjectsAsFaults = false
let fetchedResultsController = NSFetchedResultsController ( fetchRequest : fetchRequest , managedObjectContext : DatabaseManager . shared . viewContext , sectionNameKeyPath : # keyPath ( LoggedError . localizedDateString ) , cacheName : nil )
let dataSource = RSTFetchedResultsTableViewPrefetchingDataSource < LoggedError , UIImage > ( fetchedResultsController : fetchedResultsController )
dataSource . proxy = self
dataSource . rowAnimation = . fade
dataSource . cellConfigurationHandler = { [ weak self ] ( cell , loggedError , indexPath ) in
guard let self else { return }
let cell = cell as ! ErrorLogTableViewCell
cell . dateLabel . text = self . timeFormatter . string ( from : loggedError . date )
cell . errorFailureLabel . text = loggedError . localizedFailure ? ? NSLocalizedString ( " Operation Failed " , comment : " " )
2023-01-24 13:56:41 -06:00
cell . errorCodeLabel . text = loggedError . error . localizedErrorCode
2022-09-09 17:44:15 -05:00
2022-09-09 17:44:15 -05:00
let nsError = loggedError . error as NSError
let errorDescription = [ nsError . localizedDescription , nsError . localizedRecoverySuggestion ] . compactMap { $0 } . joined ( separator : " \n \n " )
cell . errorDescriptionTextView . text = errorDescription
cell . errorDescriptionTextView . maximumNumberOfLines = 5
cell . errorDescriptionTextView . isCollapsed = ! self . expandedErrorIDs . contains ( loggedError . objectID )
cell . errorDescriptionTextView . moreButton . addTarget ( self , action : #selector ( ErrorLogViewController . toggleCollapsingCell ( _ : ) ) , for : . primaryActionTriggered )
cell . appIconImageView . image = nil
cell . appIconImageView . isIndicatingActivity = true
cell . appIconImageView . layer . borderColor = UIColor . gray . cgColor
let displayScale = ( self . traitCollection . displayScale = = 0.0 ) ? 1.0 : self . traitCollection . displayScale // 0 . 0 = = " u n s p e c i f i e d "
cell . appIconImageView . layer . borderWidth = 1.0 / displayScale
if #available ( iOS 14 , * )
{
let menu = UIMenu ( title : " " , children : [
UIAction ( title : NSLocalizedString ( " Copy Error Message " , comment : " " ) , image : UIImage ( systemName : " doc.on.doc " ) ) { [ weak self ] _ in
self ? . copyErrorMessage ( for : loggedError )
} ,
UIAction ( title : NSLocalizedString ( " Copy Error Code " , comment : " " ) , image : UIImage ( systemName : " doc.on.doc " ) ) { [ weak self ] _ in
self ? . copyErrorCode ( for : loggedError )
} ,
UIAction ( title : NSLocalizedString ( " Search FAQ " , comment : " " ) , image : UIImage ( systemName : " magnifyingglass " ) ) { [ weak self ] _ in
self ? . searchFAQ ( for : loggedError )
2024-08-06 10:43:52 +09:00
} ,
UIAction ( title : NSLocalizedString ( " View More Details " , comment : " " ) , image : UIImage ( systemName : " ellipsis.circle " ) ) { [ weak self ] _ in
2024-11-10 02:54:18 +05:30
self ? . viewMoreDetails ( for : loggedError )
} ,
2022-09-09 17:44:15 -05:00
] )
cell . menuButton . menu = menu
2024-08-06 10:43:52 +09:00
cell . menuButton . showsMenuAsPrimaryAction = self . isScrolling ? false : true
cell . selectionStyle = . none
} else {
cell . menuButton . isUserInteractionEnabled = false
2022-09-09 17:44:15 -05:00
}
2024-08-06 10:43:52 +09:00
2022-09-09 17:44:15 -05:00
// I n c l u d e e r r o r D e s c r i p t i o n T e x t V i e w ' s t e x t i n c e l l s u m m a r y .
cell . accessibilityLabel = [ cell . errorFailureLabel . text , cell . dateLabel . text , cell . errorCodeLabel . text , cell . errorDescriptionTextView . text ] . compactMap { $0 } . joined ( separator : " . " )
// G r o u p a l l p a r a g r a p h s t o g e t h e r i n t o s i n g l e a c c e s s i b i l i t y e l e m e n t ( o t h e r w i s e , e a c h p a r a g r a p h i s i n d e p e n d e n t l y s e l e c t a b l e ) .
cell . errorDescriptionTextView . accessibilityLabel = cell . errorDescriptionTextView . text
}
dataSource . prefetchHandler = { ( loggedError , indexPath , completion ) in
RSTAsyncBlockOperation { ( operation ) in
loggedError . managedObjectContext ? . perform {
if let installedApp = loggedError . installedApp
{
installedApp . loadIcon { ( result ) in
switch result
{
case . failure ( let error ) : completion ( nil , error )
case . success ( let image ) : completion ( image , nil )
}
}
}
else if let storeApp = loggedError . storeApp
{
ImagePipeline . shared . loadImage ( with : storeApp . iconURL , progress : nil ) { ( response , error ) in
guard ! operation . isCancelled else { return operation . finish ( ) }
if let image = response ? . image
{
completion ( image , nil )
}
else
{
completion ( nil , error )
}
}
}
else
{
completion ( nil , nil )
}
}
}
}
dataSource . prefetchCompletionHandler = { ( cell , image , indexPath , error ) in
let cell = cell as ! ErrorLogTableViewCell
cell . appIconImageView . image = image
cell . appIconImageView . isIndicatingActivity = false
}
let placeholderView = RSTPlaceholderView ( )
placeholderView . textLabel . text = NSLocalizedString ( " No Errors " , comment : " " )
placeholderView . detailTextLabel . text = NSLocalizedString ( " Errors that occur when sideloading or refreshing apps will appear here. " , comment : " " )
dataSource . placeholderView = placeholderView
return dataSource
}
}
private extension ErrorLogViewController
{
@IBAction func toggleCollapsingCell ( _ sender : UIButton )
{
let point = self . tableView . convert ( sender . center , from : sender . superview )
guard let indexPath = self . tableView . indexPathForRow ( at : point ) , let cell = self . tableView . cellForRow ( at : indexPath ) as ? ErrorLogTableViewCell else { return }
let loggedError = self . dataSource . item ( at : indexPath )
if cell . errorDescriptionTextView . isCollapsed
{
self . expandedErrorIDs . remove ( loggedError . objectID )
}
else
{
self . expandedErrorIDs . insert ( loggedError . objectID )
}
self . tableView . performBatchUpdates {
cell . layoutIfNeeded ( )
}
}
2023-01-09 16:17:00 +08:00
@IBAction func showMinimuxerLogs ( _ sender : UIBarButtonItem )
{
// S h o w m i n i m u x e r . l o g
let previewController = QLPreviewController ( )
previewController . dataSource = self
let navigationController = UINavigationController ( rootViewController : previewController )
present ( navigationController , animated : true , completion : nil )
}
2022-09-09 17:44:15 -05:00
@IBAction func clearLoggedErrors ( _ sender : UIBarButtonItem )
{
let alertController = UIAlertController ( title : NSLocalizedString ( " Are you sure you want to clear the error log? " , comment : " " ) , message : nil , preferredStyle : . actionSheet )
alertController . popoverPresentationController ? . barButtonItem = sender
alertController . addAction ( . cancel )
alertController . addAction ( UIAlertAction ( title : NSLocalizedString ( " Clear Error Log " , comment : " " ) , style : . destructive ) { _ in
self . clearLoggedErrors ( )
} )
self . present ( alertController , animated : true )
}
func clearLoggedErrors ( )
{
DatabaseManager . shared . purgeLoggedErrors { result in
do
{
try result . get ( )
}
catch
{
DispatchQueue . main . async {
let alertController = UIAlertController ( title : NSLocalizedString ( " Failed to Clear Error Log " , comment : " " ) , message : error . localizedDescription , preferredStyle : . alert )
alertController . addAction ( . ok )
self . present ( alertController , animated : true )
}
}
}
}
func copyErrorMessage ( for loggedError : LoggedError )
{
let nsError = loggedError . error as NSError
let errorMessage = [ nsError . localizedDescription , nsError . localizedRecoverySuggestion ] . compactMap { $0 } . joined ( separator : " \n \n " )
UIPasteboard . general . string = errorMessage
}
func copyErrorCode ( for loggedError : LoggedError )
{
let errorCode = loggedError . error . localizedErrorCode
UIPasteboard . general . string = errorCode
}
func searchFAQ ( for loggedError : LoggedError )
{
2022-10-26 18:09:15 -05:00
let baseURL = URL ( string : " https://faq.altstore.io/getting-started/error-codes " ) !
2022-09-09 17:44:15 -05:00
var components = URLComponents ( url : baseURL , resolvingAgainstBaseURL : false ) !
2023-01-24 13:56:41 -06:00
let query = [ loggedError . domain , " \( loggedError . error . displayCode ) " ] . joined ( separator : " + " )
2022-09-09 17:44:15 -05:00
components . queryItems = [ URLQueryItem ( name : " q " , value : query ) ]
let safariViewController = SFSafariViewController ( url : components . url ? ? baseURL )
safariViewController . preferredControlTintColor = . altPrimary
self . present ( safariViewController , animated : true )
}
2024-08-06 10:43:52 +09:00
func viewMoreDetails ( for loggedError : LoggedError ) {
self . performSegue ( withIdentifier : " showErrorDetails " , sender : loggedError )
}
2022-09-09 17:44:15 -05:00
}
extension ErrorLogViewController
{
override func tableView ( _ tableView : UITableView , didSelectRowAt indexPath : IndexPath )
{
2024-08-06 10:43:52 +09:00
guard # unavailable ( iOS 14 ) else { return }
2022-09-09 17:44:15 -05:00
let loggedError = self . dataSource . item ( at : indexPath )
let alertController = UIAlertController ( title : nil , message : nil , preferredStyle : . actionSheet )
alertController . addAction ( UIAlertAction ( title : UIAlertAction . cancel . title , style : UIAlertAction . cancel . style ) { _ in
tableView . deselectRow ( at : indexPath , animated : true )
} )
alertController . addAction ( UIAlertAction ( title : NSLocalizedString ( " Copy Error Message " , comment : " " ) , style : . default ) { [ weak self ] _ in
self ? . copyErrorMessage ( for : loggedError )
tableView . deselectRow ( at : indexPath , animated : true )
} )
alertController . addAction ( UIAlertAction ( title : NSLocalizedString ( " Copy Error Code " , comment : " " ) , style : . default ) { [ weak self ] _ in
self ? . copyErrorCode ( for : loggedError )
tableView . deselectRow ( at : indexPath , animated : true )
} )
alertController . addAction ( UIAlertAction ( title : NSLocalizedString ( " Search FAQ " , comment : " " ) , style : . default ) { [ weak self ] _ in
self ? . searchFAQ ( for : loggedError )
tableView . deselectRow ( at : indexPath , animated : true )
} )
self . present ( alertController , animated : true )
}
override func tableView ( _ tableView : UITableView , trailingSwipeActionsConfigurationForRowAt indexPath : IndexPath ) -> UISwipeActionsConfiguration ?
{
let deleteAction = UIContextualAction ( style : . destructive , title : NSLocalizedString ( " Delete " , comment : " " ) ) { _ , _ , completion in
let loggedError = self . dataSource . item ( at : indexPath )
DatabaseManager . shared . persistentContainer . performBackgroundTask { context in
do
{
let loggedError = context . object ( with : loggedError . objectID ) as ! LoggedError
context . delete ( loggedError )
try context . save ( )
completion ( true )
}
catch
{
print ( " [ALTLog] Failed to delete LoggedError \( loggedError . objectID ) : " , error )
completion ( false )
}
}
}
let configuration = UISwipeActionsConfiguration ( actions : [ deleteAction ] )
configuration . performsFirstActionWithFullSwipe = false
return configuration
}
override func tableView ( _ tableView : UITableView , titleForHeaderInSection section : Int ) -> String ?
{
let indexPath = IndexPath ( row : 0 , section : section )
let loggedError = self . dataSource . item ( at : indexPath )
2022-09-27 15:41:41 -05:00
if Calendar . current . isDateInToday ( loggedError . date )
{
return NSLocalizedString ( " Today " , comment : " " )
}
else
{
return loggedError . localizedDateString
}
2022-09-09 17:44:15 -05:00
}
}
2023-01-09 16:17:00 +08:00
extension ErrorLogViewController : QLPreviewControllerDataSource {
func numberOfPreviewItems ( in controller : QLPreviewController ) -> Int {
return 1
}
func previewController ( _ controller : QLPreviewController , previewItemAt index : Int ) -> QLPreviewItem {
let fileURL = FileManager . default . documentsDirectory . appendingPathComponent ( " minimuxer.log " )
return fileURL as QLPreviewItem
}
}
2024-08-06 10:43:52 +09:00
extension ErrorLogViewController
{
override func scrollViewWillBeginDragging ( _ scrollView : UIScrollView )
{
self . isScrolling = true
}
override func scrollViewDidEndDecelerating ( _ scrollView : UIScrollView )
{
self . isScrolling = false
}
override func scrollViewDidEndDragging ( _ scrollView : UIScrollView , willDecelerate decelerate : Bool )
{
guard ! decelerate else { return }
self . isScrolling = false
}
private func updateButtonInteractivity ( )
{
guard #available ( iOS 14 , * ) else { return }
for case let cell as ErrorLogTableViewCell in self . tableView . visibleCells
{
cell . menuButton . showsMenuAsPrimaryAction = self . isScrolling ? false : true
}
}
}