Merge branch 'develop' into users/junepark678/altsign-fixes

This commit is contained in:
Spidy123222
2025-01-20 22:27:31 -08:00
committed by GitHub
61 changed files with 3642 additions and 950 deletions

View File

@@ -2,14 +2,22 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- <key>com.apple.security.files.user-selected.read-write</key>
<array>
<string></string>
</array>
<key>com.apple.developer.applesignin</key>
<array>
<string></string>
</array> -->
<key>com.apple.developer.kernel.extended-virtual-addressing</key>
<true/>
<key>com.apple.developer.kernel.increased-debugging-memory-limit</key>
<true/>
<key>com.apple.developer.kernel.increased-memory-limit</key>
<true/>
<key>aps-environment</key>
<string>development</string>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.siri</key>
<true/>
<key>com.apple.security.application-groups</key>

View File

@@ -41,8 +41,26 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
private let intentHandler = IntentHandler()
private let viewAppIntentHandler = ViewAppIntentHandler()
public let consoleLog = ConsoleLog()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
// navigation bar buttons spacing is too much (so hack it to use minimal spacing)
// this is swift-5 specific behavior and might change
// https://stackoverflow.com/a/64988363/11971304
//
// Warning: this affects all screens through out the app, and basically overrides storyboard
let stackViewAppearance = UIStackView.appearance(whenContainedInInstancesOf: [UINavigationBar.self])
stackViewAppearance.spacing = -8 // adjust as needed
consoleLog.startCapturing()
print("===================================================")
print("| App is Starting up |")
print("===================================================")
print("| Console Logger started capturing output streams |")
print("===================================================")
print("\n ")
// Override point for customization after application launch.
// UserDefaults.standard.setValue(true, forKey: "com.apple.CoreData.MigrationDebug")
// UserDefaults.standard.setValue(true, forKey: "com.apple.CoreData.SQLDebug")
@@ -81,9 +99,9 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
UserDefaults.standard.preferredServerID = Bundle.main.object(forInfoDictionaryKey: Bundle.Info.serverID) as? String
// #if DEBUG || BETA
#if DEBUG && (targetEnvironment(simulator) || BETA)
UserDefaults.standard.isDebugModeEnabled = true
// #endif
#endif
self.prepareForBackgroundFetch()
@@ -130,6 +148,17 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
default: return nil
}
}
func applicationWillTerminate(_ application: UIApplication) {
// Stop console logging and clean up resources
print("\n ")
print("===================================================")
print("| Console Logger stopped capturing output streams |")
print("===================================================")
print("| App is being terminated |")
print("===================================================")
consoleLog.stopCapturing()
}
}
extension AppDelegate
@@ -269,7 +298,7 @@ extension AppDelegate
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { (success, error) in
}
#if DEBUG
#if DEBUG && targetEnvironment(simulator)
UIApplication.shared.registerForRemoteNotifications()
#endif
}

View File

@@ -125,6 +125,7 @@ private extension AuthenticationViewController
let error = error.withLocalizedTitle(NSLocalizedString("Failed to Log In", comment: ""))
let toastView = ToastView(error: error)
toastView.show(in: self)
toastView.backgroundColor = .white
toastView.textLabel.textColor = .altPrimary
toastView.detailTextLabel.textColor = .altPrimary
self.toastView = toastView

View File

@@ -67,30 +67,10 @@ class ToastView: RSTToastView
convenience init(error: Error)
{
var error = error as NSError
var underlyingError = error.underlyingError
let error = error as NSError
if
let unwrappedUnderlyingError = underlyingError,
error.domain == AltServerErrorDomain && error.code == ALTServerError.Code.underlyingError.rawValue
{
// Treat underlyingError as the primary error, but keep localized title + failure.
let nsError = error as NSError
error = unwrappedUnderlyingError as NSError
if let localizedTitle = nsError.localizedTitle {
error = error.withLocalizedTitle(localizedTitle)
}
if let localizedFailure = nsError.localizedFailure {
error = error.withLocalizedFailure(localizedFailure)
}
underlyingError = nil
}
let text = error.localizedTitle ?? NSLocalizedString("Operation Failed", comment: "")
let detailText = error.localizedDescription
let detailText = ErrorProcessing(.fullError).getDescription(error: error)
self.init(text: text, detailText: detailText)
}

View File

@@ -1,96 +0,0 @@
//
// ProcessInfo+SideStore.swift
// SideStore
//
// Created by ny on 10/23/24.
// Copyright © 2024 SideStore. All rights reserved.
//
import Foundation
fileprivate struct BuildVersion: Comparable {
let prefix: String
let numericPart: Int
let suffix: Character?
init?(_ buildString: String) {
// Initialize indices
var index = buildString.startIndex
// Extract prefix (letters before the numeric part)
while index < buildString.endIndex, !buildString[index].isNumber {
index = buildString.index(after: index)
}
guard index > buildString.startIndex else { return nil }
self.prefix = String(buildString[buildString.startIndex..<index])
// Extract numeric part
let startOfNumeric = index
while index < buildString.endIndex, buildString[index].isNumber {
index = buildString.index(after: index)
}
guard let numericValue = Int(buildString[startOfNumeric..<index]) else { return nil }
self.numericPart = numericValue
// Extract suffix (if any)
if index < buildString.endIndex {
self.suffix = buildString[index]
} else {
self.suffix = nil
}
}
// Implement Comparable protocol
static func < (lhs: BuildVersion, rhs: BuildVersion) -> Bool {
// Compare prefixes
if lhs.prefix != rhs.prefix {
return lhs.prefix < rhs.prefix
}
// Compare numeric parts
if lhs.numericPart != rhs.numericPart {
return lhs.numericPart < rhs.numericPart
}
// Compare suffixes
switch (lhs.suffix, rhs.suffix) {
case let (l?, r?):
return l < r
case (nil, _?):
return true // nil is considered less than any character
case (_?, nil):
return false
default:
return false // Both are nil and equal
}
}
static func == (lhs: BuildVersion, rhs: BuildVersion) -> Bool {
return lhs.prefix == rhs.prefix &&
lhs.numericPart == rhs.numericPart &&
lhs.suffix == rhs.suffix
}
}
extension ProcessInfo {
var shortVersion: String {
operatingSystemVersionString
.replacingOccurrences(of: "Version ", with: "")
.replacingOccurrences(of: "Build ", with: "")
}
var operatingSystemBuild: String {
if let start = shortVersion.range(of: "(")?.upperBound,
let end = shortVersion.range(of: ")")?.lowerBound {
shortVersion[start..<end].replacingOccurrences(of: "Build ", with: "")
} else { "???" }
}
var sparseRestorePatched: Bool {
if operatingSystemVersion < OperatingSystemVersion(majorVersion: 18, minorVersion: 1, patchVersion: 0) { false }
else if operatingSystemVersion > OperatingSystemVersion(majorVersion: 18, minorVersion: 1, patchVersion: 1) { true }
else if operatingSystemVersion >= OperatingSystemVersion(majorVersion: 18, minorVersion: 1, patchVersion: 0),
let currentBuild = BuildVersion(operatingSystemBuild),
let targetBuild = BuildVersion("22B5054e") {
currentBuild >= targetBuild
} else { false }
}
}

View File

