mirror of
https://github.com/SideStore/SideStore.git
synced 2026-03-29 14:55:39 +02:00
Merge branch 'develop' into users/junepark678/altsign-fixes
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: "")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
219
AltStore/Operations/RemoveAppExtensionsOperation.swift
Normal file
219
AltStore/Operations/RemoveAppExtensionsOperation.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
226
AltStore/Settings/Error Log/ConsoleLogView.swift
Normal file
226
AltStore/Settings/Error Log/ConsoleLogView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
284
AltStore/Settings/OperationsLoggingContolView.swift
Normal file
284
AltStore/Settings/OperationsLoggingContolView.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user