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
2023-12-08 18:15:48 -06:00
import QuickLook
2022-09-09 17:44:15 -05:00
import AltStoreCore
import Roxas
import Nuke
2023-01-09 16:17:00 +08:00
import QuickLook
2024-12-29 03:12:59 +05:30
import SwiftUI
2023-01-09 16:17:00 +08:00
2024-12-07 17:45:09 +05:30
final class ErrorLogViewController : UITableViewController , QLPreviewControllerDelegate
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 ( )
}
}
2024-12-07 17:45:09 +05:30
2022-09-09 17:44:15 -05:00
private lazy var timeFormatter : DateFormatter = {
let dateFormatter = DateFormatter ( )
dateFormatter . dateStyle = . none
dateFormatter . timeStyle = . short
return dateFormatter
} ( )
2023-10-18 14:26:37 -05:00
@IBOutlet private var exportLogButton : UIBarButtonItem !
@IBOutlet private var clearLogButton : UIBarButtonItem !
2023-12-08 18:15:48 -06:00
private var _exportedLogURL : URL ?
2022-09-09 17:44:15 -05:00
override var preferredStatusBarStyle : UIStatusBarStyle {
return . lightContent
}
override func viewDidLoad ( )
{
super . viewDidLoad ( )
self . tableView . dataSource = self . dataSource
self . tableView . prefetchDataSource = self . dataSource
2023-10-18 14:26:37 -05:00
self . exportLogButton . activityIndicatorView . color = . white
if # unavailable ( iOS 15 )
{
// A s s i g n j u s t c l e a r L o g B u t t o n t o h i d e e x p o r t b u t t o n .
self . navigationItem . rightBarButtonItems = [ self . clearLogButton ]
}
2022-09-09 17:44:15 -05:00
}
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
2023-03-02 16:53:36 -06:00
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 )
} ,
UIAction ( title : NSLocalizedString ( " View More Details " , comment : " " ) , image : UIImage ( systemName : " ellipsis.circle " ) ) { [ weak self ] _ in
self ? . viewMoreDetails ( for : loggedError )
} ,
] )
2022-09-09 17:44:15 -05:00
cell . menuButton . menu = menu
2024-12-07 17:45:09 +05:30
if self . isScrolling
{
cell . menuButton . showsMenuAsPrimaryAction = false
2022-09-09 17:44:15 -05:00
}
2024-12-07 17:45:09 +05:30
else
{
cell . menuButton . showsMenuAsPrimaryAction = true
}
cell . selectionStyle = . none
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
{
2023-03-01 15:00:27 -06:00
ImagePipeline . shared . loadImage ( with : storeApp . iconURL , progress : nil ) { result in
2022-09-09 17:44:15 -05:00
guard ! operation . isCancelled else { return operation . finish ( ) }
2023-03-01 15:00:27 -06:00
switch result
2022-09-09 17:44:15 -05:00
{
2023-03-01 15:00:27 -06:00
case . success ( let response ) : completion ( response . image , nil )
case . failure ( let error ) : completion ( nil , error )
2022-09-09 17:44:15 -05:00
}
}
}
else
{
2022-11-16 18:25:55 -06:00
// I n s t a l l e d A p p w a s p r o b a b l y d e l e t e d .
2022-09-09 17:44:15 -05:00
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 ( )
}
}
2024-12-29 03:12:59 +05:30
enum LogView : String {
case consoleLog = " console-log "
case minimuxerLog = " minimuxer-log "
// / / T h i s c l a s s w i l l m a n a g e t h e Q L P r e v i e w C o n t r o l l e r a n d t h e t i m e r .
// p r i v a t e c l a s s L o g V i e w M a n a g e r {
// v a r p r e v i e w C o n t r o l l e r : Q L P r e v i e w C o n t r o l l e r
// v a r r e f r e s h T i m e r : T i m e r ?
//
// i n i t ( p r e v i e w C o n t r o l l e r : Q L P r e v i e w C o n t r o l l e r ) {
// s e l f . p r e v i e w C o n t r o l l e r = p r e v i e w C o n t r o l l e r
// }
//
// / / S t a r t r e f r e s h i n g t h e p r e v i e w c o n t r o l l e r e v e r y s e c o n d
// f u n c s t a r t R e f r e s h i n g ( ) {
// r e f r e s h T i m e r = T i m e r . s c h e d u l e d T i m e r ( t i m e I n t e r v a l : 1 . 0 , t a r g e t : s e l f , s e l e c t o r : # s e l e c t o r ( r e f r e s h P r e v i e w ) , u s e r I n f o : n i l , r e p e a t s : t r u e )
// }
//
// @ o b j c p r i v a t e f u n c r e f r e s h P r e v i e w ( ) {
// p r e v i e w C o n t r o l l e r . r e l o a d D a t a ( )
// }
//
// / / S t o p t h e t i m e r t o p r e v e n t l e a k s
// f u n c s t o p R e f r e s h i n g ( ) {
// r e f r e s h T i m e r ? . i n v a l i d a t e ( )
// r e f r e s h T i m e r = n i l
// }
// }
//
// / / M e t h o d t o g e t t h e Q L P r e v i e w C o n t r o l l e r f o r t h i s l o g t y p e
// f u n c g e t V i e w C o n t r o l l e r ( _ d a t a S o u r c e : Q L P r e v i e w C o n t r o l l e r D a t a S o u r c e ) - > Q L P r e v i e w C o n t r o l l e r {
// l e t p r e v i e w C o n t r o l l e r = Q L P r e v i e w C o n t r o l l e r ( )
// p r e v i e w C o n t r o l l e r . r e s t o r a t i o n I d e n t i f i e r = s e l f . r a w V a l u e
// p r e v i e w C o n t r o l l e r . d a t a S o u r c e = d a t a S o u r c e
//
// / / C r e a t e L o g V i e w M a n a g e r a n d s t a r t r e f r e s h i n g
// l e t m a n a g e r = L o g V i e w M a n a g e r ( p r e v i e w C o n t r o l l e r : p r e v i e w C o n t r o l l e r )
// m a n a g e r . s t a r t R e f r e s h i n g ( )
//
// r e t u r n p r e v i e w C o n t r o l l e r
// }
// T h i s c l a s s w i l l m a n a g e t h e Q L P r e v i e w C o n t r o l l e r a n d t h e t i m e r .
private class LogViewManager {
var previewController : QLPreviewController
var refreshTimer : Timer ?
var logView : LogView
init ( previewController : QLPreviewController , logView : LogView ) {
self . previewController = previewController
self . logView = logView
}
// S t a r t r e f r e s h i n g t h e p r e v i e w c o n t r o l l e r e v e r y s e c o n d
func startRefreshing ( ) {
refreshTimer = Timer . scheduledTimer ( timeInterval : 1.0 , target : self , selector : #selector ( refreshPreview ) , userInfo : nil , repeats : true )
}
@objc private func refreshPreview ( ) {
previewController . reloadData ( )
}
// S t o p t h e t i m e r t o p r e v e n t l e a k s
func stopRefreshing ( ) {
refreshTimer ? . invalidate ( )
refreshTimer = nil
}
func updateLogPath ( ) {
// F o r c e t h e Q L P r e v i e w C o n t r o l l e r t o r e l o a d b y c h a n g i n g t h e f i l e p a t h
previewController . reloadData ( )
}
}
// M e t h o d t o g e t t h e Q L P r e v i e w C o n t r o l l e r f o r t h i s l o g t y p e
func getViewController ( _ dataSource : QLPreviewControllerDataSource ) -> QLPreviewController {
let previewController = QLPreviewController ( )
previewController . restorationIdentifier = self . rawValue
previewController . dataSource = dataSource
// C r e a t e L o g V i e w M a n a g e r a n d s t a r t r e f r e s h i n g
let manager = LogViewManager ( previewController : previewController , logView : self )
manager . startRefreshing ( )
return previewController
}
func getLogPath ( ) -> URL {
switch self {
case . consoleLog :
let appDelegate = UIApplication . shared . delegate as ! AppDelegate
return appDelegate . consoleLog . logFileURL
case . minimuxerLog :
return FileManager . default . documentsDirectory . appendingPathComponent ( " minimuxer.log " )
}
}
2023-01-09 16:17:00 +08:00
}
2024-12-29 03:12:59 +05:30
@IBAction func showMinimuxerLogs ( _ sender : UIBarButtonItem ) {
// C r e a t e t h e S w i f t U I C o n s o l e L o g V i e w w i t h t h e U R L
let consoleLogView = ConsoleLogView ( logURL : ( UIApplication . shared . delegate as ! AppDelegate ) . consoleLog . logFileURL )
// C r e a t e t h e U I H o s t i n g C o n t r o l l e r
let consoleLogController = UIHostingController ( rootView : consoleLogView )
// C o n f i g u r e t h e b o t t o m s h e e t p r e s e n t a t i o n
consoleLogController . modalPresentationStyle = . pageSheet
if let sheet = consoleLogController . sheetPresentationController {
sheet . detents = [ . medium ( ) , . large ( ) ] // Y o u c a n a d j u s t t h e s i z e o f t h e s h e e t ( m e d i u m / l a r g e )
sheet . prefersGrabberVisible = true // O p t i o n a l : S h o w s a g r a b b e r a t t h e t o p o f t h e s h e e t
sheet . selectedDetentIdentifier = . large // D e f a u l t s i z e w h e n p r e s e n t e d
}
// P r e s e n t t h e b o t t o m s h e e t
present ( consoleLogController , 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 )
}
2023-10-18 14:26:37 -05:00
@ available ( iOS 15 , * )
@IBAction func exportDetailedLog ( _ sender : UIBarButtonItem )
{
self . exportLogButton . isIndicatingActivity = true
Task < Void , Never > . detached ( priority : . userInitiated ) {
do
{
let store = try OSLogStore ( scope : . currentProcessIdentifier )
// A l l l o g s s i n c e t h e a p p l a u n c h e d .
let position = store . position ( timeIntervalSinceLatestBoot : 0 )
2024-12-29 03:12:59 +05:30
// l e t p r e d i c a t e = N S P r e d i c a t e ( f o r m a t : " s u b s y s t e m = = % @ " , L o g g e r . a l t s t o r e S u b s y s t e m )
//
// l e t e n t r i e s = t r y s t o r e . g e t E n t r i e s ( a t : p o s i t i o n , m a t c h i n g : p r e d i c a t e )
// . c o m p a c t M a p { $ 0 a s ? O S L o g E n t r y L o g }
// . m a p { " [ \ ( $ 0 . d a t e . f o r m a t t e d ( ) ) ] [ \ ( $ 0 . c a t e g o r y ) ] [ \ ( $ 0 . l e v e l . l o c a l i z e d N a m e ) ] \ ( $ 0 . c o m p o s e d M e s s a g e ) " }
//
// R e m o v e t h e p r e d i c a t e t o g e t a l l l o g e n t r i e s
// l e t e n t r i e s = t r y s t o r e . g e t E n t r i e s ( a t : p o s i t i o n )
// . c o m p a c t M a p { $ 0 a s ? O S L o g E n t r y L o g }
// . m a p { " [ \ ( $ 0 . d a t e . f o r m a t t e d ( ) ) ] [ \ ( $ 0 . c a t e g o r y ) ] [ \ ( $ 0 . l e v e l . l o c a l i z e d N a m e ) ] \ ( $ 0 . c o m p o s e d M e s s a g e ) " }
let entries = try store . getEntries ( at : position )
// l e t o u t p u t T e x t = e n t r i e s . j o i n e d ( s e p a r a t o r : " \ n " )
let outputText = entries . map { $0 . description } . joined ( separator : " \n " )
2023-10-18 14:26:37 -05:00
let outputDirectory = FileManager . default . uniqueTemporaryURL ( )
try FileManager . default . createDirectory ( at : outputDirectory , withIntermediateDirectories : true )
2023-12-08 18:15:48 -06:00
let outputURL = outputDirectory . appendingPathComponent ( " altstore.log " )
2023-10-18 14:26:37 -05:00
try outputText . write ( to : outputURL , atomically : true , encoding : . utf8 )
2023-12-08 18:15:48 -06:00
await MainActor . run {
self . _exportedLogURL = outputURL
let previewController = QLPreviewController ( )
previewController . delegate = self
previewController . dataSource = self
previewController . view . tintColor = . altPrimary
self . present ( previewController , animated : true )
2023-10-18 14:26:37 -05:00
}
}
catch
{
Logger . main . error ( " Failed to export OSLog entries. \( error . localizedDescription , privacy : . public ) " )
await MainActor . run {
let alertController = UIAlertController ( title : NSLocalizedString ( " Unable to Export Detailed Log " , comment : " " ) , message : error . localizedDescription , preferredStyle : . alert )
alertController . addAction ( . ok )
self . present ( alertController , animated : true )
}
}
await MainActor . run {
self . exportLogButton . isIndicatingActivity = false
}
}
}
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
}
2024-12-29 03:12:59 +05:30
func previewController ( _ controller : QLPreviewController , previewItemAt index : Int ) -> QLPreviewItem
{
guard let identifier = controller . restorationIdentifier ,
let logView = LogView ( rawValue : identifier ) else {
fatalError ( " Invalid restorationIdentifier " )
}
return logView . getLogPath ( ) as QLPreviewItem
2023-01-09 16:17:00 +08:00
}
}
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 ( )
{
for case let cell as ErrorLogTableViewCell in self . tableView . visibleCells
{
2024-12-07 17:45:09 +05:30
if self . isScrolling
{
cell . menuButton . showsMenuAsPrimaryAction = false
}
else
{
cell . menuButton . showsMenuAsPrimaryAction = true
}
2024-08-06 10:43:52 +09:00
}
}
}
2023-12-08 18:15:48 -06:00
2024-12-07 17:45:09 +05:30
// e x t e n s i o n E r r o r L o g V i e w C o n t r o l l e r : Q L P r e v i e w C o n t r o l l e r D a t a S o u r c e , Q L P r e v i e w C o n t r o l l e r D e l e g a t e
// {
// f u n c n u m b e r O f P r e v i e w I t e m s ( i n c o n t r o l l e r : Q L P r e v i e w C o n t r o l l e r ) - > I n t
// {
// r e t u r n 1
// }
//
// f u n c p r e v i e w C o n t r o l l e r ( _ c o n t r o l l e r : Q L P r e v i e w C o n t r o l l e r , p r e v i e w I t e m A t i n d e x : I n t ) - > Q L P r e v i e w I t e m
// {
// r e t u r n ( _ e x p o r t e d L o g U R L a s ? N S U R L ) ? ? N S U R L ( )
// }
//
// f u n c p r e v i e w C o n t r o l l e r D i d D i s m i s s ( _ c o n t r o l l e r : Q L P r e v i e w C o n t r o l l e r )
// {
// g u a r d l e t e x p o r t e d L o g U R L = _ e x p o r t e d L o g U R L e l s e { r e t u r n }
//
// l e t p a r e n t D i r e c t o r y = e x p o r t e d L o g U R L . d e l e t i n g L a s t P a t h C o m p o n e n t ( )
//
// d o
// {
// t r y F i l e M a n a g e r . d e f a u l t . r e m o v e I t e m ( a t : p a r e n t D i r e c t o r y )
// }
// c a t c h
// {
// L o g g e r . m a i n . e r r o r ( " F a i l e d t o r e m o v e t e m p o r a r y l o g d i r e c t o r y \ ( p a r e n t D i r e c t o r y . l a s t P a t h C o m p o n e n t , p r i v a c y : . p u b l i c ) . \ ( e r r o r . l o c a l i z e d D e s c r i p t i o n , p r i v a c y : . p u b l i c ) " )
// }
//
// _ e x p o r t e d L o g U R L = n i l
// }
// }