@@ -248,7 +248,9 @@ final class LaunchViewController: RSTLaunchViewController, UIDocumentPickerDeleg
target_minimuxer_address()
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
do {
try start(pairing_file, documentsDirectory)
// enable minimuxer console logging only if enabled in settings
let isMinimuxerConsoleLoggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled
try minimuxer.startWithLogger(pairing_file, documentsDirectory, isMinimuxerConsoleLoggingEnabled)
} catch {
try! FileManager.default.removeItem(at: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)"))
displayError("minimuxer failed to start, please restart SideStore. \((error as? LocalizedError)?.failureReason ?? "UNKNOWN ERROR!!!!!! REPORT TO GITHUB ISSUES!")")
@@ -311,6 +313,9 @@ extension LaunchViewController
guard case .failure(let error) = result else { return }
Logger.main.error("Failed to update sources on launch. \(error.localizedDescription, privacy: .public)")
let errorDesc = ErrorProcessing(.fullError).getDescription(error: error as NSError)
print("Failed to update sources on launch. \(errorDesc)")
let toastView = ToastView(error: error)
toastView.addTarget(self.destinationViewController, action: #selector(TabBarController.presentSources), for: .touchUpInside)
toastView.show(in: self.destinationViewController.selectedViewController ?? self.destinationViewController)
@@ -318,6 +323,7 @@ extension LaunchViewController
self.updateKnownSources()
// Ask widgets to be refreshed
WidgetCenter.shared.reloadAllTimelines()
// Add view controller as child (rather than presenting modally)

View File

@@ -626,6 +626,11 @@ extension AppManager
self.fetchSources() { (result) in
do
{
// Check if the result is failure and rethrow
if case .failure(let error) = result {
throw error // Rethrow the error
}
do
{
let (_, context) = try result.get()
@@ -1146,27 +1151,28 @@ private extension AppManager
case .activate(let app) where UserDefaults.standard.isLegacyDeactivationSupported: fallthrough
case .refresh(let app):
// Check if backup app is installed in place of real app.
let uti = UTTypeCopyDeclaration(app.installedBackupAppUTI as CFString)?.takeRetainedValue() as NSDictionary?
// let altBackupUti = UTTypeCopyDeclaration(app.installedBackupAppUTI as CFString)?.takeRetainedValue() as NSDictionary?
if app.certificateSerialNumber != group.context.certificate?.serialNumber ||
uti != nil ||
app.needsResign ||
// if app.certificateSerialNumber != group.context.certificate?.serialNumber ||
// altBackupUti != nil || // why would altbackup requires reinstall? it shouldn't cause we are just renewing profiles
// app.needsResign || // why would an app require resign during refresh? it shouldn't!
// We need to reinstall ourselves on refresh to ensure the new provisioning profile is used
app.bundleIdentifier == StoreApp.altstoreAppID
{
// => mahee96: jkcoxson confirmed misagent manages profiles independently without requiring lockdownd or installd intervention, so sidestore profile renewal shouldn't require reinstall
// app.bundleIdentifier == StoreApp.altstoreAppID
// {
// Resign app instead of just refreshing profiles because either:
// * Refreshing using different certificate
// * Backup app is still installed
// * App explicitly needs resigning
// * Refreshing using different certificate // when can this happen?, lets assume, refreshing with different certificate, why not just ask user to re-install manually? (probably we need re-install button)
// * Backup app is still installed // but why? I mean the AltBackup was put in place for a reason? ie during refresh just renew appIDs don't care about the app itself.
// * App explicitly needs resigning // when can this happen?
// * Device is jailbroken and using AltDaemon on iOS 14.0 or later (b/c refreshing with provisioning profiles is broken)
let installProgress = self._install(app, operation: operation, group: group) { (result) in
self.finish(operation, result: result, group: group, progress: progress)
}
progress?.addChild(installProgress, withPendingUnitCount: 80)
}
else
{
// let installProgress = self._install(app, operation: operation, group: group) { (result) in
// self.finish(operation, result: result, group: group, progress: progress)
// }
// progress?.addChild(installProgress, withPendingUnitCount: 80)
// }
// else
// {
// Refreshing with same certificate as last time, and backup app isn't still installed,
// so we can just refresh provisioning profiles.
@@ -1174,7 +1180,7 @@ private extension AppManager
self.finish(operation, result: result, group: group, progress: progress)
}
progress?.addChild(refreshProgress, withPendingUnitCount: 80)
}
// }
case .activate(let app):
let activateProgress = self._activate(app, operation: operation, group: group) { (result) in
@@ -1234,132 +1240,6 @@ private extension AppManager
return group
}
func removeAppExtensions(
from application: ALTApplication,
existingApp: InstalledApp?,
extensions: Set<ALTApplication>,
_ presentingViewController: UIViewController?,
completion: @escaping (Result<Void, Error>) -> Void
) {
// App-Extensions: Ensure existing app's extensions and currently installing app's extensions must match
if let existingApp {
_ = RSTAsyncBlockOperation { _ in
let existingAppEx: Set<InstalledExtension> = existingApp.appExtensions
let currentAppEx: Set<ALTApplication> = application.appExtensions
let currentAppExNames = currentAppEx.map{ appEx in appEx.bundleIdentifier}
let existingAppExNames = existingAppEx.map{ appEx in appEx.bundleIdentifier}
let excessExtensions = currentAppEx.filter{
!(existingAppExNames.contains($0.bundleIdentifier))
}
let isMatching = (currentAppEx.count == existingAppEx.count) && excessExtensions.isEmpty
let diagnosticsMsg = "AppManager.removeAppExtensions: App Extensions in existingApp and currentApp are matching: \(isMatching)\n"
+ "AppManager.removeAppExtensions: existingAppEx: \(existingAppExNames); currentAppEx: \(String(describing: currentAppExNames))\n"
print(diagnosticsMsg)
// if background mode, then remove only the excess extensions
guard let presentingViewController: UIViewController = presentingViewController else {
// perform silent extensions cleanup for those that aren't already present in existing app
print("\n Performing background mode Extensions removal \n")
print("AppManager.removeAppExtensions: Excess Extensions: \(excessExtensions)")
do {
for appExtension in excessExtensions {
print("Deleting extension \(appExtension.bundleIdentifier)")
try FileManager.default.removeItem(at: appExtension.fileURL)
}
return completion(.success(()))
} catch {
return completion(.failure(error))
}
}
}
}
guard !application.appExtensions.isEmpty else { return completion(.success(())) }
DispatchQueue.main.async {
let firstSentence: String
if UserDefaults.standard.activeAppLimitIncludesExtensions
{
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions.", comment: "")
}
else
{
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to creating 10 App IDs per week.", comment: "")
}
let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit? There are \(extensions.count) Extensions", comment: "")
let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in
completion(.failure(OperationError.cancelled))
}))
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in
completion(.success(()))
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in
do
{
for appExtension in application.appExtensions
{
print("Deleting extension \(appExtension.bundleIdentifier)")
try FileManager.default.removeItem(at: appExtension.fileURL)
}
completion(.success(()))
}
catch
{
completion(.failure(error))
}
})
if let presentingViewController {
alertController.addAction(UIAlertAction(title: NSLocalizedString("Choose App Extensions", comment: ""), style: .default) { (action) in
let popoverContentController = AppExtensionViewHostingController(extensions: extensions) { (selection) in
do
{
for appExtension in selection
{
print("Deleting extension \(appExtension.bundleIdentifier)")
try FileManager.default.removeItem(at: appExtension.fileURL)
}
completion(.success(()))
}
catch
{
completion(.failure(error))
}
return nil
}
let suiview = popoverContentController.view!
suiview.translatesAutoresizingMaskIntoConstraints = false
popoverContentController.modalPresentationStyle = .popover
if let popoverPresentationController = popoverContentController.popoverPresentationController {
popoverPresentationController.sourceView = presentingViewController.view
popoverPresentationController.sourceRect = CGRect(x: 50, y: 50, width: 4, height: 4)
popoverPresentationController.delegate = popoverContentController
presentingViewController.present(popoverContentController, animated: true)
}
})
presentingViewController.present(alertController, animated: true)
}
}
}
private func _install(_ app: AppProtocol,
operation appOperation: AppOperation,
group: RefreshGroup,
@@ -1469,52 +1349,20 @@ private extension AppManager
verifyOperation.addDependency(downloadOperation)
/* Remove App Extensions */
let removeAppExtensionsOperation = RSTAsyncBlockOperation { [weak self] (operation) in
do
{
if let error = context.error
{
throw error
}
/*
guard case .install = appOperation else {
operation.finish()
return
}
*/
guard let extensions = context.app?.appExtensions else {
throw OperationError.invalidParameters("AppManager._install.removeAppExtensionsOperation: context.app?.appExtensions is nil")
}
guard let currentApp = context.app else {
throw OperationError.invalidParameters("AppManager._install.removeAppExtensionsOperation: context.app is nil")
}
self?.removeAppExtensions(from: currentApp,
existingApp: app as? InstalledApp,
extensions: extensions,
context.authenticatedContext.presentingViewController
) { result in
switch result {
case .success(): break
case .failure(let error): context.error = error
}
operation.finish()
}
}
catch
let localAppExtensions = (app as? ALTApplication)?.appExtensions
let removeAppExtensionsOperation = RemoveAppExtensionsOperation(context: context,
localAppExtensions: localAppExtensions)
removeAppExtensionsOperation.resultHandler = { (result) in
switch result
{
case .failure(let error):
context.error = error
operation.finish()
case .success: break
}
}
removeAppExtensionsOperation.addDependency(verifyOperation)
/* Refresh Anisette Data */
let refreshAnisetteDataOperation = FetchAnisetteDataOperation(context: group.context)
refreshAnisetteDataOperation.resultHandler = { (result) in
@@ -1529,7 +1377,7 @@ private extension AppManager
/* Fetch Provisioning Profiles */
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesInstallOperation(context: context)
fetchProvisioningProfilesOperation.additionalEntitlements = additionalEntitlements
fetchProvisioningProfilesOperation.resultHandler = { (result) in
switch result
@@ -1763,7 +1611,7 @@ private extension AppManager
private func exportResginedAppsToDocsDir(_ resignedApp: ALTApplication)
{
// Check if the user has enabled exporting resigned apps to the Documents directory and continue
guard UserDefaults.standard.isResignedAppExportEnabled else {
guard UserDefaults.standard.isExportResignedAppEnabled else {
return
}
@@ -1815,26 +1663,27 @@ private extension AppManager
let context = AppOperationContext(bundleIdentifier: app.bundleIdentifier, authenticatedContext: group.context)
context.app = ALTApplication(fileURL: app.fileURL)
//App-Extensions: Ensure DB data and disk state must match
let dbAppEx: Set<InstalledExtension> = Set(app.appExtensions)
let diskAppEx: Set<ALTApplication> = Set(context.app!.appExtensions)
let diskAppExNames = diskAppEx.map { $0.bundleIdentifier }
let dbAppExNames = dbAppEx.map{ $0.bundleIdentifier }
let isMatching = Set(dbAppExNames) == Set(diskAppExNames)
// Since this doesn't involve modifying app bundle which will cause re-install, this is safe in refresh path
//App-Extensions: Ensure DB data and disk state must match
let dbAppEx: Set<InstalledExtension> = Set(app.appExtensions)
let diskAppEx: Set<ALTApplication> = Set(context.app!.appExtensions)
let diskAppExNames = diskAppEx.map { $0.bundleIdentifier }
let dbAppExNames = dbAppEx.map{ $0.bundleIdentifier }
let isMatching = Set(dbAppExNames) == Set(diskAppExNames)
let validateAppExtensionsOperation = RSTAsyncBlockOperation { op in
let errMessage = "AppManager.refresh: App Extensions in DB and Disk are matching: \(isMatching)\n"
+ "AppManager.refresh: dbAppEx: \(dbAppExNames); diskAppEx: \(String(describing: diskAppExNames))\n"
print(errMessage)
if(!isMatching){
completionHandler(.failure(OperationError.refreshAppFailed(message: errMessage)))
}
op.finish()
}
let validateAppExtensionsOperation = RSTAsyncBlockOperation { op in
let errMessage = "AppManager.refresh: App Extensions in DB and Disk are matching: \(isMatching)\n"
+ "AppManager.refresh: dbAppEx: \(dbAppExNames); diskAppEx: \(String(describing: diskAppExNames))\n"
print(errMessage)
if(!isMatching){
completionHandler(.failure(OperationError.refreshAppFailed(message: errMessage)))
}
op.finish()
}
/* Fetch Provisioning Profiles */
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesOperation(context: context)
let fetchProvisioningProfilesOperation = FetchProvisioningProfilesRefreshOperation(context: context)
fetchProvisioningProfilesOperation.resultHandler = { (result) in
switch result
{
@@ -1844,7 +1693,7 @@ private extension AppManager
}
}
progress.addChild(fetchProvisioningProfilesOperation.progress, withPendingUnitCount: 60)
fetchProvisioningProfilesOperation.addDependency(validateAppExtensionsOperation)
// fetchProvisioningProfilesOperation.addDependency(validateAppExtensionsOperation)
/* Refresh */
let refreshAppOperation = RefreshAppOperation(context: context)
@@ -1853,7 +1702,10 @@ private extension AppManager
{
case .success(let installedApp):
completionHandler(.success(installedApp))
// refreshing local app's provisioning profile means talking to misagent daemon
// which requires loopback vpn
case .failure(MinimuxerError.ProfileInstall):
completionHandler(.failure(OperationError.noWiFi))
@@ -1878,7 +1730,8 @@ private extension AppManager
progress.addChild(refreshAppOperation.progress, withPendingUnitCount: 40)
refreshAppOperation.addDependency(fetchProvisioningProfilesOperation)
let operations = [validateAppExtensionsOperation, fetchProvisioningProfilesOperation, refreshAppOperation]
// let operations = [validateAppExtensionsOperation, fetchProvisioningProfilesOperation, refreshAppOperation]
let operations = [fetchProvisioningProfilesOperation, refreshAppOperation]
group.add(operations)
self.run(operations, context: group.context)
@@ -2280,6 +2133,7 @@ private extension AppManager
AnalyticsManager.shared.trackEvent(event)
}
// Ask widgets to be refreshed
WidgetCenter.shared.reloadAllTimelines()
do

View File

@@ -1151,7 +1151,9 @@ private extension MyAppsViewController
func refresh(_ installedApp: InstalledApp)
{
guard minimuxerStatus else { return }
// we do need minimuxer, coz it needs to talk to misagent daemon which manages profiles
// so basically loopback vpn is still required
guard minimuxerStatus else { return } // we don't need minimuxer when renewing appIDs only do we, heck we can even do it on mobile internet
let previousProgress = AppManager.shared.refreshProgress(for: installedApp)
guard previousProgress == nil else {
@@ -1357,6 +1359,50 @@ private extension MyAppsViewController
self.present(alertController, animated: true, completion: nil)
}
func importBackup(for installedApp: InstalledApp){
ImportExport.importBackup(presentingViewController: self, for: installedApp) { result in
var toast: ToastView
switch(result){
case .failure(let error):
toast = ToastView(error: error, opensLog: false)
break
case .success:
toast = ToastView(text: "Import Backup successful for \(installedApp.name)",
detailText: "Use 'Restore Backup' option to restore data from this imported backup")
}
DispatchQueue.main.async {
toast.show(in: self)
}
}
}
private func getPreviousBackupURL(_ installedApp: InstalledApp) -> URL
{
let backupURL = FileManager.default.backupDirectoryURL(for: installedApp)!
let backupBakURL = ImportExport.getPreviousBackupURL(backupURL)
return backupBakURL
}
func restorePreviousBackup(for installedApp: InstalledApp){
let backupURL = FileManager.default.backupDirectoryURL(for: installedApp)!
let backupBakURL = ImportExport.getPreviousBackupURL(backupURL)
// backupBakURL is expected to exist at this point, this needs to be ensured by caller logic
// or invoke this action only when backupBakURL exists
// delete the current backup
if(FileManager.default.fileExists(atPath: backupURL.path)){
try! FileManager.default.removeItem(at: backupURL)
}
// restore the previously saved backup as current backup
// (don't delete the N-1 backup yet so copy instead of move)
try! FileManager.default.copyItem(at: backupBakURL, to: backupURL)
//perform restore of data from the backup
restore(installedApp)
}
func restore(_ installedApp: InstalledApp)
{
guard minimuxerStatus else { return }
@@ -1423,7 +1469,10 @@ private extension MyAppsViewController
do
{
let tempApp = context.object(with: installedApp.objectID) as! InstalledApp
tempApp.needsResign = true
tempApp.needsResign = true // why do we want to resign it during refresh ?!!!!
// I see now, so here we just mark that icon needs to be changed but leave it for refresh/install to do it
// this is bad, coz now the weight of installing goes to refresh step !!! which is not what we want
tempApp.hasAlternateIcon = (image != nil)
if let image = image
@@ -1459,27 +1508,27 @@ private extension MyAppsViewController
}
}
func enableJIT(for installedApp: InstalledApp)
{
func enableJIT(for installedApp: InstalledApp) {
let sidejitenabled = UserDefaults.standard.sidejitenable
if #unavailable(iOS 17) {
if #unavailable(iOS 17), !sidejitenabled {
guard minimuxerStatus else { return }
}
if #available(iOS 17, *), !sidejitenabled {
ToastView(error: (OperationError.tooNewError as NSError).withLocalizedTitle("No iOS 17 On Device JIT!"), opensLog: true).show(in: self)
AppManager.shared.log(OperationError.tooNewError, operation: .enableJIT, app: installedApp)
let error = OperationError.tooNewError as NSError
let localizedError = error.withLocalizedTitle("No iOS 17 On Device JIT!")
ToastView(error: localizedError, opensLog: true).show(in: self)
AppManager.shared.log(error, operation: .enableJIT, app: installedApp)
return
}
AppManager.shared.enableJIT(for: installedApp) { result in
DispatchQueue.main.async {
switch result
{
case .success: break
switch result {
case .success:
break
case .failure(let error):
ToastView(error: error, opensLog: true).show(in: self)
AppManager.shared.log(error, operation: .enableJIT, app: installedApp)
@@ -1810,9 +1859,17 @@ extension MyAppsViewController
self.exportBackup(for: installedApp)
}
let restoreBackupAction = UIAction(title: NSLocalizedString("Restore Backup", comment: ""), image: UIImage(systemName: "arrow.down.doc")) { (action) in
let importBackupAction = UIAction(title: NSLocalizedString("Import Backup", comment: ""), image: UIImage(systemName: "arrow.down.doc")) { (action) in
self.importBackup(for: installedApp)
}
let restoreBackupAction = UIAction(title: NSLocalizedString("Restore Backup", comment: "Restores the last or current backup of this app"), image: UIImage(systemName: "arrow.down.doc")) { (action) in
self.restore(installedApp)
}
let restorePreviousBackupAction = UIAction(title: NSLocalizedString("Restore Previous Backup", comment: "Restores the backup saved before the current backup was created."), image: UIImage(systemName: "arrow.down.doc")) { (action) in
self.restorePreviousBackup(for: installedApp)
}
let chooseIconAction = UIAction(title: NSLocalizedString("Photos", comment: ""), image: UIImage(systemName: "photo")) { (action) in
self.chooseIcon(for: installedApp)
@@ -1878,7 +1935,8 @@ extension MyAppsViewController
var outError: NSError? = nil
self.coordinator.coordinate(readingItemAt: backupDirectoryURL, options: [.withoutChanges], error: &outError) { (backupDirectoryURL) in
#if DEBUG
#if DEBUG && targetEnvironment(simulator)
backupExists = true
#else
backupExists = FileManager.default.fileExists(atPath: backupDirectoryURL.path)
@@ -1903,15 +1961,21 @@ extension MyAppsViewController
if installedApp.isActive
{
actions.append(deactivateAction)
// import backup into shared backups dir is allowed
actions.append(importBackupAction)
}
#if DEBUG
// have an option to restore the n-1 backup
if FileManager.default.fileExists(atPath: getPreviousBackupURL(installedApp).path){
actions.append(restorePreviousBackupAction)
}
#if DEBUG && targetEnvironment(simulator)
if installedApp.bundleIdentifier != StoreApp.altstoreAppID
{
actions.append(removeAction)
}
#else
if (UserDefaults.standard.legacySideloadedApps ?? []).contains(installedApp.bundleIdentifier)
@@ -1929,6 +1993,26 @@ extension MyAppsViewController
#endif
}
// Change the order of entries to make changes to how the context menu is displayed
let orderedActions = [
openMenu,
refreshAction,
activateAction,
jitAction,
changeIconMenu,
backupAction,
exportBackupAction,
importBackupAction,
restoreBackupAction,
restorePreviousBackupAction,
deactivateAction,
removeAction,
]
// remove non-selected actions from the all-actions ordered list
// this way the declaration of the action in the above code doesn't determine the context menu order
actions = orderedActions.filter{ action in actions.contains(action)}
var title: String?
if let storeApp = installedApp.storeApp, storeApp.isPledgeRequired, !storeApp.isPledged

View File

@@ -715,9 +715,10 @@ private extension AuthenticationOperation
// If we're not using the same certificate used to install AltStore, warn user that they need to refresh.
guard !provisioningProfile.certificates.contains(signer.certificate) else { return completionHandler(false) }
#if DEBUG
completionHandler(false)
#else
// #if DEBUG && targetEnvironment(simulator)
// completionHandler(false)
// #else
DispatchQueue.main.async {
let context = AuthenticatedOperationContext(context: self.context)
context.operations.removeAllObjects() // Prevent deadlock due to endless waiting on previous operations to finish.
@@ -733,7 +734,7 @@ private extension AuthenticationOperation
completionHandler(false)
}
}
#endif
// #endif
}
}

View File

@@ -103,7 +103,14 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<Inst
target_minimuxer_address()
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
do {
try minimuxer.start(try String(contentsOf: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")), documentsDirectory)
// enable minimuxer console logging only if enabled in settings
let isMinimuxerConsoleLoggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled
try minimuxer.startWithLogger(
try String(contentsOf: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")),
documentsDirectory,
isMinimuxerConsoleLoggingEnabled
)
} catch {
self.finish(.failure(error))
}

View File

@@ -53,38 +53,37 @@ final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
guard let installedApp = self.context.installedApp else {
return self.finish(.failure(OperationError.invalidParameters("EnableJITOperation.main: self.context.installedApp is nil")))
}
if #available(iOS 17, *) {
let sideJITenabled = UserDefaults.standard.sidejitenable
let SideJITIP = UserDefaults.standard.textInputSideJITServerurl ?? ""
if sideJITenabled {
installedApp.managedObjectContext?.perform {
EnableJITSideJITServer(serverurl: SideJITIP, installedapp: installedApp) { result in
switch result {
case .failure(let error):
switch error {
case .invalidURL, .errorConnecting:
self.finish(.failure(OperationError.unableToConnectSideJIT))
case .deviceNotFound:
self.finish(.failure(OperationError.unableToRespondSideJITDevice))
case .other(let message):
if let startRange = message.range(of: "<p>"),
let endRange = message.range(of: "</p>", range: startRange.upperBound..<message.endIndex) {
let pContent = message[startRange.upperBound..<endRange.lowerBound]
self.finish(.failure(OperationError.SideJITIssue(error: String(pContent))))
print(message + " + " + String(pContent))
} else {
print(message)
self.finish(.failure(OperationError.SideJITIssue(error: message)))
}
let userdefaults = UserDefaults.standard
if #available(iOS 17, *), userdefaults.sidejitenable {
let SideJITIP = userdefaults.textInputSideJITServerurl ?? "http://sidejitserver._http._tcp.local:8080"
installedApp.managedObjectContext?.perform {
enableJITSideJITServer(serverURL: URL(string: SideJITIP)!, installedApp: installedApp) { result in
switch result {
case .failure(let error):
switch error {
case .invalidURL, .errorConnecting:
self.finish(.failure(OperationError.unableToConnectSideJIT))
case .deviceNotFound:
self.finish(.failure(OperationError.unableToRespondSideJITDevice))
case .other(let message):
if let startRange = message.range(of: "<p>"),
let endRange = message.range(of: "</p>", range: startRange.upperBound..<message.endIndex) {
let pContent = message[startRange.upperBound..<endRange.lowerBound]
self.finish(.failure(OperationError.SideJITIssue(error: String(pContent))))
print(message + " + " + String(pContent))
} else {
print(message)
self.finish(.failure(OperationError.SideJITIssue(error: message)))
}
case .success():
self.finish(.success(()))
print("Thank you for using this, it was made by Stossy11 and tested by trolley or sniper1239408")
}
case .success():
self.finish(.success(()))
print("JIT Enabled Successfully :3 (code made by Stossy11!)")
}
return
}
return
}
} else {
installedApp.managedObjectContext?.perform {
@@ -107,48 +106,40 @@ final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
}
@available(iOS 17, *)
func EnableJITSideJITServer(serverurl: String, installedapp: InstalledApp, completion: @escaping (Result<Void, SideJITServerErrorType>) -> Void) {
func enableJITSideJITServer(serverURL: URL, installedApp: InstalledApp, completion: @escaping (Result<Void, SideJITServerErrorType>) -> Void) {
guard let udid = fetch_udid()?.toString() else {
completion(.failure(.other("Unable to get UDID")))
return
}
var SJSURL = serverurl
let serverURLWithUDID = serverURL.appendingPathComponent(udid)
let fullURL = serverURLWithUDID.appendingPathComponent(installedApp.resignedBundleIdentifier)
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
SJSURL = "http://sidejitserver._http._tcp.local:8080"
}
if !SJSURL.hasPrefix("http") {
completion(.failure(.invalidURL))
return
}
let fullurl = SJSURL + "/\(udid)/" + installedapp.resignedBundleIdentifier
let url = URL(string: fullurl)!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
let task = URLSession.shared.dataTask(with: fullURL) { (data, response, error) in
if let error = error {
completion(.failure(.errorConnecting))
return
}
guard let data = data, let datastring = String(data: data, encoding: .utf8) else { return }
guard let data = data, let dataString = String(data: data, encoding: .utf8) else {
return
}
if datastring == "Enabled JIT for '\(installedapp.name)'!" {
if dataString == "Enabled JIT for '\(installedApp.name)'!" {
let content = UNMutableNotificationContent()
content.title = "JIT Successfully Enabled"
content.subtitle = "JIT Enabled For \(installedapp.name)"
content.sound = UNNotificationSound.default
content.subtitle = "JIT Enabled For \(installedApp.name)"
content.sound = .default
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
let request = UNNotificationRequest(identifier: "EnabledJIT", content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
completion(.success(()))
} else {
let errorType: SideJITServerErrorType = datastring == "Could not find device!" ? .deviceNotFound : .other(datastring)
let errorType: SideJITServerErrorType = dataString == "Could not find device!"
? .deviceNotFound
: .other(dataString)
completion(.failure(errorType))
}
}

View File

@@ -58,6 +58,8 @@ extension OperationError
case anisetteV3Error//(message: String)
case cacheClearError//(errors: [String])
case noWiFi
case invalidOperationContext
}
static var cancelled: CancellationError { CancellationError() }
@@ -130,6 +132,10 @@ extension OperationError
OperationError(code: .invalidParameters, failureReason: message)
}
static func invalidOperationContext(_ message: String? = nil) -> OperationError {
OperationError(code: .invalidOperationContext, failureReason: message)
}
static func forbidden(failureReason: String? = nil, file: String = #fileID, line: UInt = #line) -> OperationError {
OperationError(code: .forbidden, failureReason: failureReason, sourceFile: file, sourceLine: line)
}
@@ -232,7 +238,10 @@ struct OperationError: ALTLocalizedError {
case .invalidParameters:
let message = self._failureReason.map { ": \n\($0)" } ?? "."
return String(format: NSLocalizedString("Invalid parameters%@", comment: ""), message)
return String(format: NSLocalizedString("Invalid parameters\n%@", comment: ""), message)
case .invalidOperationContext:
let message = self._failureReason.map { ": \n\($0)" } ?? "."
return String(format: NSLocalizedString("Invalid Operation Context\n%@", comment: ""), message)
case .serverNotFound: return NSLocalizedString("AltServer could not be found.", comment: "")
case .connectionFailed: return NSLocalizedString("A connection to AltServer could not be established.", comment: "")
case .connectionDropped: return NSLocalizedString("The connection to AltServer was dropped.", comment: "")

View File

@@ -14,6 +14,8 @@ import AltStoreCore
import AltSign
import Roxas
class ANISETTE_VERBOSITY: Operation {} // dummy tag iface
@objc(FetchAnisetteDataOperation)
final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSocketDelegate
{
@@ -58,7 +60,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
UserDefaults.standard.menuAnisetteURL = urlString
let url = URL(string: urlString)
self.url = url
print("Anisette URL: \(self.url!.absoluteString)")
self.printOut("Anisette URL: \(self.url!.absoluteString)")
if let identifier = Keychain.shared.identifier,
let adiPb = Keychain.shared.adiPb {
@@ -107,7 +109,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
guard let url = URL(string: currentServerUrlString) else {
// Invalid URL, skip to next
let errmsg = "Skipping invalid URL: \(currentServerUrlString)"
print(errmsg)
self.printOut(errmsg)
showToast(viewContext: viewContext, message: errmsg)
tryNextServer(from: serverUrls, viewContext, currentIndex: currentIndex + 1, completion: completion)
return
@@ -118,7 +120,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
if success {
// If the server is reachable, return the URL
let okmsg = "Found working server: \(url.absoluteString)"
print(okmsg)
self.printOut(okmsg)
if(currentIndex > 0){
// notify user if available server is different the user-specified one
self.showToast(viewContext: viewContext, message: okmsg)
@@ -127,7 +129,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
} else {
// If not, try the next URL
let errmsg = "Failed to reach server: \(url.absoluteString), trying next server."
print(errmsg)
self.printOut(errmsg)
self.showToast(viewContext: viewContext, message: errmsg)
self.tryNextServer(from: serverUrls, viewContext, currentIndex: currentIndex + 1, completion: completion)
}
@@ -170,10 +172,10 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
if v3 {
if json["result"] == "GetHeadersError" {
let message = json["message"]
print("Error getting V3 headers: \(message ?? "no message")")
self.printOut("Error getting V3 headers: \(message ?? "no message")")
if let message = message,
message.contains("-45061") {
print("Error message contains -45061 (not provisioned), resetting adi.pb and retrying")
self.printOut("Error message contains -45061 (not provisioned), resetting adi.pb and retrying")
Keychain.shared.adiPb = nil
return provision()
} else { throw OperationError.anisetteV3Error(message: message ?? "Unknown error") }
@@ -214,16 +216,16 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
if let response = response,
let version = response.value(forHTTPHeaderField: "Implementation-Version") {
print("Implementation-Version: \(version)")
} else { print("No Implementation-Version header") }
self.printOut("Implementation-Version: \(version)")
} else { self.printOut("No Implementation-Version header") }
print("Anisette used: \(formattedJSON)")
print("Original JSON: \(json)")
self.printOut("Anisette used: \(formattedJSON)")
self.printOut("Original JSON: \(json)")
if let anisette = ALTAnisetteData(json: formattedJSON) {
print("Anisette is valid!")
self.printOut("Anisette is valid!")
self.finish(.success(anisette))
} else {
print("Anisette is invalid!!!!")
self.printOut("Anisette is invalid!!!!")
if v3 {
throw OperationError.anisetteV3Error(message: "Invalid anisette (the returned data may not have all the required fields)")
} else {
@@ -242,22 +244,22 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
// MARK: - V1
func handleV1() {
print("Server is V1")
self.printOut("Server is V1")
if UserDefaults.shared.trustedServerURL == AnisetteManager.currentURLString {
print("Server has already been trusted, fetching anisette")
self.printOut("Server has already been trusted, fetching anisette")
return self.fetchAnisetteV1()
}
print("Alerting user about outdated server")
self.printOut("Alerting user about outdated server")
let alert = UIAlertController(title: "WARNING: Outdated anisette server", message: "We've detected you are using an older anisette server. Using this server has a higher likelihood of locking your account and causing other issues. Are you sure you want to continue?", preferredStyle: UIAlertController.Style.alert)
alert.addAction(UIAlertAction(title: "Continue", style: UIAlertAction.Style.destructive, handler: { action in
print("Fetching anisette via V1")
self.printOut("Fetching anisette via V1")
UserDefaults.shared.trustedServerURL = AnisetteManager.currentURLString
self.fetchAnisetteV1()
}))
alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertAction.Style.cancel, handler: { action in
print("Cancelled anisette operation")
self.printOut("Cancelled anisette operation")
self.finish(.failure(OperationError.cancelled))
}))
@@ -273,14 +275,14 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
}
func fetchAnisetteV1() {
print("Fetching anisette V1")
self.printOut("Fetching anisette V1")
URLSession.shared.dataTask(with: self.url!) { data, response, error in
do {
guard let data = data, error == nil else { throw OperationError.anisetteV1Error(message: "Unable to fetch data\(error != nil ? " (\(error!.localizedDescription))" : "")") }
try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: false)
} catch let error as NSError {
print("Failed to load: \(error.localizedDescription)")
self.printOut("Failed to load: \(error.localizedDescription)")
self.finish(.failure(error))
}
}.resume()
@@ -290,7 +292,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
func provision() {
fetchClientInfo {
print("Getting provisioning URLs")
self.printOut("Getting provisioning URLs")
var request = self.buildAppleRequest(url: URL(string: "https://gsa.apple.com/grandslam/GsService2/lookup")!)
request.httpMethod = "GET"
URLSession.shared.dataTask(with: request) { data, response, error in
@@ -302,12 +304,12 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
let endProvisioningURL = URL(string: endProvisioningString) {
self.startProvisioningURL = startProvisioningURL
self.endProvisioningURL = endProvisioningURL
print("startProvisioningURL: \(self.startProvisioningURL!.absoluteString)")
print("endProvisioningURL: \(self.endProvisioningURL!.absoluteString)")
print("Starting a provisioning session")
self.printOut("startProvisioningURL: \(self.startProvisioningURL!.absoluteString)")
self.printOut("endProvisioningURL: \(self.endProvisioningURL!.absoluteString)")
self.printOut("Starting a provisioning session")
self.startProvisioningSession()
} else {
print("Apple didn't give valid URLs! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
self.printOut("Apple didn't give valid URLs! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid URLs. Please try again later", message: nil)))
}
}.resume()
@@ -329,19 +331,19 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
do {
if let json = try JSONSerialization.jsonObject(with: string.data(using: .utf8)!, options: []) as? [String: Any] {
guard let result = json["result"] as? String else {
print("The server didn't give us a result")
self.printOut("The server didn't give us a result")
client.disconnect(closeCode: 0)
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a result", message: nil)))
return
}
print("Received result: \(result)")
self.printOut("Received result: \(result)")
switch result {
case "GiveIdentifier":
print("Giving identifier")
self.printOut("Giving identifier")
client.json(["identifier": Keychain.shared.identifier!])
case "GiveStartProvisioningData":
print("Getting start provisioning data")
self.printOut("Getting start provisioning data")
let body = [
"Header": [String: Any](),
"Request": [String: Any](),
@@ -353,19 +355,19 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
if let data = data,
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
let spim = plist["Response"]?["spim"] as? String {
print("Giving start provisioning data")
self.printOut("Giving start provisioning data")
client.json(["spim": spim])
} else {
print("Apple didn't give valid start provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
self.printOut("Apple didn't give valid start provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
client.disconnect(closeCode: 0)
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid start provisioning data. Please try again later", message: nil)))
}
}.resume()
case "GiveEndProvisioningData":
print("Getting end provisioning data")
self.printOut("Getting end provisioning data")
guard let cpim = json["cpim"] as? String else {
print("The server didn't give us a cpim")
self.printOut("The server didn't give us a cpim")
client.disconnect(closeCode: 0)
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a cpim", message: nil)))
return
@@ -384,20 +386,20 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
let ptm = plist["Response"]?["ptm"] as? String,
let tk = plist["Response"]?["tk"] as? String {
print("Giving end provisioning data")
self.printOut("Giving end provisioning data")
client.json(["ptm": ptm, "tk": tk])
} else {
print("Apple didn't give valid end provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
self.printOut("Apple didn't give valid end provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
client.disconnect(closeCode: 0)
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid end provisioning data. Please try again later", message: nil)))
}
}.resume()
case "ProvisioningSuccess":
print("Provisioning succeeded!")
self.printOut("Provisioning succeeded!")
client.disconnect(closeCode: 0)
guard let adiPb = json["adi_pb"] as? String else {
print("The server didn't give us an adi.pb file")
self.printOut("The server didn't give us an adi.pb file")
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us an adi.pb file", message: nil)))
return
}
@@ -406,27 +408,27 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
default:
if result.contains("Error") || result.contains("Invalid") || result == "ClosingPerRequest" || result == "Timeout" || result == "TextOnly" {
print("Failing because of \(result)")
self.printOut("Failing because of \(result)")
self.finish(.failure(OperationError.provisioningError(result: result, message: json["message"] as? String)))
}
}
}
} catch let error as NSError {
print("Failed to handle text: \(error.localizedDescription)")
self.printOut("Failed to handle text: \(error.localizedDescription)")
self.finish(.failure(OperationError.provisioningError(result: error.localizedDescription, message: nil)))
}
case .connected:
print("Connected")
self.printOut("Connected")
case .disconnected(let string, let code):
print("Disconnected: \(code); \(string)")
self.printOut("Disconnected: \(code); \(string)")
case .error(let error):
print("Got error: \(String(describing: error))")
self.printOut("Got error: \(String(describing: error))")
default:
print("Unknown event: \(event)")
self.printOut("Unknown event: \(event)")
}
}
@@ -460,10 +462,10 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
self.mdLu != nil &&
self.deviceId != nil &&
Keychain.shared.identifier != nil {
print("Skipping client_info fetch since all the properties we need aren't nil")
self.printOut("Skipping client_info fetch since all the properties we need aren't nil")
return callback()
}
print("Trying to get client_info")
self.printOut("Trying to get client_info")
let clientInfoURL = self.url!.appendingPathComponent("v3").appendingPathComponent("client_info")
URLSession.shared.dataTask(with: clientInfoURL) { data, response, error in
do {
@@ -473,20 +475,20 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
if let clientInfo = json["client_info"] {
print("Server is V3")
self.printOut("Server is V3")
self.clientInfo = clientInfo
self.userAgent = json["user_agent"]!
print("Client-Info: \(self.clientInfo!)")
print("User-Agent: \(self.userAgent!)")
self.printOut("Client-Info: \(self.clientInfo!)")
self.printOut("User-Agent: \(self.userAgent!)")
if Keychain.shared.identifier == nil {
print("Generating identifier")
self.printOut("Generating identifier")
var bytes = [Int8](repeating: 0, count: 16)
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
if status != errSecSuccess {
print("ERROR GENERATING IDENTIFIER!!! \(status)")
self.printOut("ERROR GENERATING IDENTIFIER!!! \(status)")
return self.finish(.failure(OperationError.provisioningError(result: "Couldn't generate identifier", message: nil)))
}
@@ -495,16 +497,16 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
let decoded = Data(base64Encoded: Keychain.shared.identifier!)!
self.mdLu = decoded.sha256().hexEncodedString()
print("X-Apple-I-MD-LU: \(self.mdLu!)")
self.printOut("X-Apple-I-MD-LU: \(self.mdLu!)")
let uuid: UUID = decoded.object()
self.deviceId = uuid.uuidString.uppercased()
print("X-Mme-Device-Id: \(self.deviceId!)")
self.printOut("X-Mme-Device-Id: \(self.deviceId!)")
callback()
} else { self.handleV1() }
} else { self.finish(.failure(OperationError.anisetteV3Error(message: "Couldn't fetch client info. The returned data may not be in JSON"))) }
} catch let error as NSError {
print("Failed to load: \(error.localizedDescription)")
self.printOut("Failed to load: \(error.localizedDescription)")
self.handleV1()
}
}.resume()
@@ -512,7 +514,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
func fetchAnisetteV3(_ identifier: String, _ adiPb: String) {
fetchClientInfo {
print("Fetching anisette V3")
self.printOut("Fetching anisette V3")
let url = UserDefaults.standard.menuAnisetteURL
var request = URLRequest(url: self.url!.appendingPathComponent("v3").appendingPathComponent("get_headers"))
request.httpMethod = "POST"
@@ -527,12 +529,21 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: true)
} catch let error as NSError {
print("Failed to load: \(error.localizedDescription)")
self.printOut("Failed to load: \(error.localizedDescription)")
self.finish(.failure(error))
}
}.resume()
}
}
private func printOut(_ text: String?){
let isInternalLoggingEnabled = OperationsLoggingControl.getFromDatabase(for: ANISETTE_VERBOSITY.self)
if(isInternalLoggingEnabled){
// logging enabled, so log it
text.map{ _ in print(text!) } ?? print()
}
}
}
extension WebSocketClient {

View File

@@ -13,15 +13,16 @@ import AltSign
import Roxas
@objc(FetchProvisioningProfilesOperation)
final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
{
let context: AppOperationContext
var additionalEntitlements: [ALTEntitlement: Any]?
private let appGroupsLock = NSLock()
internal let appGroupsLock = NSLock()
init(context: AppOperationContext)
// this class is abstract or shouldn't be instantiated outside, use the subclasses
fileprivate init(context: AppOperationContext)
{
self.context = context
@@ -40,11 +41,13 @@ final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProv
return
}
guard
let team = self.context.team,
let session = self.context.session
else {
return self.finish(.failure(OperationError.invalidParameters("FetchProvisioningProfilesOperation.main: self.context.team or self.context.session is nil"))) }
guard let team = self.context.team,
let session = self.context.session else {
return self.finish(.failure(
OperationError.invalidParameters("FetchProvisioningProfilesOperation.main: self.context.team or self.context.session is nil"))
)
}
guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound(name: nil))) }
@@ -120,7 +123,11 @@ final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProv
extension FetchProvisioningProfilesOperation
{
func prepareProvisioningProfile(for app: ALTApplication, parentApp: ALTApplication?, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
private func prepareProvisioningProfile(for app: ALTApplication,
parentApp: ALTApplication?,
team: ALTTeam,
session: ALTAppleAPISession, c
completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
@@ -134,19 +141,21 @@ extension FetchProvisioningProfilesOperation
// or if installedApp.team is nil but resignedBundleIdentifier contains the team's identifier.
let teamsMatch = installedApp.team?.identifier == team.identifier || (installedApp.team == nil && installedApp.resignedBundleIdentifier.contains(team.identifier))
// #if DEBUG
//
// if app.isAltStoreApp
// {
// // Use legacy bundle ID format for AltStore.
// preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
// }
// else
// {
// preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
// }
//
// #else
// TODO: @mahee96: Try to keep the debug build and release build operations similar, refactor later with proper reasoning
// for now, restricted it to debug on simulator only
#if DEBUG && targetEnvironment(simulator)
if app.isAltStoreApp
{
// Use legacy bundle ID format for AltStore.
preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
}
else
{
preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
}
#else
if teamsMatch
{
@@ -160,7 +169,7 @@ extension FetchProvisioningProfilesOperation
preferredBundleID = nil
}
// #endif
#endif
}
else
{
@@ -211,35 +220,22 @@ extension FetchProvisioningProfilesOperation
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Update features
self.updateFeatures(for: appID, app: app, team: team, session: session) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Update app groups
self.updateAppGroups(for: appID, app: app, team: team, session: session) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Fetch Provisioning Profile
self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in
completionHandler(result)
}
}
}
}
}
//process
self.fetchProvisioningProfile(
for: appID, team: team, session: session, completionHandler: completionHandler
)
}
}
}
}
func registerAppID(for application: ALTApplication, name: String, bundleIdentifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
private func registerAppID(for application: ALTApplication,
name: String,
bundleIdentifier: String,
team: ALTTeam,
session: ALTAppleAPISession,
completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
do
@@ -333,7 +329,81 @@ extension FetchProvisioningProfilesOperation
}
}
func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
internal func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
switch Result(profile, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success(let profile):
// Delete existing profile
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
switch Result(success, error)
{
case .failure:
// As of March 20, 2023, the free provisioning profile is re-generated each fetch, and you can no longer delete it.
// So instead, we just return the fetched profile from above.
completionHandler(.success(profile))
case .success:
Logger.sideload.notice("Generating new free provisioning profile for App ID \(appID.bundleIdentifier, privacy: .public).")
// Fetch new provisioning profile
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
completionHandler(Result(profile, error))
}
}
}
}
}
}
}
class FetchProvisioningProfilesRefreshOperation: FetchProvisioningProfilesOperation, @unchecked Sendable {
override init(context: AppOperationContext)
{
super.init(context: context)
}
}
class FetchProvisioningProfilesInstallOperation: FetchProvisioningProfilesOperation, @unchecked Sendable{
override init(context: AppOperationContext)
{
super.init(context: context)
}
// modify Operations are allowed for the app groups and other stuffs
func fetchProvisioningProfile(appID: ALTAppID,
for app: ALTApplication,
team: ALTTeam,
session: ALTAppleAPISession,
completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
// Update features
self.updateFeatures(for: appID, app: app, team: team, session: session) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Update app groups
self.updateAppGroups(for: appID, app: app, team: team, session: session) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Fetch Provisioning Profile
super.fetchProvisioningProfile(for: appID, team: team, session: session, completionHandler: completionHandler)
}
}
}
}
}
private func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
var entitlements = app.entitlements
for (key, value) in additionalEntitlements ?? [:]
@@ -412,7 +482,7 @@ extension FetchProvisioningProfilesOperation
}
}
func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
private func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
var entitlements = app.entitlements
for (key, value) in additionalEntitlements ?? [:]
@@ -511,7 +581,7 @@ extension FetchProvisioningProfilesOperation
Logger.sideload.notice("Created new App Group \(group.groupIdentifier, privacy: .public).")
groups.append(group)
case .failure(let error):
case .failure(let error):
Logger.sideload.notice("Failed to create new App Group \(adjustedGroupIdentifier, privacy: .public). \(error.localizedDescription, privacy: .public)")
errors.append(error)
}
@@ -547,34 +617,4 @@ extension FetchProvisioningProfilesOperation
}
}
}
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
switch Result(profile, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success(let profile):
// Delete existing profile
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
switch Result(success, error)
{
case .failure:
// As of March 20, 2023, the free provisioning profile is re-generated each fetch, and you can no longer delete it.
// So instead, we just return the fetched profile from above.
completionHandler(.success(profile))
case .success:
Logger.sideload.notice("Generating new free provisioning profile for App ID \(appID.bundleIdentifier, privacy: .public).")
// Fetch new provisioning profile
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
completionHandler(Result(profile, error))
}
}
}
}
}
}
}

