diff --git a/Delta/Base.lproj/Settings.storyboard b/Delta/Base.lproj/Settings.storyboard index 3f986ce..41c89ba 100644 --- a/Delta/Base.lproj/Settings.storyboard +++ b/Delta/Base.lproj/Settings.storyboard @@ -19,7 +19,7 @@ - + @@ -505,11 +505,38 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -528,7 +555,7 @@ - + @@ -548,7 +575,7 @@ - + @@ -571,7 +598,7 @@ - + @@ -594,7 +621,7 @@ - + @@ -617,7 +644,7 @@ - + @@ -640,7 +667,7 @@ - + @@ -663,7 +690,7 @@ - + @@ -679,7 +706,7 @@ - + @@ -717,6 +744,7 @@ + diff --git a/Delta/Settings/SettingsViewController.swift b/Delta/Settings/SettingsViewController.swift index b83b4ed..c0f2ba9 100644 --- a/Delta/Settings/SettingsViewController.swift +++ b/Delta/Settings/SettingsViewController.swift @@ -8,8 +8,10 @@ import UIKit import SafariServices +import QuickLook import DeltaCore +import Harmony import Roxas @@ -43,6 +45,12 @@ private extension SettingsViewController case status } + enum AdvancedRow: Int, CaseIterable + { + case exportLog + case experimentalFeatures + } + enum CreditsRow: Int, CaseIterable { case riley @@ -68,6 +76,7 @@ class SettingsViewController: UITableViewController @IBOutlet private var versionLabel: UILabel! @IBOutlet private var syncingServiceLabel: UILabel! + @IBOutlet private var exportLogActivityIndicatorView: UIActivityIndicatorView! private var selectionFeedbackGenerator: UISelectionFeedbackGenerator? @@ -75,6 +84,8 @@ class SettingsViewController: UITableViewController private var syncingConflictsCount = 0 + private var _exportedLogURL: URL? + required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) @@ -196,6 +207,15 @@ private extension SettingsViewController { switch section { + case .advanced: + guard #unavailable(iOS 15) else { return false } + + #if BETA + return false + #else + return true + #endif + case .hapticTouch: if #available(iOS 13, *) { @@ -294,6 +314,55 @@ private extension SettingsViewController let hostingController = ExperimentalFeaturesView.makeViewController() self.navigationController?.pushViewController(hostingController, animated: true) } + + @available(iOS 15, *) + func exportErrorLog() + { + self.exportLogActivityIndicatorView.startAnimating() + + if let indexPath = self.tableView.indexPathForSelectedRow + { + self.tableView.deselectRow(at: indexPath, animated: true) + } + + Task.detached(priority: .userInitiated) { + do + { + let store = try OSLogStore(scope: .currentProcessIdentifier) + + // All logs since the app launched. + let position = store.position(timeIntervalSinceLatestBoot: 0) + let predicate = NSPredicate(format: "subsystem IN %@", [Logger.deltaSubsystem, Logger.harmonySubsystem]) + + let entries = try store.getEntries(at: position, matching: predicate) + .compactMap { $0 as? OSLogEntryLog } + .map { "[\($0.date.formatted())] [\($0.category)] [\($0.level.localizedName)] \($0.composedMessage)" } + + let outputText = entries.joined(separator: "\n") + + let outputDirectory = FileManager.default.uniqueTemporaryURL() + try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true) + + let outputURL = outputDirectory.appendingPathComponent("delta.log") + try outputText.write(to: outputURL, atomically: true, encoding: .utf8) + + await MainActor.run { + self._exportedLogURL = outputURL + + let previewController = QLPreviewController() + previewController.delegate = self + previewController.dataSource = self + self.present(previewController, animated: true) + } + } + catch + { + print("Failed to export Harmony logs.", error) + } + + await self.exportLogActivityIndicatorView.stopAnimating() + } + } } private extension SettingsViewController @@ -341,6 +410,9 @@ extension SettingsViewController case .controllers: return 4 case .controllerSkins: return System.registeredSystems.count case .syncing: return SyncManager.shared.coordinator?.account == nil ? 1 : super.tableView(tableView, numberOfRowsInSection: sectionIndex) + #if !BETA + case .advanced: return 1 + #endif default: if isSectionHidden(section) { @@ -410,7 +482,17 @@ extension SettingsViewController case .controllerSkins: self.performSegue(withIdentifier: Segue.controllerSkins.rawValue, sender: cell) case .cores: self.performSegue(withIdentifier: Segue.dsSettings.rawValue, sender: cell) case .controllerOpacity, .gameAudio, .hapticFeedback, .hapticTouch, .syncing: break - case .advanced: self.showExperimentalFeatures() + case .advanced: + let row = AdvancedRow(rawValue: indexPath.row)! + switch row + { + case .exportLog: + guard #available(iOS 15, *) else { return } + self.exportErrorLog() + + case .experimentalFeatures: self.showExperimentalFeatures() + } + case .patreon: let patreonURL = URL(string: "altstore://patreon")! @@ -449,6 +531,17 @@ extension SettingsViewController primary: switch Section(rawValue: indexPath.section)! { + case .advanced: + let row = AdvancedRow(rawValue: indexPath.row)! + switch row + { + case .exportLog: + guard #unavailable(iOS 15) else { break } + return 0.0 + + default: break + } + case .credits: let row = CreditsRow(rawValue: indexPath.row)! switch row @@ -492,14 +585,15 @@ extension SettingsViewController override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { let section = Section(rawValue: section)! + guard !isSectionHidden(section) else { return nil } - if isSectionHidden(section) + switch section { - return nil - } - else - { - return super.tableView(tableView, titleForFooterInSection: section.rawValue) + #if !BETA + case .advanced: return nil + #endif + + default: return super.tableView(tableView, titleForFooterInSection: section.rawValue) } } @@ -531,3 +625,26 @@ extension SettingsViewController } } } + +extension SettingsViewController: QLPreviewControllerDataSource, QLPreviewControllerDelegate +{ + func numberOfPreviewItems(in controller: QLPreviewController) -> Int + { + return 1 + } + + func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem + { + return (_exportedLogURL as? NSURL) ?? NSURL() + } + + func previewControllerDidDismiss(_ controller: QLPreviewController) + { + guard let exportedLogURL = _exportedLogURL else { return } + + let parentDirectory = exportedLogURL.deletingLastPathComponent() + try? FileManager.default.removeItem(at: parentDirectory) + + _exportedLogURL = nil + } +}