Files
SideStore/AltStore/Settings/SettingsViewController.swift

566 lines
20 KiB
Swift
Raw Normal View History

2019-06-06 14:46:23 -07:00
//
2019-09-05 11:59:10 -07:00
// SettingsViewController.swift
2019-06-06 14:46:23 -07:00
// AltStore
//
2019-09-05 11:59:10 -07:00
// Created by Riley Testut on 8/31/19.
2019-06-06 14:46:23 -07:00
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import SafariServices
import MessageUI
import Intents
import IntentsUI
2019-06-06 14:46:23 -07:00
import AltStoreCore
2019-09-05 11:59:10 -07:00
extension SettingsViewController
{
fileprivate enum Section: Int, CaseIterable
{
case signIn
case account
case patreon
case appRefresh
case jailbreak
case instructions
case credits
2019-09-05 11:59:10 -07:00
case debug
}
fileprivate enum AppRefreshRow: Int, CaseIterable
{
case backgroundRefresh
@available(iOS 14, *)
case addToSiri
static var allCases: [AppRefreshRow] {
guard #available(iOS 14, *) else { return [.backgroundRefresh] }
return [.backgroundRefresh, .addToSiri]
}
}
fileprivate enum CreditsRow: Int, CaseIterable
{
case developer
case designer
case softwareLicenses
}
fileprivate enum DebugRow: Int, CaseIterable
{
case sendFeedback
case refreshAttempts
}
2019-09-05 11:59:10 -07:00
}
2019-06-06 14:46:23 -07:00
class SettingsViewController: UITableViewController
2019-06-06 14:46:23 -07:00
{
2019-09-05 11:59:10 -07:00
private var activeTeam: Team?
2019-06-06 14:46:23 -07:00
2019-09-05 11:59:10 -07:00
private var prototypeHeaderFooterView: SettingsHeaderFooterView!
2019-06-06 14:46:23 -07:00
private var debugGestureCounter = 0
private weak var debugGestureTimer: Timer?
2019-09-05 11:59:10 -07:00
@IBOutlet private var accountNameLabel: UILabel!
@IBOutlet private var accountEmailLabel: UILabel!
@IBOutlet private var accountTypeLabel: UILabel!
2019-06-06 14:46:23 -07:00
2019-09-05 11:59:10 -07:00
@IBOutlet private var backgroundRefreshSwitch: UISwitch!
2019-06-06 14:46:23 -07:00
2020-01-13 13:32:55 -08:00
@IBOutlet private var versionLabel: UILabel!
2019-10-24 13:04:30 -07:00
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)
}
2019-06-06 14:46:23 -07:00
override func viewDidLoad()
{
super.viewDidLoad()
2019-09-05 11:59:10 -07:00
let nib = UINib(nibName: "SettingsHeaderFooterView", bundle: nil)
self.prototypeHeaderFooterView = nib.instantiate(withOwner: nil, options: nil)[0] as? SettingsHeaderFooterView
self.tableView.register(nib, forHeaderFooterViewReuseIdentifier: "HeaderFooterView")
2019-06-06 14:46:23 -07:00
let debugModeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(SettingsViewController.handleDebugModeGesture(_:)))
debugModeGestureRecognizer.delegate = self
debugModeGestureRecognizer.direction = .up
debugModeGestureRecognizer.numberOfTouchesRequired = 3
self.tableView.addGestureRecognizer(debugModeGestureRecognizer)
2020-01-13 13:32:55 -08:00
if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
{
self.versionLabel.text = NSLocalizedString(String(format: "AltStore %@", version), comment: "AltStore Version")
}
else
{
self.versionLabel.text = NSLocalizedString("AltStore", comment: "")
}
2020-01-24 11:34:26 -08:00
self.tableView.contentInset.bottom = 20
2019-06-06 14:46:23 -07:00
self.update()
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
self.update()
}
2019-06-06 14:46:23 -07:00
}
private extension SettingsViewController
2019-06-06 14:46:23 -07:00
{
func update()
{
if let team = DatabaseManager.shared.activeTeam()
{
2019-09-05 11:59:10 -07:00
self.accountNameLabel.text = team.name
2019-06-06 14:46:23 -07:00
self.accountEmailLabel.text = team.account.appleID
2019-09-05 11:59:10 -07:00
self.accountTypeLabel.text = team.type.localizedDescription
2019-06-06 14:46:23 -07:00
2019-09-05 11:59:10 -07:00
self.activeTeam = team
2019-06-06 14:46:23 -07:00
}
else
{
2019-09-05 11:59:10 -07:00
self.activeTeam = nil
2019-06-06 14:46:23 -07:00
}
2019-09-05 11:59:10 -07:00
self.backgroundRefreshSwitch.isOn = UserDefaults.standard.isBackgroundRefreshEnabled
2019-06-06 14:46:23 -07:00
if self.isViewLoaded
{
self.tableView.reloadData()
}
}
2019-09-05 11:59:10 -07:00
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 AltStore.", comment: "")
}
case .patreon:
2019-09-25 12:43:32 -07:00
if isHeader
{
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("PATREON", comment: "")
}
else
{
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("Receive access to beta versions of AltStore, Delta, and more by becoming a patron.", comment: "")
}
2019-09-05 11:59:10 -07:00
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 the same WiFi as AltServer.", comment: "")
}
2019-09-05 11:59:10 -07:00
case .jailbreak:
if isHeader
{
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("JAILBREAK", comment: "")
}
else
{
settingsHeaderFooterView.secondaryLabel.text = NSLocalizedString("AltDaemon allows AltStore to install and refresh apps without a computer. You can install AltDaemon using Filza or another file manager.", comment: "")
}
case .instructions:
break
case .credits:
settingsHeaderFooterView.primaryLabel.text = NSLocalizedString("CREDITS", comment: "")
2019-09-05 11:59:10 -07:00
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
}
2019-06-06 14:46:23 -07:00
}
private extension SettingsViewController
2019-06-06 14:46:23 -07:00
{
2019-09-05 11:59:10 -07:00
func signIn()
2019-06-06 14:46:23 -07:00
{
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
}
2019-06-06 14:46:23 -07:00
self.update()
}
}
}
2019-09-05 11:59:10 -07:00
@objc func signOut(_ sender: UIBarButtonItem)
2019-06-06 14:46:23 -07:00
{
func signOut()
{
DatabaseManager.shared.signOut { (error) in
DispatchQueue.main.async {
if let error = error
{
let toastView = ToastView(error: error)
toastView.show(in: self)
2019-06-06 14:46:23 -07:00
}
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)
self.present(alertController, animated: true, completion: nil)
}
2019-09-05 11:59:10 -07:00
@IBAction func toggleIsBackgroundRefreshEnabled(_ sender: UISwitch)
{
UserDefaults.standard.isBackgroundRefreshEnabled = sender.isOn
}
@available(iOS 14, *)
@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)
}
@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)
}
}
}
2019-06-06 14:46:23 -07:00
}
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)
}
}
}
extension SettingsViewController
2019-06-06 14:46:23 -07:00
{
override func numberOfSections(in tableView: UITableView) -> Int
{
var numberOfSections = super.numberOfSections(in: tableView)
if !UserDefaults.standard.isDebugModeEnabled
{
numberOfSections -= 1
}
return numberOfSections
}
2019-09-05 11:59:10 -07:00
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
2019-06-06 14:46:23 -07:00
{
2019-09-05 11:59:10 -07:00
let section = Section.allCases[section]
switch section
{
case .signIn: return (self.activeTeam == nil) ? 1 : 0
case .account: return (self.activeTeam == nil) ? 0 : 3
case .appRefresh: return AppRefreshRow.allCases.count
case .jailbreak: return UIDevice.current.isJailbroken ? 1 : 0
2019-09-05 11:59:10 -07:00
default: return super.tableView(tableView, numberOfRowsInSection: section.rawValue)
}
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
{
let section = Section.allCases[section]
switch section
{
case .signIn where self.activeTeam != nil: return nil
case .account where self.activeTeam == nil: return nil
case .jailbreak where !UIDevice.current.isJailbroken: return nil
2019-09-05 11:59:10 -07:00
case .signIn, .account, .patreon, .appRefresh, .jailbreak, .credits, .debug:
2019-09-05 11:59:10 -07:00
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView
self.prepare(headerView, for: section, isHeader: true)
return headerView
case .instructions: return nil
2019-09-05 11:59:10 -07:00
}
}
override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView?
{
let section = Section.allCases[section]
switch section
{
case .signIn where self.activeTeam != nil: return nil
case .jailbreak where !UIDevice.current.isJailbroken: return nil
2019-09-05 11:59:10 -07:00
case .signIn, .patreon, .appRefresh, .jailbreak:
2019-09-05 11:59:10 -07:00
let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "HeaderFooterView") as! SettingsHeaderFooterView
self.prepare(footerView, for: section, isHeader: false)
return footerView
case .account, .credits, .debug, .instructions: return nil
2019-09-05 11:59:10 -07:00
}
2019-06-06 14:46:23 -07:00
}
2019-09-05 11:59:10 -07:00
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
{
let section = Section.allCases[section]
switch section
{
case .signIn where self.activeTeam != nil: return 1.0
case .account where self.activeTeam == nil: return 1.0
case .jailbreak where !UIDevice.current.isJailbroken: return 1.0
2019-09-05 11:59:10 -07:00
case .signIn, .account, .patreon, .appRefresh, .jailbreak, .credits, .debug:
2019-09-05 11:59:10 -07:00
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: true)
return height
case .instructions: return 0.0
2019-09-05 11:59:10 -07:00
}
}
override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat
{
let section = Section.allCases[section]
switch section
{
case .signIn where self.activeTeam != nil: return 1.0
case .account where self.activeTeam == nil: return 1.0
case .jailbreak where !UIDevice.current.isJailbroken: return 1.0
2019-09-05 11:59:10 -07:00
case .signIn, .patreon, .appRefresh, .jailbreak:
2019-09-05 11:59:10 -07:00
let height = self.preferredHeight(for: self.prototypeHeaderFooterView, in: section, isHeader: false)
return height
case .account, .credits, .debug, .instructions: return 0.0
2019-09-05 11:59:10 -07:00
}
}
}
2019-06-06 14:46:23 -07:00
2019-09-05 11:59:10 -07:00
extension SettingsViewController
{
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let section = Section.allCases[indexPath.section]
switch section
{
case .signIn: self.signIn()
case .instructions: break
case .appRefresh:
let row = AppRefreshRow.allCases[indexPath.row]
switch row
{
case .backgroundRefresh: break
case .addToSiri:
guard #available(iOS 14, *) else { return }
self.addRefreshAppsShortcut()
}
case .jailbreak:
let fileURL = Bundle.main.url(forResource: "AltDaemon", withExtension: "deb")!
let activityViewController = UIActivityViewController(activityItems: [fileURL], applicationActivities: nil)
self.present(activityViewController, animated: true, completion: nil)
tableView.deselectRow(at: indexPath, animated: true)
case .credits:
let row = CreditsRow.allCases[indexPath.row]
switch row
{
case .developer: self.openTwitter(username: "rileytestut")
case .designer: self.openTwitter(username: "1carolinemoore")
case .softwareLicenses: break
}
case .debug:
let row = DebugRow.allCases[indexPath.row]
switch row
{
case .sendFeedback:
if MFMailComposeViewController.canSendMail()
{
let mailViewController = MFMailComposeViewController()
mailViewController.mailComposeDelegate = self
mailViewController.setToRecipients(["support@altstore.io"])
if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
{
mailViewController.setSubject("AltStore Beta \(version) Feedback")
}
else
{
mailViewController.setSubject("AltStore 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)
}
case .refreshAttempts: break
}
2019-09-05 11:59:10 -07:00
default: 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)
}
}