View File

@@ -38,10 +38,14 @@ class ResultOperation<ResultType>: Operation
result = .failure(error)
}
// diagnostics logging
let resultStatus = String(describing: result).prefix("success".count).uppercased()
print("\n ====> OPERATION: `\(type(of: self))` completed with: \(resultStatus) <====\n\n" +
" Result: \(result)\n")
// Diagnostics: perform verbose logging of the operations only if enabled (so as to not flood console logs)
let isLoggingEnabledForThisOperation = OperationsLoggingControl.getFromDatabase(for: type(of: self))
if UserDefaults.standard.isVerboseOperationsLoggingEnabled && isLoggingEnabledForThisOperation {
// diagnostics logging
let resultStatus = String(describing: result).prefix("success".count).uppercased()
print("\n ====> OPERATION: `\(type(of: self))` completed with: \(resultStatus) <====\n\n" +
" Result: \(result)\n")
}
self.resultHandler?(result)

View File

@@ -58,17 +58,18 @@ final class RemoveAppBackupOperation: ResultOperation<Void>
}
catch let error as CocoaError where error.code == CocoaError.Code.fileNoSuchFile
{
#if DEBUG
// When debugging, it's expected that app groups don't match, so ignore.
self.finish(.success(()))
#else
// TODO: @mahee96: Find out why should in debug builds the app-groups is not expected to match
// #if DEBUG
//
// // When debugging, it's expected that app groups don't match, so ignore.
// self.finish(.success(()))
//
// #else
Logger.sideload.error("Failed to remove app backup directory \(backupDirectoryURL.lastPathComponent, privacy: .public). \(error.localizedDescription, privacy: .public)")
self.finish(.failure(error))
#endif
// #endif
}
catch
{

View File

@@ -0,0 +1,219 @@
//
// RefreshAppOperation.swift
// AltStore
//
// Created by Riley Testut on 2/27/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltStoreCore
import Roxas
import AltSign
@objc(RemoveAppExtensionsOperation)
final class RemoveAppExtensionsOperation: ResultOperation<Void>
{
let context: AppOperationContext
let localAppExtensions: Set<ALTApplication>?
init(context: AppOperationContext, localAppExtensions: Set<ALTApplication>?)
{
self.context = context
self.localAppExtensions = localAppExtensions
super.init()
}
override func main()
{
super.main()
if let error = self.context.error
{
self.finish(.failure(error))
return
}
guard let targetAppBundle = context.app else {
return self.finish(.failure(
OperationError.invalidParameters("RemoveAppExtensionsOperation: context.app is nil")
))
}
self.removeAppExtensions(from: targetAppBundle,
localAppExtensions: localAppExtensions,
extensions: targetAppBundle.appExtensions,
context.authenticatedContext.presentingViewController)
}
private static func removeExtensions(from extensions: Set<ALTApplication>) throws {
for appExtension in extensions {
print("Deleting extension \(appExtension.bundleIdentifier)")
try FileManager.default.removeItem(at: appExtension.fileURL)
}
}
private func removeAppExtensions(from targetAppBundle: ALTApplication,
localAppExtensions: Set<ALTApplication>?,
extensions: Set<ALTApplication>,
_ presentingViewController: UIViewController?)
{
// target App Bundle doesn't contain extensions so don't bother
guard !targetAppBundle.appExtensions.isEmpty else {
return self.finish(.success(()))
}
// process extensionsInfo
let excessExtensions = processExtensionsInfo(from: targetAppBundle, localAppExtensions: localAppExtensions)
DispatchQueue.main.async {
guard let presentingViewController: UIViewController = presentingViewController,
presentingViewController.viewIfLoaded?.window != nil else {
// background mode: remove only the excess extensions automatically for re-installs
// keep all extensions for fresh install (localAppBundle = nil)
return self.backgroundModeExtensionsCleanup(excessExtensions: excessExtensions)
}
// present prompt to the user if we have a view context
let alertController = self.createAlertDialog(from: targetAppBundle, extensions: extensions, presentingViewController)
presentingViewController.present(alertController, animated: true){
// if for any reason the view wasn't presented, then just signal that as error
if presentingViewController.presentedViewController == nil {
let errMsg = "RemoveAppExtensionsOperation: unable to present dialog, view context not available." +
"\nDid you move to different screen or background after starting the operation?"
self.finish(.failure(
OperationError.invalidOperationContext(errMsg)
))
}
}
}
}
private func createAlertDialog(from targetAppBundle: ALTApplication,
extensions: Set<ALTApplication>,
_ presentingViewController: UIViewController) -> UIAlertController
{
/// Foreground prompt:
let firstSentence: String
if UserDefaults.standard.activeAppLimitIncludesExtensions
{
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions.", comment: "")
}
else
{
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to creating 10 App IDs per week.", comment: "")
}
let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit? There are \(extensions.count) Extensions", comment: "")
let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in
self.finish(.failure(OperationError.cancelled))
}))
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in
self.finish(.success(()))
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in
do {
try Self.removeExtensions(from: targetAppBundle.appExtensions)
return self.finish(.success(()))
} catch {
return self.finish(.failure(error))
}
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Choose App Extensions", comment: ""), style: .default) { (action) in
let popoverContentController = AppExtensionViewHostingController(extensions: extensions) { (selection) in
do {
try Self.removeExtensions(from: Set(selection))
return self.finish(.success(()))
} catch {
return self.finish(.failure(error))
}
}
let suiview = popoverContentController.view!
suiview.translatesAutoresizingMaskIntoConstraints = false
popoverContentController.modalPresentationStyle = .popover
if let popoverPresentationController = popoverContentController.popoverPresentationController {
popoverPresentationController.sourceView = presentingViewController.view
popoverPresentationController.sourceRect = CGRect(x: 50, y: 50, width: 4, height: 4)
popoverPresentationController.delegate = popoverContentController
DispatchQueue.main.async {
presentingViewController.present(popoverContentController, animated: true)
}
}else{
self.finish(.failure(
OperationError.invalidParameters("RemoveAppExtensionsOperation: popoverContentController.popoverPresentationController is nil"))
)
}
})
return alertController
}
struct ExtensionsInfo{
let excessInTarget: Set<ALTApplication>
let necessaryInExisting: Set<ALTApplication>
}
private func processExtensionsInfo(from targetAppBundle: ALTApplication,
localAppExtensions: Set<ALTApplication>?) -> Set<ALTApplication>
{
//App-Extensions: Ensure existing app's extensions in DB and currently installing app bundle's extensions must match
let targetAppEx: Set<ALTApplication> = targetAppBundle.appExtensions
let targetAppExNames = targetAppEx.map{ appEx in appEx.bundleIdentifier}
guard let extensionsInExistingApp = localAppExtensions else {
let diagnosticsMsg = "RemoveAppExtensionsOperation: ExistingApp is nil, Hence keeping all app extensions from targetAppBundle"
+ "RemoveAppExtensionsOperation: ExistingAppEx: nil; targetAppBundleEx: \(targetAppExNames)"
print(diagnosticsMsg)
return Set() // nothing is excess since we are keeping all, so returning empty
}
let existingAppEx: Set<ALTApplication> = extensionsInExistingApp
let existingAppExNames = existingAppEx.map{ appEx in appEx.bundleIdentifier}
let excessExtensionsInTargetApp = targetAppEx.filter{
!(existingAppExNames.contains($0.bundleIdentifier))
}
let isMatching = (targetAppEx.count == existingAppEx.count) && excessExtensionsInTargetApp.isEmpty
let diagnosticsMsg = "RemoveAppExtensionsOperation: App Extensions in localAppBundle and targetAppBundle are matching: \(isMatching)\n"
+ "RemoveAppExtensionsOperation: \nlocalAppBundleEx: \(existingAppExNames); \ntargetAppBundleEx: \(String(describing: targetAppExNames))\n"
print(diagnosticsMsg)
return excessExtensionsInTargetApp
}
private func backgroundModeExtensionsCleanup(excessExtensions: Set<ALTApplication>) {
// perform silent extensions cleanup for those that aren't already present in existing app
print("\n Performing background mode Extensions removal \n")
print("RemoveAppExtensionsOperation: Excess Extensions In TargetAppBundle: \(excessExtensions.map{$0.bundleIdentifier})")
do {
try Self.removeExtensions(from: excessExtensions)
return self.finish(.success(()))
} catch {
return self.finish(.failure(error))
}
}
}

