Files
SideStore/AltStore/Settings/SettingsViewController.swift
Riley Testut 86ae06e0c8 Adds “Disable Response Caching” debug setting
When enabled, AltStore will ignore cached responses for certain requests and will always make a new request to the server. This is useful for development when repeatedly testing changes to remote files.

Limited to UpdateKnownSourcesOperation for now, but will eventually affect fetching sources as well.
2024-12-26 21:15:29 +05:30

935 lines
38 KiB
Swift

//
// SettingsViewController.swift
// AltStore
//
// Created by Riley Testut on 8/31/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import SwiftUI
import SafariServices
import MessageUI
import Intents
import IntentsUI
import AltStoreCore
extension SettingsViewController
{
fileprivate enum Section: Int, CaseIterable
{
case signIn
case account
case patreon
case appRefresh
case instructions
case techyThings
case credits
case macDirtyCow
case debug
}
fileprivate enum AppRefreshRow: Int, CaseIterable
{
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)
// conditional entries go at the last to preserve ordering
if !ProcessInfo().sparseRestorePatched { c.append(.disableAppLimit) }
return c
}
}
fileprivate enum CreditsRow: Int, CaseIterable
{
case developer
case operations
case designer
case softwareLicenses
}
fileprivate enum TechyThingsRow: Int, CaseIterable
{
case errorLog
case clearCache
}
fileprivate enum DebugRow: Int, CaseIterable
{
case sendFeedback
case refreshAttempts
case errorLog
case refreshSideJITServer
case clearCache
case resetPairingFile
case anisetteServers
case advancedSettings
case responseCaching
}
}
final class SettingsViewController: UITableViewController
{
private var activeTeam: Team?
private var prototypeHeaderFooterView: SettingsHeaderFooterView!
private var debugGestureCounter = 0
private weak var debugGestureTimer: Timer?
@IBOutlet private var accountNameLabel: UILabel!
@IBOutlet private var accountEmailLabel: UILabel!
@IBOutlet private var accountTypeLabel: UILabel!
@IBOutlet private var backgroundRefreshSwitch: UISwitch!
@IBOutlet private var noIdleTimeoutSwitch: UISwitch!
@IBOutlet private var disableAppLimitSwitch: UISwitch!
@IBOutlet private var refreshSideJITServer: UILabel!
@IBOutlet private var enforceThreeAppLimitSwitch: UISwitch!
@IBOutlet private var disableResponseCachingSwitch: UISwitch!
@IBOutlet private var versionLabel: UILabel!
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.openPatreonSettings(_:)), name: AppDelegate.openPatreonSettingsDeepLinkNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.openErrorLog(_:)), name: ToastView.openErrorLogNotification, object: nil)
}
override func viewDidLoad()
{
super.viewDidLoad()
let nib = UINib(nibName: "SettingsHeaderFooterView", bundle: nil)
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)
if let installedApp = InstalledApp.fetchAltStore(in: DatabaseManager.shared.viewContext)
{
#if BETA
// Only show build version for BETA builds.
let localizedVersion = installedApp.localizedVersion
#else
let localizedVersion = installedApp.version
#endif
self.versionLabel.text = NSLocalizedString(String(format: "AltStore %@", localizedVersion), comment: "AltStore 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)"
}
}
}
else
{
versionString += "SideStore\t"
}
versionString += "\n\(Bundle.Info.appbundleIdentifier)"
self.versionLabel.text = NSLocalizedString(versionString, comment: "SideStore Version")
self.versionLabel.numberOfLines = 0
self.versionLabel.lineBreakMode = .byWordWrapping
self.versionLabel.setNeedsUpdateConstraints()
self.tableView.contentInset.bottom = 40
self.update()
if #available(iOS 15, *), let appearance = self.tabBarController?.tabBar.standardAppearance
{
appearance.stackedLayoutAppearance.normal.badgeBackgroundColor = .altPrimary
self.navigationController?.tabBarItem.scrollEdgeAppearance = appearance
}
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
self.update()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "anisetteServers" {
let controller = UIHostingController(rootView: AnisetteServers(selected: UserDefaults.standard.menuAnisetteURL, errorCallback: {
ToastView(text: "Cleared adi.pb!", detailText: "You will need to log back into Apple ID in SideStore.").show(in: self)
}))
self.show(controller, sender: nil)
} else {
super.prepare(for: segue, sender: sender)
}
}
}
private extension SettingsViewController
{
func update()
{
if let team = DatabaseManager.shared.activeTeam()
{
self.accountNameLabel.text = team.name
self.accountEmailLabel.text = team.account.appleID
self.accountTypeLabel.text = team.type.localizedDescription
self.activeTeam = team
}
else
{
self.activeTeam = nil
}
self.backgroundRefreshSwitch.isOn = UserDefaults.standard.isBackgroundRefreshEnabled
self.noIdleTimeoutSwitch.isOn = UserDefaults.standard.isIdleTimeoutDisableEnabled
self.disableAppLimitSwitch.isOn = UserDefaults.standard.isAppLimitDisabled
self.enforceThreeAppLimitSwitch.isOn = !UserDefaults.standard.ignoreActiveAppsLimit
self.disableResponseCachingSwitch.isOn = UserDefaults.standard.responseCachingDisabled
if self.isViewLoaded
{
self.tableView.reloadData()
}
}
func prepare(_ settingsHeaderFooterView: SettingsHeaderFooterView, for section: Section, isHeader: Bool)
{
settingsHeaderFooterView.primaryLabel.isHidden = !isHeader
settingsHeaderFooterView.secondaryLabel.isHidden = isHeader
settingsHeaderFooterView.button.isHidden = true
settingsHeaderFooterView.layoutMargins.bottom = isHeader ? 0 : 8
switch section
{
case .signIn:
if isHeader
{
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("ACCOUNT", comment: "")
}
else
{
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Sign in with your Apple ID to download apps from SideStore.", comment: "")
}
case .patreon:
if isHeader
{
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("SUPPORT US", comment: "")
}
else
{
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Support the SideStore Team by following our socials or becoming a patron!", comment: "")
}
case .account:
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("ACCOUNT", comment: "")
settingsHeaderFooterView.button.setTitle(NSLocalizedString("SIGN OUT", comment: ""), for: .normal)
settingsHeaderFooterView.button.addTarget(self, action: #selector(SettingsViewController.signOut(_:)), for: .primaryActionTriggered)
settingsHeaderFooterView.button.isHidden = false
case .appRefresh:
if isHeader
{
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("REFRESHING APPS", comment: "")
}
else
{
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Enable Background Refresh to automatically refresh apps in the background when connected to Wi-Fi. \n\nEnable Disable Idle Timeout to allow SideStore to keep your device awake during a refresh or install of any apps.", comment: "")
}
case .instructions:
break
case .techyThings:
if isHeader
{
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("TECHY THINGS", comment: "")
}
else
{
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Free up disk space by removing non-essential data, such as temporary files and backups for uninstalled apps.", comment: "")
}
case .credits:
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("CREDITS", comment: "")
case .macDirtyCow:
if isHeader
{
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("MACDIRTYCOW", comment: "")
}
else
{
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: "")
}
}
func preferredHeight(for settingsHeaderFooterView: SettingsHeaderFooterView, in section: Section, isHeader: Bool) -> CGFloat
{
let widthConstraint = settingsHeaderFooterView.contentView.widthAnchor.constraint(equalToConstant: tableView.bounds.width)
NSLayoutConstraint.activate([widthConstraint])
defer { NSLayoutConstraint.deactivate([widthConstraint]) }
self.prepare(settingsHeaderFooterView, for: section, isHeader: isHeader)
let size = settingsHeaderFooterView.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
return size.height
}
func isSectionHidden(_ section: Section) -> Bool
{
switch section
{
case .macDirtyCow:
let isHidden = !(UserDefaults.standard.isCowExploitSupported && UserDefaults.standard.isDebugModeEnabled)
return isHidden
default: return false
}
}
}
private extension SettingsViewController
{
func signIn()
{
AppManager.shared.authenticate(presentingViewController: self) { (result) in
DispatchQueue.main.async {
switch result
{
case .failure(OperationError.cancelled):
// Ignore
break
case .failure(let error):
let toastView = ToastView(error: error)
toastView.show(in: self)
case .success: break
}
self.update()
}
}
}
@objc func signOut(_ sender: UIBarButtonItem)
{
func signOut()
{
DatabaseManager.shared.signOut { (error) in
DispatchQueue.main.async {
if let error = error
{
let toastView = ToastView(error: error)
toastView.show(in: self)
}
self.update()
}
}
}
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to sign out?", comment: ""), message: NSLocalizedString("You will no longer be able to install or refresh apps once you sign out.", comment: ""), preferredStyle: .actionSheet)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Sign Out", comment: ""), style: .destructive) { _ in signOut() })
alertController.addAction(.cancel)
//Fix crash on iPad
alertController.popoverPresentationController?.barButtonItem = sender
self.present(alertController, animated: true, completion: nil)
}
@IBAction func toggleDisableAppLimit(_ sender: UISwitch) {
UserDefaults.standard.isAppLimitDisabled = sender.isOn
}
@IBAction func toggleIsBackgroundRefreshEnabled(_ sender: UISwitch)
{
UserDefaults.standard.isBackgroundRefreshEnabled = sender.isOn
}
@IBAction func toggleNoIdleTimeoutEnabled(_ sender: UISwitch)
{
UserDefaults.standard.isIdleTimeoutDisableEnabled = sender.isOn
}
@IBAction func toggleEnforceThreeAppLimit(_ sender: UISwitch)
{
UserDefaults.standard.ignoreActiveAppsLimit = !sender.isOn
if UserDefaults.standard.activeAppsLimit != nil
{
UserDefaults.standard.activeAppsLimit = InstalledApp.freeAccountActiveAppsLimit
}
}
@IBAction func toggleDisableResponseCaching(_ sender: UISwitch)
{
UserDefaults.standard.responseCachingDisabled = sender.isOn
}
@IBAction func addRefreshAppsShortcut()
{
guard let shortcut = INShortcut(intent: INInteraction.refreshAllApps().intent) else { return }
let viewController = INUIAddVoiceShortcutViewController(shortcut: shortcut)
viewController.delegate = self
viewController.modalPresentationStyle = .formSheet
self.present(viewController, animated: true, completion: nil)
}
func clearCache()
{
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to clear SideStore's cache?", comment: ""),
message: NSLocalizedString("This will remove all temporary files as well as backups for uninstalled apps.", comment: ""),
preferredStyle: .actionSheet)
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { [weak self] _ in
self?.tableView.indexPathForSelectedRow.map { self?.tableView.deselectRow(at: $0, animated: true) }
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Clear Cache", comment: ""), style: .destructive) { [weak self] _ in
AppManager.shared.clearAppCache { result in
DispatchQueue.main.async {
self?.tableView.indexPathForSelectedRow.map { self?.tableView.deselectRow(at: $0, animated: true) }
switch result
{
case .success: break
case .failure(let error):
let alertController = UIAlertController(title: NSLocalizedString("Unable to Clear Cache", comment: ""), message: error.localizedDescription, preferredStyle: .alert)
alertController.addAction(.ok)
self?.present(alertController, animated: true)
}
}
}
})
if let popoverController = alertController.popoverPresentationController {
popoverController.sourceView = self.view
popoverController.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 0, height: 0)
}
self.present(alertController, animated: true)
}
@IBAction func handleDebugModeGesture(_ gestureRecognizer: UISwipeGestureRecognizer)
{
self.debugGestureCounter += 1
self.debugGestureTimer?.invalidate()
if self.debugGestureCounter >= 3
{
self.debugGestureCounter = 0
UserDefaults.standard.isDebugModeEnabled.toggle()
self.tableView.reloadData()
}
else
{
self.debugGestureTimer = Timer.scheduledTimer(withTimeInterval: 0.4, repeats: false) { [weak self] (timer) in
self?.debugGestureCounter = 0
}
}
}
func openTwitter(username: String)
{
let twitterAppURL = URL(string: "twitter://user?screen_name=" + username)!
UIApplication.shared.open(twitterAppURL, options: [:]) { (success) in
if success
{
if let selectedIndexPath = self.tableView.indexPathForSelectedRow
{
self.tableView.deselectRow(at: selectedIndexPath, animated: true)
}
}
else
{
let safariURL = URL(string: "https://twitter.com/" + username)!
let safariViewController = SFSafariViewController(url: safariURL)
safariViewController.preferredControlTintColor = .altPrimary
self.present(safariViewController, animated: true, completion: nil)
}
}
}
}
private extension SettingsViewController
{
@objc func openPatreonSettings(_ notification: Notification)
{
guard self.presentedViewController == nil else { return }
UIView.performWithoutAnimation {
self.navigationController?.popViewController(animated: false)
self.performSegue(withIdentifier: "showPatreon", sender: nil)
}
}
@objc func openErrorLog(_: Notification) {
guard self.presentedViewController == nil else { return }
self.navigationController?.popViewController(animated: false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.performSegue(withIdentifier: "showErrorLog", sender: nil)
}
}
}
extension SettingsViewController
{
override func numberOfSections(in tableView: UITableView) -> Int
{
var numberOfSections = super.numberOfSections(in: tableView)
if !UserDefaults.standard.isDebugModeEnabled
{
numberOfSections -= 1
}
return numberOfSections
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
let section = Section.allCases[section]
switch section
{
case _ where isSectionHidden(section): return 0
case .signIn: return (self.activeTeam == nil) ? 1 : 0
case .account: return (self.activeTeam == nil) ? 0 : 3
case .appRefresh: return AppRefreshRow.allCases.count
default: return super.tableView(tableView, numberOfRowsInSection: section.rawValue)
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = super.tableView(tableView, cellForRowAt: indexPath)
if #available(iOS 14, *) {}
else if let cell = cell as? InsetGroupTableViewCell,
indexPath.section == Section.appRefresh.rawValue,
indexPath.row == AppRefreshRow.backgroundRefresh.rawValue
{
// Only one row is visible pre-iOS 14.
cell.style = .single
}
if AppRefreshRow.AllCases().count == 1
{
if let cell = cell as? InsetGroupTableViewCell,
indexPath.section == Section.appRefresh.rawValue,
indexPath.row == AppRefreshRow.backgroundRefresh.rawValue
{
cell.style = .single
}
}
if let cell = cell as? InsetGroupTableViewCell,
indexPath.section == Section.appRefresh.rawValue,
indexPath.row == AppRefreshRow.allCases.count-1 // last row
{
cell.setValue(3, forKey: "style")
}
return cell
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
{
let section = Section.allCases[section]
switch section
{
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, .appRefresh, .techyThings, .credits, .macDirtyCow, .debug:
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView
self.prepare(headerView, for: section, isHeader: true)
return headerView
case .instructions: return nil
}
}
override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView?
{
let section = Section.allCases[section]
switch section
{
case _ where isSectionHidden(section): return nil
case .signIn where self.activeTeam != nil: return nil
case .signIn, .patreon, .appRefresh, .techyThings, .macDirtyCow:
let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView
self.prepare(footerView, for: section, isHeader: false)
return footerView
case .account, .credits, .debug, .instructions: return nil
}
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
{
let section = Section.allCases[section]
switch section
{
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, .appRefresh, .techyThings, .credits, .macDirtyCow, .debug:
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: true)
return height
case .instructions: return 0.0
}
}
override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat
{
let section = Section.allCases[section]
switch section
{
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, .patreon, .appRefresh, .techyThings, .macDirtyCow:
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: false)
return height
case .account, .credits, .debug, .instructions: return 0.0
}
}
}
extension SettingsViewController
{
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let section = Section.allCases[indexPath.section]
switch section
{
case .signIn: self.signIn()
case .appRefresh:
let row = AppRefreshRow.allCases[indexPath.row]
switch row
{
case .backgroundRefresh: break
case .noIdleTimeout: break
case .disableAppLimit: break
case .addToSiri:
guard #available(iOS 14, *) else { return }
self.addRefreshAppsShortcut()
}
case .techyThings:
let row = TechyThingsRow.allCases[indexPath.row]
switch row
{
case .errorLog: break
case .clearCache: self.clearCache()
}
case .credits:
let row = CreditsRow.allCases[indexPath.row]
switch row
{
case .developer: self.openTwitter(username: "sidestoreio")
case .operations: self.openTwitter(username: "sidestoreio")
case .designer: self.openTwitter(username: "lit_ritt")
case .softwareLicenses: break
}
case .debug:
let row = DebugRow.allCases[indexPath.row]
switch row
{
case .sendFeedback:
let alertController = UIAlertController(title: "Send Feedback", message: "Choose a method to send feedback:", preferredStyle: .actionSheet)
// Option 1: GitHub
alertController.addAction(UIAlertAction(title: "GitHub", style: .default) { _ in
if let githubURL = URL(string: "https://github.com/SideStore/SideStore/issues") {
let safariViewController = SFSafariViewController(url: githubURL)
safariViewController.preferredControlTintColor = .altPrimary
self.present(safariViewController, animated: true, completion: nil)
}
})
// Option 2: Discord
alertController.addAction(UIAlertAction(title: "Discord", style: .default) { _ in
if let discordURL = URL(string: "https://discord.gg/sidestore-949183273383395328") {
let safariViewController = SFSafariViewController(url: discordURL)
safariViewController.preferredControlTintColor = .altPrimary
self.present(safariViewController, animated: true, completion: nil)
}
})
// Option 3: Mail
alertController.addAction(UIAlertAction(title: "Send Email", style: .default) { _ in
if MFMailComposeViewController.canSendMail() {
let mailViewController = MFMailComposeViewController()
mailViewController.mailComposeDelegate = self
mailViewController.setToRecipients(["support@sidestore.io"])
if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String {
mailViewController.setSubject("SideStore Beta \(version) Feedback")
} else {
mailViewController.setSubject("SideStore Beta Feedback")
}
self.present(mailViewController, animated: true, completion: nil)
} 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))
// For iPad: Set the source view if presenting on iPad to avoid crashes
if let popoverController = alertController.popoverPresentationController {
popoverController.sourceView = self.view
popoverController.sourceRect = self.view.bounds
}
// Present the action sheet
self.present(alertController, animated: true, completion: nil)
case .refreshSideJITServer:
if #available(iOS 17, *) {
let alertController = UIAlertController(
title: NSLocalizedString("SideJITServer", comment: ""),
message: NSLocalizedString("Settings for SideJITServer", comment: ""),
preferredStyle: UIAlertController.Style.actionSheet)
if UserDefaults.standard.sidejitenable {
alertController.addAction(UIAlertAction(title: NSLocalizedString("Disable", comment: ""), style: .default){ _ in
UserDefaults.standard.sidejitenable = false
})
} else {
alertController.addAction(UIAlertAction(title: NSLocalizedString("Enable", comment: ""), style: .default){ _ in
UserDefaults.standard.sidejitenable = true
})
}
alertController.addAction(UIAlertAction(title: NSLocalizedString("Server Address", comment: ""), style: .default){ _ in
let alertController1 = UIAlertController(title: "SideJITServer Address", message: "Please Enter the SideJITServer Address Below. (this is not needed if SideJITServer has already been detected)", preferredStyle: .alert)
alertController1.addTextField { textField in
textField.placeholder = "SideJITServer Address"
}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alertController1.addAction(cancelAction)
let okAction = UIAlertAction(title: "OK", style: .default) { _ in
if let text = alertController1.textFields?.first?.text {
UserDefaults.standard.textInputSideJITServerurl = text
}
}
alertController1.addAction(okAction)
// Present the alert controller
self.present(alertController1, animated: true)
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Refresh", comment: ""), style: .destructive){ _ in
if UserDefaults.standard.sidejitenable {
var SJSURL = ""
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
SJSURL = "http://sidejitserver._http._tcp.local:8080"
} else {
SJSURL = UserDefaults.standard.textInputSideJITServerurl ?? ""
}
let url = URL(string: SJSURL + "/re/")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let error = error {
print("Error: \(error)")
} else {
// Do nothing with data or response
}
}
task.resume()
}
})
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alertController.addAction(cancelAction)
//Fix crash on iPad
alertController.popoverPresentationController?.sourceView = self.tableView
alertController.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath)
self.present(alertController, animated: true)
self.tableView.deselectRow(at: indexPath, animated: true)
} else {
let alertController = UIAlertController(
title: NSLocalizedString("You are not on iOS 17+ This will not work", comment: ""),
message: NSLocalizedString("This is meant for 'SideJITServer' and it only works on iOS 17+ ", comment: ""),
preferredStyle: UIAlertController.Style.actionSheet)
alertController.addAction(.cancel)
//Fix crash on iPad
alertController.popoverPresentationController?.sourceView = self.tableView
alertController.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath)
self.present(alertController, animated: true)
self.tableView.deselectRow(at: indexPath, animated: true)
}
case .clearCache: self.clearCache()
case .resetPairingFile:
let filename = "ALTPairingFile.mobiledevicepairing"
let fm = FileManager.default
let documentsPath = fm.documentsDirectory.appendingPathComponent("/\(filename)")
let alertController = UIAlertController(
title: NSLocalizedString("Are you sure to reset the pairing file?", comment: ""),
message: NSLocalizedString("You can reset the pairing file when you cannot sideload apps or enable JIT. You need to restart SideStore.", comment: ""),
preferredStyle: UIAlertController.Style.actionSheet)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Delete and Reset", comment: ""), style: .destructive){ _ in
if fm.fileExists(atPath: documentsPath.path), let contents = try? String(contentsOf: documentsPath), !contents.isEmpty {
UserDefaults.standard.isPairingReset = true
try? fm.removeItem(atPath: documentsPath.path)
NSLog("Pairing File Reseted")
}
self.tableView.deselectRow(at: indexPath, animated: true)
let dialogMessage = UIAlertController(title: NSLocalizedString("Pairing File Reset", comment: ""), message: NSLocalizedString("Please restart SideStore", comment: ""), preferredStyle: .alert)
self.present(dialogMessage, animated: true, completion: nil)
})
alertController.addAction(.cancel)
//Fix crash on iPad
alertController.popoverPresentationController?.sourceView = self.tableView
alertController.popoverPresentationController?.sourceRect = self.tableView.rectForRow(at: indexPath)
self.present(alertController, animated: true)
self.tableView.deselectRow(at: indexPath, animated: true)
case .anisetteServers:
self.prepare(for: UIStoryboardSegue(identifier: "anisetteServers", source: self, destination: UIHostingController(rootView: AnisetteServers(selected: "", errorCallback: {
ToastView(text: "Reset adi.pb", detailText: "Buh").show(in: self)
}))), sender: nil)
// self.performSegue(withIdentifier: "anisetteServers", sender: nil)
case .advancedSettings:
// 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.
UIApplication.shared.open(url)
} else {
ELOG("UIApplication.openSettingsURLString invalid")
}
case .refreshAttempts, .errorLog, .responseCaching: break
}
case .account, .patreon, .instructions, .macDirtyCow: break
}
}
}
extension SettingsViewController: MFMailComposeViewControllerDelegate
{
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?)
{
if let error = error
{
let toastView = ToastView(error: error)
toastView.show(in: self)
}
controller.dismiss(animated: true, completion: nil)
}
}
extension SettingsViewController: UIGestureRecognizerDelegate
{
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool
{
return true
}
}
extension SettingsViewController: INUIAddVoiceShortcutViewControllerDelegate
{
func addVoiceShortcutViewController(_ controller: INUIAddVoiceShortcutViewController, didFinishWith voiceShortcut: INVoiceShortcut?, error: Error?)
{
if let indexPath = self.tableView.indexPathForSelectedRow
{
self.tableView.deselectRow(at: indexPath, animated: true)
}
controller.dismiss(animated: true, completion: nil)
guard let error = error else { return }
let toastView = ToastView(error: error)
toastView.show(in: self)
}
func addVoiceShortcutViewControllerDidCancel(_ controller: INUIAddVoiceShortcutViewController)
{
if let indexPath = self.tableView.indexPathForSelectedRow
{
self.tableView.deselectRow(at: indexPath, animated: true)
}
controller.dismiss(animated: true, completion: nil)
}
}