View File

@@ -244,6 +244,7 @@ private extension ResignAppOperation
{
for case let fileURL as URL in enumerator
{
// for both sim and device, in debug mode builds, remove the tests bundles (if any)
#if DEBUG
guard !fileURL.lastPathComponent.lowercased().contains(".xctest") else {
// Remove embedded XCTest (+ dSYM) bundle from copied app bundle.

View File

@@ -0,0 +1,226 @@
//
// ConsoleLogView.swift
// AltStore
//
// Created by Magesh K on 29/12/24.
// Copyright © 2024 SideStore. All rights reserved.
//
import SwiftUI
class ConsoleLogViewModel: ObservableObject {
@Published var logLines: [String] = []
@Published var searchTerm: String = ""
@Published var currentSearchIndex: Int = 0
@Published var searchResults: [Int] = [] // Stores indices of matching lines
private var fileWatcher: DispatchSourceFileSystemObject?
private let backgroundQueue = DispatchQueue(label: "com.myapp.backgroundQueue", qos: .background)
private var logURL: URL
init(logURL: URL) {
self.logURL = logURL
startFileWatcher() // Start monitoring the log file for changes
reloadLogData() // Load initial log data
}
private func startFileWatcher() {
let fileDescriptor = open(logURL.path, O_RDONLY)
guard fileDescriptor != -1 else {
print("Unable to open file for reading.")
return
}
fileWatcher = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fileDescriptor, eventMask: .write, queue: backgroundQueue)
fileWatcher?.setEventHandler {
self.reloadLogData()
}
fileWatcher?.resume()
}
private func reloadLogData() {
if let fileContents = try? String(contentsOf: logURL) {
let lines = fileContents.split(whereSeparator: \.isNewline).map { String($0) }
DispatchQueue.main.async {
self.logLines = lines
}
}
}
deinit {
fileWatcher?.cancel()
}
func performSearch() {
searchResults = logLines.enumerated()
.filter { $0.element.localizedCaseInsensitiveContains(searchTerm) }
.map { $0.offset }
}
func nextSearchResult() {
guard !searchResults.isEmpty else { return }
currentSearchIndex = (currentSearchIndex + 1) % searchResults.count
}
func previousSearchResult() {
guard !searchResults.isEmpty else { return }
currentSearchIndex = (currentSearchIndex - 1 + searchResults.count) % searchResults.count
}
}
public struct ConsoleLogView: View {
@ObservedObject var viewModel: ConsoleLogViewModel
@State private var scrollToBottom: Bool = false // State variable to trigger scroll
@State private var searchBarState: Bool = false
@FocusState private var isSearchFieldFocused: Bool
@State private var searchText: String = ""
@State private var scrollToIndex: Int?
private let resultHighlightColor = Color.orange
private let resultHighlightOpacity = 0.5
private let otherResultsColor = Color.yellow
private let otherResultsOpacity = 0.3
init(logURL: URL) {
self.viewModel = ConsoleLogViewModel(logURL: logURL)
}
public var body: some View {
VStack {
// Custom Header Bar (similar to QuickLook's preview screen)
HStack {
Text("Console Log")
.font(.system(size: 22, weight: .semibold))
.foregroundColor(.white)
Spacer()
if(!searchBarState){
SwiftUI.Button(action: {
searchBarState.toggle()
}) {
Image(systemName: "magnifyingglass")
.foregroundColor(.white)
.imageScale(.large)
}
.padding(.trailing)
}
SwiftUI.Button(action: {
scrollToBottom.toggle()
}) {
Image(systemName: "ellipsis")
.foregroundColor(.white)
.imageScale(.large)
}
}
.padding(15)
.padding(.top, 5)
.padding(.bottom, 2.5)
.background(Color.black.opacity(0.9))
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(Color.gray.opacity(0.2)), alignment: .bottom
)
if(searchBarState){
// Search bar
HStack {
Image(systemName: "magnifyingglass")
.foregroundColor(.gray)
.padding(.trailing, 4)
TextField("Search", text: $searchText)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onChange(of: searchText) { newValue in
viewModel.searchTerm = newValue
viewModel.performSearch()
}
.keyboardShortcut("f", modifiers: .command) // Focus search field
if !searchText.isEmpty {
// Search navigation buttons
SwiftUI.Button(action: {
viewModel.previousSearchResult()
scrollToIndex = viewModel.searchResults[viewModel.currentSearchIndex]
}) {
Image(systemName: "chevron.up")
}
.keyboardShortcut(.return, modifiers: [.command, .shift])
.disabled(viewModel.searchResults.isEmpty)
SwiftUI.Button(action: {
viewModel.nextSearchResult()
scrollToIndex = viewModel.searchResults[viewModel.currentSearchIndex]
}) {
Image(systemName: "chevron.down")
}
.keyboardShortcut(.return, modifiers: .command)
.disabled(viewModel.searchResults.isEmpty)
// Results counter
Text("\(viewModel.currentSearchIndex + 1)/\(viewModel.searchResults.count)")
.foregroundColor(.gray)
.font(.caption)
}
SwiftUI.Button(action: {
searchBarState.toggle()
}) {
Image(systemName: "xmark")
}
}
.padding(.horizontal, 15)
}
// Main Log Display (scrollable area)
ScrollView(.vertical) {
ScrollViewReader { proxy in
LazyVStack(alignment: .leading, spacing: 4) {
ForEach(viewModel.logLines.indices, id: \.self) { index in
Text(viewModel.logLines[index])
.font(.system(size: 12, design: .monospaced))
.foregroundColor(.white)
.background(
viewModel.searchResults.contains(index) ?
otherResultsColor.opacity(otherResultsOpacity) : Color.clear
)
.background(
viewModel.searchResults[safe: viewModel.currentSearchIndex] == index ?
resultHighlightColor.opacity(resultHighlightOpacity) : Color.clear
)
}
}
.onChange(of: scrollToIndex) { newIndex in
if let index = newIndex {
withAnimation {
proxy.scrollTo(index, anchor: .center)
}
}
}
.onChange(of: scrollToBottom) { _ in
viewModel.logLines.indices.last.map { last in
proxy.scrollTo(last, anchor: .bottom)
}
}
}
}
}
.background(Color.black) // Set background color to mimic QL's dark theme
.edgesIgnoringSafeArea(.all)
}
}
// Helper extension for safe array access
extension Array {
subscript(safe index: Index) -> Element? {
indices.contains(index) ? self[index] : nil
}
}

View File

@@ -16,6 +16,7 @@ import Roxas
import Nuke
import QuickLook
import SwiftUI
final class ErrorLogViewController: UITableViewController, QLPreviewControllerDelegate
{
@@ -59,8 +60,46 @@ final class ErrorLogViewController: UITableViewController, QLPreviewControllerDe
// Assign just clearLogButton to hide export button.
self.navigationItem.rightBarButtonItems = [self.clearLogButton]
}
// // Adjust the width of the right bar button items
// adjustRightBarButtonWidth()
}
// func adjustRightBarButtonWidth() {
// // Access the current rightBarButtonItems
// if let rightBarButtonItems = self.navigationItem.rightBarButtonItems {
// for barButtonItem in rightBarButtonItems {
// // Check if the button is a system button, and if so, replace it with a custom button
// if barButtonItem.customView == nil {
// // Replace with a custom UIButton for each bar button item
// let customButton = UIButton(type: .custom)
// if let image = barButtonItem.image {
// customButton.setImage(image, for: .normal)
// }
// if let action = barButtonItem.action{
// customButton.addTarget(barButtonItem.target, action: action, for: .touchUpInside)
// }
//
// // Calculate the original size based on the system button
// let originalSize = customButton.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude))
//
// let scaleFactor = 0.7
//
// // Scale the size by 0.7 (70%)
// let scaledSize = CGSize(width: originalSize.width * scaleFactor, height: originalSize.height * scaleFactor)
//
// // Adjust the custom button's width
//// customButton.frame.size = CGSize(width: 22, height: 22) // Adjust width as needed
// customButton.frame.size = scaledSize // Adjust width as needed
//
// // Set the custom button as the custom view for the UIBarButtonItem
// barButtonItem.customView = customButton
// }
// }
// }
// }
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
guard let loggedError = sender as? LoggedError, segue.identifier == "showErrorDetails" else { return }
@@ -216,15 +255,86 @@ private extension ErrorLogViewController
}
}
@IBAction func showMinimuxerLogs(_ sender: UIBarButtonItem)
{
// Show minimuxer.log
let previewController = QLPreviewController()
previewController.dataSource = self
let navigationController = UINavigationController(rootViewController: previewController)
present(navigationController, animated: true, completion: nil)
enum LogView: String {
case consoleLog = "console-log"
case minimuxerLog = "minimuxer-log"
// This class will manage the QLPreviewController and the timer.
private class LogViewManager {
var previewController: QLPreviewController
var refreshTimer: Timer?
var logView: LogView
init(previewController: QLPreviewController, logView: LogView) {
self.previewController = previewController
self.logView = logView
}
// Start refreshing the preview controller every second
func startRefreshing() {
refreshTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(refreshPreview), userInfo: nil, repeats: true)
}
@objc private func refreshPreview() {
previewController.reloadData()
}
// Stop the timer to prevent leaks
func stopRefreshing() {
refreshTimer?.invalidate()
refreshTimer = nil
}
func updateLogPath() {
// Force the QLPreviewController to reload by changing the file path
previewController.reloadData()
}
}
// Method to get the QLPreviewController for this log type
func getViewController(_ dataSource: QLPreviewControllerDataSource) -> QLPreviewController {
let previewController = QLPreviewController()
previewController.restorationIdentifier = self.rawValue
previewController.dataSource = dataSource
// Create LogViewManager and start refreshing
let manager = LogViewManager(previewController: previewController, logView: self)
// manager.startRefreshing() // DO NOT REFRESH the full view contents causing flickering
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")
}
}
}
@IBAction func showConsoleLogs(_ sender: UIBarButtonItem) {
// Create the SwiftUI ConsoleLogView with the URL
let consoleLogView = ConsoleLogView(logURL: (UIApplication.shared.delegate as! AppDelegate).consoleLog.logFileURL)
// Create the UIHostingController
let consoleLogController = UIHostingController(rootView: consoleLogView)
// Configure the bottom sheet presentation
consoleLogController.modalPresentationStyle = .pageSheet
if let sheet = consoleLogController.sheetPresentationController {
sheet.detents = [.medium(), .large()] // You can adjust the size of the sheet (medium/large)
sheet.prefersGrabberVisible = true // Optional: Shows a grabber at the top of the sheet
sheet.selectedDetentIdentifier = .large // Default size when presented
}
// Present the bottom sheet
present(consoleLogController, animated: true, completion: nil)
}
@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)
@@ -285,58 +395,74 @@ private extension ErrorLogViewController
self.performSegue(withIdentifier: "showErrorDetails", sender: loggedError)
}
@available(iOS 15, *)
@IBAction func exportDetailedLog(_ sender: UIBarButtonItem)
@IBAction func showMinimuxerLogs(_ sender: UIBarButtonItem)
{
self.exportLogButton.isIndicatingActivity = true
Task<Void, Never>.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 == %@", Logger.altstoreSubsystem)
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("altstore.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
previewController.view.tintColor = .altPrimary
self.present(previewController, animated: true)
}
}
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
}
}
// Show minimuxer.log
let previewController = LogView.minimuxerLog.getViewController(self)
let navigationController = UINavigationController(rootViewController: previewController)
present(navigationController, animated: true, completion: nil)
}
// @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)
//
// // All logs since the app launched.
// let position = store.position(timeIntervalSinceLatestBoot: 0)
//// let predicate = NSPredicate(format: "subsystem == %@", Logger.altstoreSubsystem)
////
//// let entries = try store.getEntries(at: position, matching: predicate)
//// .compactMap { $0 as? OSLogEntryLog }
//// .map { "[\($0.date.formatted())] [\($0.category)] [\($0.level.localizedName)] \($0.composedMessage)" }
////
// // Remove the predicate to get all log entries
//// let entries = try store.getEntries(at: position)
//// .compactMap { $0 as? OSLogEntryLog }
//// .map { "[\($0.date.formatted())] [\($0.category)] [\($0.level.localizedName)] \($0.composedMessage)" }
//
// let entries = try store.getEntries(at: position)
//
//// let outputText = entries.joined(separator: "\n")
// let outputText = entries.map { $0.description }.joined(separator: "\n")
//
// let outputDirectory = FileManager.default.uniqueTemporaryURL()
// try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
//
// let outputURL = outputDirectory.appendingPathComponent("altstore.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
// previewController.view.tintColor = .altPrimary
// self.present(previewController, animated: true)
// }
// }
// 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
// }
// }
// }
}
extension ErrorLogViewController
@@ -412,9 +538,13 @@ extension ErrorLogViewController: QLPreviewControllerDataSource {
return 1
}
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
let fileURL = FileManager.default.documentsDirectory.appendingPathComponent("minimuxer.log")
return fileURL as QLPreviewItem
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
}
}

View File

@@ -0,0 +1,284 @@
//
// SettingsView.swift
// AltStore
//
// Created by Magesh K on 14/01/25.
// Copyright © 2025 SideStore. All rights reserved.
//
import SwiftUI
import AltStoreCore
private final class DummyConformance: EnableJITContext
{
private init(){} // non instantiatable
var installedApp: AltStoreCore.InstalledApp?
var error: (any Error)?
}
struct OperationsLoggingControlView: View {
let TITLE = "Operations Logging"
let BACKGROUND_COLOR = Color(.settingsBackground)
var viewModel = OperationsLoggingControl()
var body: some View {
NavigationView {
ZStack {
// BACKGROUND_COLOR.ignoresSafeArea() // Force background to cover the entire screen
VStack{
Group{}.padding(12)
CustomList {
CustomSection(header: Text("Install Operations"))
{
CustomToggle("1. Authentication", isOn: Binding(
get: { self.viewModel.getFromDatabase(for: AuthenticationOperation.self) },
set: { value in
self.viewModel.updateDatabase(for: AuthenticationOperation.self, value: value)
}
))
CustomToggle("2. VerifyAppPledge", isOn: Binding(
get: { self.viewModel.getFromDatabase(for: VerifyAppPledgeOperation.self) },
set: { value in
self.viewModel.updateDatabase(for: VerifyAppPledgeOperation.self, value: value)
}
))
CustomToggle("3. DownloadApp", isOn: Binding(
get: { self.viewModel.getFromDatabase(for: DownloadAppOperation.self) },
set: { value in
self.viewModel.updateDatabase(for: DownloadAppOperation.self, value: value)
}
))
CustomToggle("4. VerifyApp", isOn: Binding(
get: { self.viewModel.getFromDatabase(for: VerifyAppOperation.self) },
set: { value in
self.viewModel.updateDatabase(for: VerifyAppOperation.self, value: value)
}
))
CustomToggle("5. RemoveAppExtensions", isOn: Binding(
get: { self.viewModel.getFromDatabase(for: RemoveAppExtensionsOperation.self) },
set: { value in
self.viewModel.updateDatabase(for: RemoveAppExtensionsOperation.self, value: value)
}
))
CustomToggle("6. FetchAnisetteData", isOn: Binding(
get: { self.viewModel.getFromDatabase(for: FetchAnisetteDataOperation.self) },
set: { value in
self.viewModel.updateDatabase(for: FetchAnisetteDataOperation.self, value: value)
}
))
CustomToggle("7. FetchProvisioningProfiles(I)", isOn: Binding(
get: { self.viewModel.getFromDatabase(for: FetchProvisioningProfilesInstallOperation.self) },
set: { value in
self.viewModel.updateDatabase(for: FetchProvisioningProfilesInstallOperation.self, value: value)
}
))
CustomToggle("8. ResignApp", isOn: Binding(
get: { self.viewModel.getFromDatabase(for: ResignAppOperation.self) },
set: { value in
self.viewModel.updateDatabase(for: ResignAppOperation.self, value: value)
}
))
CustomToggle("9. SendApp", isOn: Binding(
get: { self.viewModel.getFromDatabase(for: SendAppOperation.self) },
set: { value in
self.viewModel.updateDatabase(for: SendAppOperation.self, value: value)
}
))
CustomToggle("10. InstallApp", isOn: Binding(
get: { self.viewModel.getFromDatabase(for: InstallAppOperation.self) },
set: { value in
self.viewModel.updateDatabase(for: InstallAppOperation.self, value: value)
}
))
}
CustomSection(header: Text("Refresh Operations"))
{
CustomToggle("1. FetchProvisioningProfiles(R)", isOn: Binding(
get: { self.viewModel.getFromDatabase(for: FetchProvisioningProfilesRefreshOperation.self) },
set: { value in
self.viewModel.updateDatabase(for: FetchProvisioningProfilesRefreshOperation.self, value: value)
}
))
CustomToggle("2. RefreshApp", isOn: Binding(
get: { self.viewModel.getFromDatabase(for: RefreshAppOperation.self) },
set: { value in
self.viewModel.updateDatabase(for: RefreshAppOperation.self, value: value)
}
))
}
CustomSection(header: Text("AppIDs related Operations"))
{
CustomToggle("1. FetchAppIDs", isOn: Binding(
get: { self.viewModel.getFromDatabase(for: FetchAppIDsOperation.self) },
set: { value in
self.viewModel.updateDatabase(for: FetchAppIDsOperation.self, value: value)
}
))
}
CustomSection(header: Text("Sources related Operations"))
{
CustomToggle("1. FetchSource", isOn: Binding(
get: { self.viewModel.getFromDatabase(for: FetchSourceOperation.self) },
set: { value in
self.viewModel.updateDatabase(for: FetchSourceOperation.self, value: value)
}
))
CustomToggle("2. UpdateKnownSources", isOn: Binding(
get: { self.viewModel.getFromDatabase(for: UpdateKnownSourcesOperation.self) },
set: { value in
self.viewModel.updateDatabase(for: UpdateKnownSourcesOperation.self, value: value)
}
))
}
CustomSection(header: Text("Backup Operations"))
{
CustomToggle("1. BackupApp", isOn: Binding(
get: { self.viewModel.getFromDatabase(for: BackupAppOperation.self) },
set: { value in
self.viewModel.updateDatabase(for: BackupAppOperation.self, value: value)
}
))
CustomToggle("2. RemoveAppBackup", isOn: Binding(
get: { self.viewModel.getFromDatabase(for: RemoveAppBackupOperation.self) },
set: { value in
self.viewModel.updateDatabase(for: RemoveAppBackupOperation.self, value: value)
}
))
}
CustomSection(header: Text("Activate/Deactive Operations"))
{
CustomToggle("1. RemoveApp", isOn: Binding(
get: { self.viewModel.getFromDatabase(for: RemoveAppOperation.self) },
set: { value in
self.viewModel.updateDatabase(for: RemoveAppOperation.self, value: value)
}
))
CustomToggle("2. DeactivateApp", isOn: Binding(
get: { self.viewModel.getFromDatabase(for: DeactivateAppOperation.self) },
set: { value in
self.viewModel.updateDatabase(for: DeactivateAppOperation.self, value: value)
}
))
}
CustomSection(header: Text("Background refresh Operations"))
{
CustomToggle("1. BackgroundRefreshApps", isOn: Binding(
get: { self.viewModel.getFromDatabase(for: BackgroundRefreshAppsOperation.self) },
set: { value in
self.viewModel.updateDatabase(for: BackgroundRefreshAppsOperation.self, value: value)
}
))
}
CustomSection(header: Text("Enable JIT Operations"))
{
CustomToggle("1. EnableJIT", isOn: Binding(
get: { self.viewModel.getFromDatabase(for: EnableJITOperation<DummyConformance>.self) },
set: { value in
self.viewModel.updateDatabase(for: EnableJITOperation<DummyConformance>.self, value: value)
}
))
}
CustomSection(header: Text("Patrons Operations"))
{
CustomToggle("1. UpdatePatrons", isOn: Binding(
get: { self.viewModel.getFromDatabase(for: UpdatePatronsOperation.self) },
set: { value in
self.viewModel.updateDatabase(for: UpdatePatronsOperation.self, value: value)
}
))
}
CustomSection(header: Text("Cache Operations"))
{
CustomToggle("1. ClearAppCache", isOn: Binding(
get: { self.viewModel.getFromDatabase(for: ClearAppCacheOperation.self) },
set: { value in
self.viewModel.updateDatabase(for: ClearAppCacheOperation.self, value: value)
}
))
}
CustomSection(header: Text("Misc Logging"))
{
CustomToggle("1. Anisette Internal Logging", isOn: Binding(
// enable anisette internal logging by default since it was already printing before
get: { OperationsLoggingControl.getUpdatedFromDatabase(
for: ANISETTE_VERBOSITY.self, defaultVal: true
)},
set: { value in
self.viewModel.updateDatabase(for: ANISETTE_VERBOSITY.self, value: value)
}
))
}
}
}
}
.navigationTitle(TITLE)
}
.ignoresSafeArea(edges: .all)
}
private func CustomList<Content: View>(@ViewBuilder content: () -> Content) -> some View {
// ScrollView {
List {
content()
}
// .listStyle(.plain)
// .listStyle(InsetGroupedListStyle()) // Or PlainListStyle for iOS 15
// .background(Color.clear)
// .background(Color(.settingsBackground))
// .onAppear(perform: {
// // cache the current background color
// UITableView.appearance().backgroundColor = UIColor.red
// })
// .onDisappear(perform: {
// // reset the background color to the cached value
// UITableView.appearance().backgroundColor = UIColor.systemBackground
// })
}
private func CustomSection<Content: View>(header: Text, @ViewBuilder content: () -> Content) -> some View {
Section(header: header) {
content()
}
// .listRowBackground(Color.clear)
}
private func CustomToggle(_ title: String, isOn: Binding<Bool>) -> some View {
Toggle(title, isOn: isOn)
.padding(3)
// .foregroundColor(.white) // Ensures text color is always white
// .font(.headline)
}
}
struct SettingsView_Previews: PreviewProvider {
static var previews: some View {
OperationsLoggingControlView()
}
}

View File

@@ -318,7 +318,8 @@ extension PatreonViewController
case .none: footerView.button.isIndicatingActivity = true
case .success?: footerView.button.isHidden = true
case .failure?:
#if DEBUG
// In simulator debug builds only enable debug mode flag
#if DEBUG && targetEnvironment(simulator)
let debug = true
#else
let debug = false

View File

@@ -4,6 +4,7 @@
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
<capability name="Image references" minToolsVersion="12.0"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
@@ -21,7 +22,7 @@
<color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<color key="separatorColor" white="1" alpha="0.25" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<stackView key="tableFooterView" opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="equalCentering" alignment="center" spacing="15" id="48g-cT-stR">
<rect key="frame" x="0.0" y="1618.0000038146973" width="402" height="125"/>
<rect key="frame" x="0.0" y="1726.3333358764648" width="402" height="125"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="900" text="Follow SideStore for updates" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XFa-MY-7cV">
@@ -91,8 +92,8 @@
</button>
</subviews>
</stackView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="900" text="Version 2.0" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5u7-mb-jJj">
<rect key="frame" x="165" y="108" width="72.333333333333314" height="17"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="900" text="Version 0.6.0" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="5u7-mb-jJj">
<rect key="frame" x="159" y="108" width="84" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="0.69999999999999996" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
@@ -345,7 +346,6 @@
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="1"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="NO"/>
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="GYp-O0-pse" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
@@ -381,7 +381,6 @@
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="2"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="amC-sE-8O0" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
@@ -445,7 +444,6 @@
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="3"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes>
</tableViewCell>
</cells>
@@ -499,14 +497,14 @@
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="View Error Log" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vH6-7i-tCE">
<rect key="frame" x="30" y="15.5" width="119" height="20.5"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="View Error Log" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vH6-7i-tCE">
<rect key="frame" x="30" y="15.333333333333334" width="119" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="AeT-qF-bwB">
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="AeT-qF-bwB">
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
</imageView>
</subviews>
<constraints>
@@ -535,8 +533,8 @@
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Clear Cache…" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="j4e-Mz-DlL">
<rect key="frame" x="30" y="15.5" width="114.5" height="20.5"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Clear Cache…" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="j4e-Mz-DlL">
<rect key="frame" x="29.999999999999993" y="15.333333333333334" width="114.33333333333331" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
@@ -567,23 +565,23 @@
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Developers" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hRA-OK-Vjw">
<rect key="frame" x="30" y="15.5" width="86" height="20.5"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Developers" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hRA-OK-Vjw">
<rect key="frame" x="30" y="15.333333333333334" width="86" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="lx9-35-OSk">
<rect key="frame" x="187.5" y="15.5" width="157.5" height="20.5"/>
<stackView opaque="NO" contentMode="scaleToFill" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="lx9-35-OSk">
<rect key="frame" x="214.66666666666663" y="15.333333333333334" width="157.33333333333337" height="20.333333333333329"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="SideStore Team" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JAA-iZ-VGb">
<rect key="frame" x="0.0" y="0.0" width="125.5" height="20.5"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SideStore Team" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JAA-iZ-VGb">
<rect key="frame" x="0.0" y="0.0" width="125.33333333333333" height="20.333333333333332"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="Mmj-3V-fTb">
<rect key="frame" x="139.5" y="0.0" width="18" height="20.5"/>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="Mmj-3V-fTb">
<rect key="frame" x="139.33333333333334" y="0.0" width="18" height="20.333333333333332"/>
</imageView>
</subviews>
</stackView>
@@ -611,23 +609,23 @@
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="UI Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oqY-wY-1Vf">
<rect key="frame" x="30" y="15.5" width="89" height="20.5"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="UI Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oqY-wY-1Vf">
<rect key="frame" x="30" y="15.333333333333334" width="89" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="gUq-6Q-t5X">
<rect key="frame" x="198" y="15.5" width="147" height="20.5"/>
<stackView opaque="NO" contentMode="scaleToFill" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="gUq-6Q-t5X">
<rect key="frame" x="225" y="15.333333333333334" width="147" height="20.333333333333329"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Fabian (thdev)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ylE-VL-7Fq">
<rect key="frame" x="0.0" y="0.0" width="115" height="20.5"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Fabian (thdev)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ylE-VL-7Fq">
<rect key="frame" x="0.0" y="0.0" width="115" height="20.333333333333332"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="e3L-vR-Jae">
<rect key="frame" x="129" y="0.0" width="18" height="20.5"/>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="e3L-vR-Jae">
<rect key="frame" x="129" y="0.0" width="18" height="20.333333333333332"/>
</imageView>
</subviews>
</stackView>
@@ -655,23 +653,23 @@
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Asset Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fGU-Fp-XgM">
<rect key="frame" x="30" y="15.5" width="115.5" height="20.5"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Asset Designer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fGU-Fp-XgM">
<rect key="frame" x="29.999999999999993" y="15.333333333333334" width="115.33333333333331" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="R8B-DW-7mY">
<rect key="frame" x="206" y="15.5" width="139" height="20.5"/>
<stackView opaque="NO" contentMode="scaleToFill" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="R8B-DW-7mY">
<rect key="frame" x="233" y="15.333333333333334" width="139" height="20.333333333333329"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Chris (LitRitt)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hId-3P-41T">
<rect key="frame" x="0.0" y="0.0" width="107" height="20.5"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Chris (LitRitt)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hId-3P-41T">
<rect key="frame" x="0.0" y="0.0" width="107" height="20.333333333333332"/>
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="baq-cE-fMY">
<rect key="frame" x="121" y="0.0" width="18" height="20.5"/>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="baq-cE-fMY">
<rect key="frame" x="121" y="0.0" width="18" height="20.333333333333332"/>
</imageView>
</subviews>
</stackView>
@@ -699,14 +697,14 @@
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Licenses" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="D6b-cd-pVK">
<rect key="frame" x="30" y="15.5" width="67.5" height="20.5"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Licenses" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="D6b-cd-pVK">
<rect key="frame" x="30" y="15.333333333333334" width="67.333333333333329" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="0.80000000000000004" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="s79-GQ-khr">
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="s79-GQ-khr">
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
</imageView>
</subviews>
<constraints>
@@ -740,13 +738,13 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Send Feedback" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pMI-Aj-nQF">
<rect key="frame" x="30" y="15.5" width="125.5" height="20.5"/>
<rect key="frame" x="29.999999999999993" y="15.333333333333334" width="125.33333333333331" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="Jyy-x0-Owj">
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
</imageView>
</subviews>
<constraints>
@@ -773,13 +771,13 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="View Refresh Attempts" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sni-07-q0M">
<rect key="frame" x="30" y="15.5" width="187.5" height="20.5"/>
<rect key="frame" x="30" y="15.333333333333334" width="187.66666666666666" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="4d3-me-Hqc">
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
</imageView>
</subviews>
<constraints>
@@ -809,13 +807,13 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="SideJITServer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="46q-DB-5nc">
<rect key="frame" x="30" y="15.5" width="115.5" height="20.5"/>
<rect key="frame" x="29.999999999999993" y="15.333333333333334" width="115.33333333333331" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="wvD-eZ-nQI">
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
</imageView>
</subviews>
<constraints>
@@ -842,13 +840,13 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Reset Pairing File" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ysS-9s-dXm">
<rect key="frame" x="30" y="15.5" width="140" height="20.5"/>
<rect key="frame" x="30" y="15.333333333333334" width="140" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="r09-mH-pOD">
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
</imageView>
</subviews>
<constraints>
@@ -875,13 +873,13 @@
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Anisette Servers" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="eds-Dj-36y">
<rect key="frame" x="30" y="15.5" width="135.5" height="20.5"/>
<rect key="frame" x="30" y="15.333333333333334" width="135.66666666666666" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="0dh-yd-7i9">
<rect key="frame" x="327" y="16.5" width="18" height="18"/>
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
</imageView>
</subviews>
<constraints>
@@ -900,57 +898,21 @@
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="XW5-Zc-nMH" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1454.0000038146973" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="XW5-Zc-nMH" id="AtM-bL-7pS">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Disable Response Caching" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2ox-HD-0UT">
<rect key="frame" x="30" y="15.5" width="215.5" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="e30-w4-5fk">
<rect key="frame" x="296" y="10" width="51" height="31"/>
<connections>
<action selector="toggleDisableResponseCaching:" destination="aMk-Xp-UL8" eventType="valueChanged" id="uuG-Gf-7GK"/>
</connections>
</switch>
</subviews>
<constraints>
<constraint firstItem="2ox-HD-0UT" firstAttribute="centerY" secondItem="AtM-bL-7pS" secondAttribute="centerY" id="07n-jt-3rz"/>
<constraint firstItem="2ox-HD-0UT" firstAttribute="leading" secondItem="AtM-bL-7pS" secondAttribute="leadingMargin" id="Koi-9G-bG8"/>
<constraint firstAttribute="trailingMargin" secondItem="e30-w4-5fk" secondAttribute="trailing" id="Wa7-n6-lcl"/>
<constraint firstItem="e30-w4-5fk" firstAttribute="centerY" secondItem="AtM-bL-7pS" secondAttribute="centerY" id="n7R-aQ-FBX"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="2"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="NO"/>
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="XW5-Zc-nXH" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1505.0000038146973" width="402" height="51"/>
<rect key="frame" x="0.0" y="1454.0000038146973" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="XW5-Zc-nXH" id="AtM-bL-8pS">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Enable Beta Updates" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2px-HD-0UT">
<rect key="frame" x="30" y="15.5" width="215.5" height="20.5"/>
<rect key="frame" x="30" y="15.333333333333334" width="169" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="e32-w4-5fk">
<rect key="frame" x="296" y="10" width="51" height="31"/>
<rect key="frame" x="323" y="10" width="51" height="31"/>
<connections>
<action selector="toggleEnableBetaUpdates:" destination="aMk-Xp-UL8" eventType="valueChanged" id="uxG-df-7GK"/>
</connections>
@@ -967,36 +929,210 @@
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="2"/>
<integer key="value" value="3"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="NO"/>
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="XW5-Zc-tXH" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1556.0000038146973" width="402" height="51"/>
</cells>
</tableViewSection>
<tableViewSection headerTitle="" id="lLQ-K0-XSb">
<cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="daQ-mk-yqC" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1545.3333377838135" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="XW5-Zc-tXH" id="AtM-bL-4pS">
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="daQ-mk-yqC" id="ZkW-ZR-twy">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Export Resigned Apps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2px-HX-0UT">
<rect key="frame" x="30" y="15.5" width="215.5" height="20.5"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Disable Response Caching" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="jFh-36-AP2">
<rect key="frame" x="30.000000000000014" y="15.333333333333334" width="215.33333333333337" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="e32-w4-3fk">
<rect key="frame" x="296" y="10" width="51" height="31"/>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="AAh-cu-qw8">
<rect key="frame" x="323" y="10" width="51" height="31"/>
<connections>
<action selector="toggleResignedAppExport:" destination="aMk-Xp-UL8" eventType="valueChanged" id="uxG-af-7GK"/>
<action selector="toggleDisableResponseCaching:" destination="aMk-Xp-UL8" eventType="valueChanged" id="lCm-qi-piH"/>
</connections>
</switch>
</subviews>
<constraints>
<constraint firstItem="2px-HX-0UT" firstAttribute="centerY" secondItem="AtM-bL-4pS" secondAttribute="centerY" id="07r-jt-8rz"/>
<constraint firstItem="2px-HX-0UT" firstAttribute="leading" secondItem="AtM-bL-4pS" secondAttribute="leadingMargin" id="K1i-9G-bG8"/>
<constraint firstAttribute="trailingMargin" secondItem="e32-w4-3fk" secondAttribute="trailing" id="Wa7-m1-lcl"/>
<constraint firstItem="e32-w4-3fk" firstAttribute="centerY" secondItem="AtM-bL-4pS" secondAttribute="centerY" id="n8R-av-FBX"/>
<constraint firstItem="jFh-36-AP2" firstAttribute="centerY" secondItem="ZkW-ZR-twy" secondAttribute="centerY" id="2u3-2Y-3VF"/>
<constraint firstItem="jFh-36-AP2" firstAttribute="leading" secondItem="ZkW-ZR-twy" secondAttribute="leadingMargin" id="98e-6Q-2Wd"/>
<constraint firstAttribute="trailingMargin" secondItem="AAh-cu-qw8" secondAttribute="trailing" id="GjM-1M-598"/>
<constraint firstItem="AAh-cu-qw8" firstAttribute="centerY" secondItem="ZkW-ZR-twy" secondAttribute="centerY" id="X4E-o0-tJC"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="1"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="hRP-jU-2hd" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1596.3333377838135" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="hRP-jU-2hd" id="JhE-O4-pRg">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Export Resigned Apps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="d5F-bf-6kB">
<rect key="frame" x="30" y="15.333333333333334" width="180" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="GYP-qn-wzh">
<rect key="frame" x="323" y="10" width="51" height="31"/>
<connections>
<action selector="toggleResignedAppExport:" destination="aMk-Xp-UL8" eventType="valueChanged" id="Z1k-xh-sjD"/>
</connections>
</switch>
</subviews>
<constraints>
<constraint firstItem="GYP-qn-wzh" firstAttribute="centerY" secondItem="JhE-O4-pRg" secondAttribute="centerY" id="6hG-aB-zmb"/>
<constraint firstAttribute="trailingMargin" secondItem="GYP-qn-wzh" secondAttribute="trailing" id="Bci-7r-MMJ"/>
<constraint firstItem="d5F-bf-6kB" firstAttribute="centerY" secondItem="JhE-O4-pRg" secondAttribute="centerY" id="g1h-ov-Teh"/>
<constraint firstItem="d5F-bf-6kB" firstAttribute="leading" secondItem="JhE-O4-pRg" secondAttribute="leadingMargin" id="rFw-nM-3Og"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="2"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="JoN-Aj-XtZ" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1647.3333377838135" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="JoN-Aj-XtZ" id="v8Q-VQ-Q1h">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Enable Verbose Ops Logging" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7bz-tI-tLY">
<rect key="frame" x="29.999999999999986" y="15.333333333333334" width="232.66666666666663" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Q5X-Mo-KpE">
<rect key="frame" x="323" y="10" width="51" height="31"/>
<connections>
<action selector="toggleVerboseOperationsLogging:" destination="aMk-Xp-UL8" eventType="valueChanged" id="n9N-Gt-OY2"/>
</connections>
</switch>
</subviews>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="Q5X-Mo-KpE" secondAttribute="trailing" id="1bV-B0-S5q"/>
<constraint firstItem="7bz-tI-tLY" firstAttribute="centerY" secondItem="v8Q-VQ-Q1h" secondAttribute="centerY" id="FJj-Bb-xmv"/>
<constraint firstItem="7bz-tI-tLY" firstAttribute="leading" secondItem="v8Q-VQ-Q1h" secondAttribute="leadingMargin" id="m5U-ml-KNJ"/>
<constraint firstItem="Q5X-Mo-KpE" firstAttribute="centerY" secondItem="v8Q-VQ-Q1h" secondAttribute="centerY" id="och-PX-wo9"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="2"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="QOO-bO-4M5" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1698.3333377838135" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="QOO-bO-4M5" id="VTT-z5-C89">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Export SqLite DB" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Ho1-To-wve">
<rect key="frame" x="30" y="15.333333333333334" width="137.66666666666666" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="wfX-fH-gXe">
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
</imageView>
</subviews>
<constraints>
<constraint firstItem="Ho1-To-wve" firstAttribute="leading" secondItem="VTT-z5-C89" secondAttribute="leadingMargin" id="50N-ql-gna"/>
<constraint firstAttribute="trailingMargin" secondItem="wfX-fH-gXe" secondAttribute="trailing" id="9fe-Pw-SAN"/>
<constraint firstItem="wfX-fH-gXe" firstAttribute="centerY" secondItem="VTT-z5-C89" secondAttribute="centerY" id="LPh-vG-0sK"/>
<constraint firstItem="Ho1-To-wve" firstAttribute="centerY" secondItem="VTT-z5-C89" secondAttribute="centerY" id="eYD-QD-yYa"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="2"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="xtI-eU-LFb" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1749.3333377838135" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="xtI-eU-LFb" id="bc9-41-6mE">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Operations Logging Control" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="LW3-gm-lj5">
<rect key="frame" x="30.000000000000014" y="15.333333333333334" width="224.33333333333337" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" image="Next" translatesAutoresizingMaskIntoConstraints="NO" id="zl4-ti-HTW">
<rect key="frame" x="354" y="16.666666666666668" width="18" height="18.000000000000004"/>
</imageView>
</subviews>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="zl4-ti-HTW" secondAttribute="trailing" id="2MC-2C-NDd"/>
<constraint firstItem="LW3-gm-lj5" firstAttribute="centerY" secondItem="bc9-41-6mE" secondAttribute="centerY" id="4Ft-s9-hPY"/>
<constraint firstItem="zl4-ti-HTW" firstAttribute="centerY" secondItem="bc9-41-6mE" secondAttribute="centerY" id="8Gt-oe-nd0"/>
<constraint firstItem="LW3-gm-lj5" firstAttribute="leading" secondItem="bc9-41-6mE" secondAttribute="leadingMargin" id="Pg6-I4-3d4"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="2"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="pvu-IV-Poa" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="1800.3333377838135" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="pvu-IV-Poa" id="zck-an-8cK">
<rect key="frame" x="0.0" y="0.0" width="402" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" ambiguous="YES" text="Minimuxer Console Logging" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ZRk-8S-kBQ">
<rect key="frame" x="30.000000000000014" y="15.333333333333334" width="225.33333333333337" height="20.333333333333329"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="uGv-Lb-Ita">
<rect key="frame" x="323" y="10" width="51" height="31"/>
<connections>
<action selector="toggleMinimuxerConsoleLogging:" destination="aMk-Xp-UL8" eventType="valueChanged" id="jOU-Ic-46O"/>
</connections>
</switch>
</subviews>
<constraints>
<constraint firstItem="ZRk-8S-kBQ" firstAttribute="leading" secondItem="zck-an-8cK" secondAttribute="leadingMargin" id="Ogt-dT-Z8F"/>
<constraint firstItem="uGv-Lb-Ita" firstAttribute="centerY" secondItem="zck-an-8cK" secondAttribute="centerY" id="UJV-OX-oF6"/>
<constraint firstAttribute="trailingMargin" secondItem="uGv-Lb-Ita" secondAttribute="trailing" id="hEe-F9-T6Z"/>
<constraint firstItem="ZRk-8S-kBQ" firstAttribute="centerY" secondItem="zck-an-8cK" secondAttribute="centerY" id="kVs-il-AuO"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -1005,7 +1141,6 @@
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="3"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="NO"/>
</userDefinedRuntimeAttributes>
</tableViewCell>
</cells>
@@ -1022,15 +1157,17 @@
<outlet property="accountNameLabel" destination="CnN-M1-AYK" id="Ldc-Py-Bix"/>
<outlet property="accountTypeLabel" destination="434-MW-Den" id="mNB-QE-4Jg"/>
<outlet property="backgroundRefreshSwitch" destination="DPu-zD-Als" id="eiG-Hv-Vko"/>
<outlet property="betaUpdatesSwitch" destination="e32-w4-5fk" id="Dty-Yb-eo1"/>
<outlet property="disableAppLimitSwitch" destination="1aa-og-ZXD" id="oVL-Md-yZ8"/>
<outlet property="disableResponseCachingSwitch" destination="e30-w4-5fk" id="Duy-Yb-eo1"/>
<outlet property="disableResponseCachingSwitch" destination="AAh-cu-qw8" id="aVT-Md-yZ8"/>
<outlet property="exportResignedAppsSwitch" destination="GYP-qn-wzh" id="aVL-Md-yZ8"/>
<outlet property="githubButton" destination="oqj-4S-I9l" id="sxB-LE-gA2"/>
<outlet property="isBetaUpdatesEnabled" destination="e32-w4-5fk" id="Dty-Yb-eo1"/>
<outlet property="isResignedAppExportEnabled" destination="e32-w4-3fk" id="Dxy-Yb-eo1"/>
<outlet property="mastodonButton" destination="B8Q-e7-beR" id="Kbe-Og-rsg"/>
<outlet property="minimuxerConsoleLoggingSwitch" destination="uGv-Lb-Ita" id="aTL-Md-tZ8"/>
<outlet property="noIdleTimeoutSwitch" destination="iQA-wm-5ag" id="jHC-js-q0Y"/>
<outlet property="threadsButton" destination="AWk-yE-9LI" id="SOc-ei-4gK"/>
<outlet property="twitterButton" destination="uYZ-Vu-RzK" id="anA-jh-w4z"/>
<outlet property="verboseOperationsLoggingSwitch" destination="Q5X-Mo-KpE" id="aVL-Md-tZ8"/>
<outlet property="versionLabel" destination="5u7-mb-jJj" id="zvU-TQ-lO6"/>
</connections>
</tableViewController>
@@ -1444,19 +1581,22 @@ Settings by i cons from the Noun Project</string>
</tableView>
<navigationItem key="navigationItem" title="Error Log" largeTitleDisplayMode="never" id="a1p-3W-bSi">
<rightBarButtonItems>
<barButtonItem systemItem="trash" id="BnQ-Eh-1gC">
<barButtonItem title="trash" id="BnQ-Eh-1gC">
<imageReference key="image" image="trash" catalog="system" symbolScale="large"/>
<connections>
<action selector="clearLoggedErrors:" destination="g8a-Rf-zWa" id="faq-89-H5j"/>
</connections>
</barButtonItem>
<barButtonItem image="ladybug" catalog="system" id="1cD-4y-vTJ" userLabel="Share">
<barButtonItem title="minimuxer" id="BNj-HE-KHr" userLabel="Minimuxer Log Button">
<imageReference key="image" image="ladybug" catalog="system" symbolScale="large"/>
<connections>
<action selector="showMinimuxerLogs:" destination="g8a-Rf-zWa" id="V0f-0y-C6C"/>
<action selector="showMinimuxerLogs:" destination="g8a-Rf-zWa" id="Kbw-Q5-9WO"/>
</connections>
</barButtonItem>
<barButtonItem systemItem="action" id="BNj-HE-KHr">
<barButtonItem title="console" id="1cD-4y-vTJ" userLabel="Console Log Button">
<imageReference key="image" image="terminal" catalog="system" symbolScale="large"/>
<connections>
<action selector="exportDetailedLog:" destination="g8a-Rf-zWa" id="Kbw-Q5-9WO"/>
<action selector="showConsoleLogs:" destination="g8a-Rf-zWa" id="V0f-0y-C6C"/>
</connections>
</barButtonItem>
</rightBarButtonItems>
@@ -1539,6 +1679,8 @@ Settings by i cons from the Noun Project</string>
<image name="Threads" width="130" height="130"/>
<image name="Twitter" width="130" height="130"/>
<image name="ladybug" catalog="system" width="128" height="122"/>
<image name="terminal" catalog="system" width="128" height="93"/>
<image name="trash" catalog="system" width="117" height="128"/>
<namedColor name="SettingsBackground">
<color red="0.45098039215686275" green="0.015686274509803921" blue="0.68627450980392157" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>

View File

@@ -27,7 +27,9 @@ extension SettingsViewController
case instructions
case techyThings
case credits
case debug
case advancedSettings
// diagnostics section, will be enabled on release builds only on swipe down with 3 fingers 3 times
case diagnostics
// case macDirtyCow
}
@@ -35,17 +37,17 @@ extension SettingsViewController
{
case backgroundRefresh
case noIdleTimeout
@available(iOS 14, *)
case addToSiri
case disableAppLimit
static var allCases: [AppRefreshRow] {
var c: [AppRefreshRow] = [.backgroundRefresh, .noIdleTimeout]
guard #available(iOS 14, *) else { return c }
c.append(.addToSiri)
var c: [AppRefreshRow] = [.backgroundRefresh, .noIdleTimeout, .addToSiri]
// conditional entries go at the last to preserve ordering
if !ProcessInfo().sparseRestorePatched { c.append(.disableAppLimit) }
if UserDefaults.standard.isCowExploitSupported || !ProcessInfo().sparseRestorePatched
{
c.append(.disableAppLimit)
}
return c
}
}
@@ -64,17 +66,25 @@ extension SettingsViewController
case clearCache
}
fileprivate enum DebugRow: Int, CaseIterable
fileprivate enum AdvancedSettingsRow: Int, CaseIterable
{
case sendFeedback
case refreshAttempts
case refreshSideJITServer
case resetPairingFile
case anisetteServers
case responseCaching
case betaUpdates
case resignedAppExport
// case advancedSettings
// case hiddenSettings
}
fileprivate enum DiagnosticsRow: Int, CaseIterable
{
case responseCaching
case exportResignedApp
case verboseOperationsLogging
case exportSqliteDB
case operationsLoggingControl
case minimuxerConsoleLogging
}
}
@@ -94,10 +104,12 @@ final class SettingsViewController: UITableViewController
@IBOutlet private var backgroundRefreshSwitch: UISwitch!
@IBOutlet private var noIdleTimeoutSwitch: UISwitch!
@IBOutlet private var disableAppLimitSwitch: UISwitch!
@IBOutlet private var isBetaUpdatesEnabled: UISwitch!
@IBOutlet private var isResignedAppExportEnabled: UISwitch!
@IBOutlet private var betaUpdatesSwitch: UISwitch!
@IBOutlet private var exportResignedAppsSwitch: UISwitch!
@IBOutlet private var verboseOperationsLoggingSwitch: UISwitch!
@IBOutlet private var minimuxerConsoleLoggingSwitch: UISwitch!
@IBOutlet private var refreshSideJITServer: UILabel!
// @IBOutlet private var refreshSideJITServer: UILabel!
@IBOutlet private var disableResponseCachingSwitch: UISwitch!
@IBOutlet private var mastodonButton: UIButton!
@@ -111,6 +123,8 @@ final class SettingsViewController: UITableViewController
return .lightContent
}
private var exportDBInProgress = false
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
@@ -127,53 +141,15 @@ final class SettingsViewController: UITableViewController
self.prototypeHeaderFooterView = nib.instantiate(withOwner: nil, options: nil)[0] as? SettingsHeaderFooterView
self.tableView.register(nib, forHeaderFooterViewReuseIdentifier: "HeaderFooterView")
let debugModeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(SettingsViewController.handleDebugModeGesture(_:)))
debugModeGestureRecognizer.delegate = self
debugModeGestureRecognizer.direction = .up
debugModeGestureRecognizer.numberOfTouchesRequired = 3
self.tableView.addGestureRecognizer(debugModeGestureRecognizer)
var versionString: String = ""
if let installedApp = InstalledApp.fetchAltStore(in: DatabaseManager.shared.viewContext)
{
#if BETA
// Only show build version for BETA builds.
let localizedVersion = if let bundleVersion = Bundle.main.object(forInfoDictionaryKey: kCFBundleVersionKey as String) as? String {
"\(installedApp.version) (\(bundleVersion))"
} else {
installedApp.localizedVersion
}
#else
let localizedVersion = installedApp.version
#endif
self.versionLabel.text = NSLocalizedString(String(format: "Version %@", localizedVersion), comment: "SideStore Version")
}
else if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
{
versionString += "SideStore \(version)"
if let xcode = Bundle.main.object(forInfoDictionaryKey: "DTXcode") as? String {
versionString += " - Xcode \(xcode) - "
if let build = Bundle.main.object(forInfoDictionaryKey: "DTXcodeBuild") as? String {
versionString += "\(build)"
}
}
if let pairing = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String {
let pair_test = pairing == "<insert pairing file here>"
if !pair_test {
versionString += " - \(!pair_test)"
}
}
self.versionLabel.text = NSLocalizedString(String(format: "Version %@", version), comment: "SideStore Version")
}
else
{
self.versionLabel.text = nil
versionString += "SideStore\t"
versionString += "\n\(Bundle.Info.appbundleIdentifier)"
self.versionLabel.text = NSLocalizedString(versionString, comment: "SideStore Version")
}
let debugModeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(SettingsViewController.handleDebugModeGesture(_:)))
debugModeGestureRecognizer.delegate = self
debugModeGestureRecognizer.direction = .up
debugModeGestureRecognizer.numberOfTouchesRequired = 3
self.tableView.addGestureRecognizer(debugModeGestureRecognizer)
// set the version label to show in settings screen
self.versionLabel.text = getVersionLabel()
self.versionLabel.numberOfLines = 0
self.versionLabel.lineBreakMode = .byWordWrapping
@@ -233,6 +209,74 @@ final class SettingsViewController: UITableViewController
private extension SettingsViewController
{
private func getVersionLabel() -> String {
let MARKETING_VERSION_KEY = "CFBundleShortVersionString"
let BUILD_REVISION = "CFBundleRevision" // commit ID for now (but could be any, set by build env vars
let CURRENT_PROJECT_VERSION = kCFBundleVersionKey as String
func getXcodeVersion() -> String {
let XCODE_VERSION = "DTXcode"
let XCODE_REVISION = "DTXcodeBuild"
let xcode = Bundle.main.object(forInfoDictionaryKey: XCODE_VERSION) as? String
let build = Bundle.main.object(forInfoDictionaryKey: XCODE_REVISION) as? String
var xcodeVersion = xcode.map { version in
// " - Xcode \(version) - " + (build.map { revision in "\(revision)" } ?? "") // Ex: "0.6.0 - Xcode 16.2 - 21ac1ef"
"Xcode \(version) - " + (build.map { revision in "\(revision)" } ?? "") // Ex: "0.6.0 - Xcode 16.2 - 21ac1ef"
} ?? ""
if let pairing = Bundle.main.object(forInfoDictionaryKey: "ALTPairingFile") as? String,
pairing != "<insert pairing file here>"{
xcodeVersion += " - true"
}
return xcodeVersion
}
var versionLabel: String = ""
if let installedApp = InstalledApp.fetchAltStore(in: DatabaseManager.shared.viewContext)
{
#if BETA
// Only show build version (and build revision) for BETA builds.
let bundleVersion: String? = Bundle.main.object(forInfoDictionaryKey: CURRENT_PROJECT_VERSION) as? String
let buildRevision: String? = Bundle.main.object(forInfoDictionaryKey: BUILD_REVISION) as? String
var localizedVersion = bundleVersion.map { version in
"\(installedApp.version) (\(version))" + (buildRevision.map { revision in " - \(revision)" } ?? "") // Ex: "0.6.0 (0600) - 1acdef3"
} ?? installedApp.localizedVersion
#else
var localizedVersion = installedApp.version
#endif
versionLabel = NSLocalizedString(String(format: "Version %@", localizedVersion), comment: "SideStore Version")
}
else if let version = Bundle.main.object(forInfoDictionaryKey: MARKETING_VERSION_KEY) as? String
{
var version = "SideStore \(version)"
version += getXcodeVersion()
versionLabel = NSLocalizedString(String(format: "Version %@", version), comment: "SideStore Version")
}
else
{
var version = "SideStore\t"
version += "\n\(Bundle.Info.appbundleIdentifier)"
versionLabel = NSLocalizedString(version, comment: "SideStore Version")
}
// add xcode build version if in debug mode
#if DEBUG
versionLabel += "\n\(getXcodeVersion())"
#endif
return versionLabel
}
func update()
{
if let team = DatabaseManager.shared.activeTeam()
@@ -248,12 +292,19 @@ private extension SettingsViewController
self.activeTeam = nil
}
// AppRefreshRow
self.backgroundRefreshSwitch.isOn = UserDefaults.standard.isBackgroundRefreshEnabled
self.noIdleTimeoutSwitch.isOn = UserDefaults.standard.isIdleTimeoutDisableEnabled
self.disableAppLimitSwitch.isOn = UserDefaults.standard.isAppLimitDisabled
// AdvancedSettingsRow
self.betaUpdatesSwitch.isOn = UserDefaults.standard.isBetaUpdatesEnabled
// DiagnosticsRow
self.disableResponseCachingSwitch.isOn = UserDefaults.standard.responseCachingDisabled
self.isBetaUpdatesEnabled.isOn = UserDefaults.standard.isBetaUpdatesEnabled
self.isResignedAppExportEnabled.isOn = UserDefaults.standard.isResignedAppExportEnabled
self.exportResignedAppsSwitch.isOn = UserDefaults.standard.isExportResignedAppEnabled
self.verboseOperationsLoggingSwitch.isOn = UserDefaults.standard.isVerboseOperationsLoggingEnabled
self.minimuxerConsoleLoggingSwitch.isOn = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled
if self.isViewLoaded
{
@@ -335,6 +386,12 @@ private extension SettingsViewController
case .credits:
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("CREDITS", comment: "")
case .advancedSettings:
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("ADVANCED SETTINGS", comment: "")
case .diagnostics:
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("DIAGNOSTICS", comment: "")
// case .macDirtyCow:
// if isHeader
// {
@@ -345,8 +402,6 @@ private extension SettingsViewController
// settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("If you've removed the 3-sideloaded app limit via the MacDirtyCow exploit, disable this setting to sideload more than 3 apps at a time.", comment: "")
// }
case .debug:
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("DEBUG", comment: "")
}
}
@@ -425,16 +480,32 @@ private extension SettingsViewController
}
@IBAction func toggleDisableAppLimit(_ sender: UISwitch) {
UserDefaults.standard.isAppLimitDisabled = sender.isOn
if UserDefaults.standard.activeAppsLimit != nil
{
UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit
if UserDefaults.standard.isCowExploitSupported || !ProcessInfo().sparseRestorePatched {
// accept state change only when valid
UserDefaults.standard.isAppLimitDisabled = sender.isOn
// TODO: Here we force reload the activeAppsLimit after detecting change in isAppLimitDisabled
// Why do we need to do this, once identified if this is intentional and working as expected, remove this todo
if UserDefaults.standard.activeAppsLimit != nil
{
UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit
}
}
}
@IBAction func toggleResignedAppExport(_ sender: UISwitch) {
// update it in database
UserDefaults.standard.isResignedAppExportEnabled = sender.isOn
UserDefaults.standard.isExportResignedAppEnabled = sender.isOn
}
@IBAction func toggleVerboseOperationsLogging(_ sender: UISwitch) {
// update it in database
UserDefaults.standard.isVerboseOperationsLoggingEnabled = sender.isOn
}
@IBAction func toggleMinimuxerConsoleLogging(_ sender: UISwitch) {
// update it in database
UserDefaults.standard.isMinimuxerConsoleLoggingEnabled = sender.isOn
}
@IBAction func toggleEnableBetaUpdates(_ sender: UISwitch) {
@@ -457,7 +528,7 @@ private extension SettingsViewController
UserDefaults.standard.responseCachingDisabled = sender.isOn
}
@IBAction func addRefreshAppsShortcut()
func addRefreshAppsShortcut()
{
guard let shortcut = INShortcut(intent: INInteraction.refreshAllApps().intent) else { return }
@@ -679,8 +750,7 @@ extension SettingsViewController
case _ where isSectionHidden(section): return nil
case .signIn where self.activeTeam != nil: return nil
case .account where self.activeTeam == nil: return nil
// case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .macDirtyCow, .debug:
case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .debug:
case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .advancedSettings, .diagnostics /* ,.macDirtyCow */:
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView
self.prepare(headerView, for: section, isHeader: true)
return headerView
@@ -702,7 +772,7 @@ extension SettingsViewController
self.prepare(footerView, for: section, isHeader: false)
return footerView
case .account, .credits, .debug, .instructions: return nil
case .account, .credits, .advancedSettings, .instructions, .diagnostics: return nil
}
}
@@ -714,8 +784,8 @@ extension SettingsViewController
case _ where isSectionHidden(section): return 1.0
case .signIn where self.activeTeam != nil: return 1.0
case .account where self.activeTeam == nil: return 1.0
// case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .macDirtyCow, .debug:
case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .debug:
// case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .macDirtyCow, .advanced:
case .signIn, .account, .patreon, .display, .appRefresh, .techyThings, .credits, .advancedSettings, .diagnostics:
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: true)
return height
@@ -732,11 +802,11 @@ extension SettingsViewController
case .signIn where self.activeTeam != nil: return 1.0
case .account where self.activeTeam == nil: return 1.0
// case .signIn, .patreon, .display, .appRefresh, .techyThings, .macDirtyCow:
case .signIn, .patreon, .display, .appRefresh, .techyThings:
case .signIn, .patreon, .display, .appRefresh, .techyThings, .diagnostics:
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: false)
return height
case .account, .credits, .debug, .instructions: return 0.0
case .account, .credits, .advancedSettings, .instructions: return 0.0
}
}
}
@@ -757,7 +827,7 @@ extension SettingsViewController
case .noIdleTimeout: break
case .disableAppLimit: break
case .addToSiri:
guard #available(iOS 14, *) else { return }
// guard #available(iOS 14, *) else { return } // our min deployment is iOS 15 now :) so commented out
self.addRefreshAppsShortcut()
}
@@ -784,8 +854,8 @@ extension SettingsViewController
self.tableView.deselectRow(at: selectedIndexPath, animated: true)
}
case .debug:
let row = DebugRow.allCases[indexPath.row]
case .advancedSettings:
let row = AdvancedSettingsRow.allCases[indexPath.row]
switch row
{
case .sendFeedback:
@@ -823,11 +893,11 @@ extension SettingsViewController
}
self.present(mailViewController, animated: true, completion: nil)
} else {
} else {
let toastView = ToastView(text: NSLocalizedString("Cannot Send Mail", comment: ""), detailText: nil)
toastView.show(in: self)
}
})
}
})
// Cancel action
alertController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
@@ -995,7 +1065,7 @@ extension SettingsViewController
self.prepare(for: UIStoryboardSegue(identifier: "anisetteServers", source: self, destination: anisetteServersController), sender: nil)
// case .advancedSettings:
// case .hiddenSettings:
// // Create the URL that deep links to your app's custom settings.
// if let url = URL(string: UIApplication.openSettingsURLString) {
// // Ask the system to open that URL.
@@ -1003,9 +1073,49 @@ extension SettingsViewController
// } else {
// ELOG("UIApplication.openSettingsURLString invalid")
// }
case .refreshAttempts, .responseCaching, .betaUpdates, .resignedAppExport : break
case .refreshAttempts, .betaUpdates : break
}
case .diagnostics:
let row = DiagnosticsRow.allCases[indexPath.row]
switch row {
case .exportSqliteDB:
// do not accept simulatenous export requests
if !exportDBInProgress {
exportDBInProgress = true
Task{
var toastView: ToastView?
do{
let exportedURL = try await CoreDataHelper.exportCoreDataStore()
print("exportSqliteDB: ExportedURL: \(exportedURL)")
toastView = ToastView(text: "Export Successful", detailText: nil)
}catch{
print("exportSqliteDB: \(error)")
toastView = ToastView(error: error)
}
// show toast to user about the result
DispatchQueue.main.async {
toastView?.show(in: self)
}
// update that work has finished
exportDBInProgress = false
}
}
case .operationsLoggingControl:
// Instantiate SwiftUI View inside UIHostingController
let operationsLoggingControlView = OperationsLoggingControlView()
let operationsLoggingController = UIHostingController(rootView: operationsLoggingControlView)
let segue = UIStoryboardSegue(identifier: "operationsLoggingControl", source: self, destination: operationsLoggingController)
self.present(segue.destination, animated: true, completion: nil)
case .responseCaching, .exportResignedApp, .verboseOperationsLogging, .minimuxerConsoleLogging : break
}
// case .account, .patreon, .display, .instructions, .macDirtyCow: break
case .account, .patreon, .display, .instructions: break