GBA001/Delta/Settings/SettingsViewController.swift

534 lines
18 KiB
Swift

//
// SettingsViewController.swift
// Delta
//
// Created by Riley Testut on 9/4/15.
// Copyright © 2015 Riley Testut. All rights reserved.
//
import UIKit
import SafariServices
import DeltaCore
import Roxas
private extension SettingsViewController
{
enum Section: Int
{
case controllers
case controllerSkins
case controllerOpacity
case gameAudio
case hapticFeedback
case syncing
case hapticTouch
case cores
case advanced
case patreon
case credits
}
enum Segue: String
{
case controllers = "controllersSegue"
case controllerSkins = "controllerSkinsSegue"
case dsSettings = "dsSettingsSegue"
}
enum SyncingRow: Int, CaseIterable
{
case service
case status
}
enum CreditsRow: Int, CaseIterable
{
case riley
case shane
case caroline
case grant
case litRitt
case contributors
case softwareLicenses
}
}
class SettingsViewController: UITableViewController
{
@IBOutlet private var controllerOpacityLabel: UILabel!
@IBOutlet private var controllerOpacitySlider: UISlider!
@IBOutlet private var respectSilentModeSwitch: UISwitch!
@IBOutlet private var buttonHapticFeedbackEnabledSwitch: UISwitch!
@IBOutlet private var thumbstickHapticFeedbackEnabledSwitch: UISwitch!
@IBOutlet private var previewsEnabledSwitch: UISwitch!
@IBOutlet private var versionLabel: UILabel!
@IBOutlet private var syncingServiceLabel: UILabel!
private var selectionFeedbackGenerator: UISelectionFeedbackGenerator?
private var previousSelectedRowIndexPath: IndexPath?
private var syncingConflictsCount = 0
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.settingsDidChange(with:)), name: Settings.didChangeNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.externalGameControllerDidConnect(_:)), name: .externalGameControllerDidConnect, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(SettingsViewController.externalGameControllerDidDisconnect(_:)), name: .externalGameControllerDidDisconnect, object: nil)
}
override func viewDidLoad()
{
super.viewDidLoad()
if let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
{
#if LITE
self.versionLabel.text = NSLocalizedString(String(format: "Delta Lite %@", version), comment: "Delta Version")
#else
self.versionLabel.text = NSLocalizedString(String(format: "Delta %@", version), comment: "Delta Version")
#endif
}
else
{
#if LITE
self.versionLabel.text = NSLocalizedString("Delta Lite", comment: "")
#else
self.versionLabel.text = NSLocalizedString("Delta", comment: "")
#endif
}
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
if let indexPath = self.previousSelectedRowIndexPath
{
if indexPath.section == Section.controllers.rawValue
{
// Update and temporarily re-select selected row.
self.tableView.reloadSections(IndexSet(integer: Section.controllers.rawValue), with: .none)
self.tableView.selectRow(at: indexPath, animated: false, scrollPosition: UITableView.ScrollPosition.none)
}
self.tableView.deselectRow(at: indexPath, animated: true)
}
self.update()
}
override func didReceiveMemoryWarning()
{
super.didReceiveMemoryWarning()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
guard
let identifier = segue.identifier,
let segueType = Segue(rawValue: identifier),
let cell = sender as? UITableViewCell,
let indexPath = self.tableView.indexPath(for: cell)
else { return }
self.previousSelectedRowIndexPath = indexPath
switch segueType
{
case Segue.controllers:
let controllersSettingsViewController = segue.destination as! ControllersSettingsViewController
controllersSettingsViewController.playerIndex = indexPath.row
case Segue.controllerSkins:
let preferredControllerSkinsViewController = segue.destination as! PreferredControllerSkinsViewController
let system = System.registeredSystems[indexPath.row]
preferredControllerSkinsViewController.system = system
case Segue.dsSettings: break
}
}
}
private extension SettingsViewController
{
func update()
{
self.controllerOpacitySlider.value = Float(Settings.translucentControllerSkinOpacity)
self.updateControllerOpacityLabel()
self.respectSilentModeSwitch.isOn = Settings.respectSilentMode
self.syncingServiceLabel.text = Settings.syncingService?.localizedName
do
{
let records = try SyncManager.shared.recordController?.fetchConflictedRecords() ?? []
self.syncingConflictsCount = records.count
}
catch
{
print(error)
}
self.buttonHapticFeedbackEnabledSwitch.isOn = Settings.isButtonHapticFeedbackEnabled
self.thumbstickHapticFeedbackEnabledSwitch.isOn = Settings.isThumbstickHapticFeedbackEnabled
self.previewsEnabledSwitch.isOn = Settings.isPreviewsEnabled
self.tableView.reloadData()
}
func updateControllerOpacityLabel()
{
let percentage = String(format: "%.f", Settings.translucentControllerSkinOpacity * 100) + "%"
self.controllerOpacityLabel.text = percentage
}
func isSectionHidden(_ section: Section) -> Bool
{
switch section
{
case .hapticTouch:
if #available(iOS 13, *)
{
// All devices on iOS 13 support either 3D touch or Haptic Touch.
return false
}
else
{
return self.view.traitCollection.forceTouchCapability != .available
}
default: return false
}
}
}
private extension SettingsViewController
{
@IBAction func beginChangingControllerOpacity(with sender: UISlider)
{
self.selectionFeedbackGenerator = UISelectionFeedbackGenerator()
self.selectionFeedbackGenerator?.prepare()
}
@IBAction func changeControllerOpacity(with sender: UISlider)
{
let roundedValue = CGFloat((sender.value / 0.05).rounded() * 0.05)
if roundedValue != Settings.translucentControllerSkinOpacity
{
self.selectionFeedbackGenerator?.selectionChanged()
}
Settings.translucentControllerSkinOpacity = CGFloat(roundedValue)
self.updateControllerOpacityLabel()
}
@IBAction func didFinishChangingControllerOpacity(with sender: UISlider)
{
sender.value = Float(Settings.translucentControllerSkinOpacity)
self.selectionFeedbackGenerator = nil
}
@IBAction func toggleButtonHapticFeedbackEnabled(_ sender: UISwitch)
{
Settings.isButtonHapticFeedbackEnabled = sender.isOn
}
@IBAction func toggleThumbstickHapticFeedbackEnabled(_ sender: UISwitch)
{
Settings.isThumbstickHapticFeedbackEnabled = sender.isOn
}
@IBAction func togglePreviewsEnabled(_ sender: UISwitch)
{
Settings.isPreviewsEnabled = sender.isOn
}
@IBAction func toggleRespectSilentMode(_ sender: UISwitch)
{
Settings.respectSilentMode = sender.isOn
}
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 = .deltaPurple
self.present(safariViewController, animated: true, completion: nil)
}
}
}
@available(iOS 14, *)
func showContributors()
{
let hostingController = ContributorsView.makeViewController()
self.navigationController?.pushViewController(hostingController, animated: true)
}
func showExperimentalFeatures()
{
let hostingController = ExperimentalFeaturesView.makeViewController()
self.navigationController?.pushViewController(hostingController, animated: true)
}
}
private extension SettingsViewController
{
@objc func settingsDidChange(with notification: Notification)
{
guard let settingsName = notification.userInfo?[Settings.NotificationUserInfoKey.name] as? Settings.Name else { return }
switch settingsName
{
case .syncingService:
let selectedIndexPath = self.tableView.indexPathForSelectedRow
self.tableView.reloadSections(IndexSet(integer: Section.syncing.rawValue), with: .none)
let syncingServiceIndexPath = IndexPath(row: SyncingRow.service.rawValue, section: Section.syncing.rawValue)
if selectedIndexPath == syncingServiceIndexPath
{
self.tableView.selectRow(at: selectedIndexPath, animated: true, scrollPosition: .none)
}
case .localControllerPlayerIndex, .preferredControllerSkin, .translucentControllerSkinOpacity, .respectSilentMode, .isButtonHapticFeedbackEnabled, .isThumbstickHapticFeedbackEnabled, .isAltJITEnabled: break
default: break
}
}
@objc func externalGameControllerDidConnect(_ notification: Notification)
{
self.tableView.reloadSections(IndexSet(integer: Section.controllers.rawValue), with: .none)
}
@objc func externalGameControllerDidDisconnect(_ notification: Notification)
{
self.tableView.reloadSections(IndexSet(integer: Section.controllers.rawValue), with: .none)
}
}
extension SettingsViewController
{
override func tableView(_ tableView: UITableView, numberOfRowsInSection sectionIndex: Int) -> Int
{
let section = Section(rawValue: sectionIndex)!
switch section
{
case .controllers: return 4
case .controllerSkins: return System.registeredSystems.count
case .syncing: return SyncManager.shared.coordinator?.account == nil ? 1 : super.tableView(tableView, numberOfRowsInSection: sectionIndex)
default:
if isSectionHidden(section)
{
return 0
}
else
{
return super.tableView(tableView, numberOfRowsInSection: sectionIndex)
}
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = super.tableView(tableView, cellForRowAt: indexPath)
let section = Section(rawValue: indexPath.section)!
switch section
{
case .controllers:
if indexPath.row == Settings.localControllerPlayerIndex
{
cell.detailTextLabel?.text = UIDevice.current.name
}
else if let index = ExternalGameControllerManager.shared.connectedControllers.firstIndex(where: { $0.playerIndex == indexPath.row })
{
let controller = ExternalGameControllerManager.shared.connectedControllers[index]
cell.detailTextLabel?.text = controller.name
}
else
{
cell.detailTextLabel?.text = nil
}
case .controllerSkins:
cell.textLabel?.text = System.registeredSystems[indexPath.row].localizedName
case .syncing:
switch SyncingRow.allCases[indexPath.row]
{
case .status:
let cell = cell as! BadgedTableViewCell
cell.badgeLabel.text = self.syncingConflictsCount.description
cell.badgeLabel.isHidden = (self.syncingConflictsCount == 0)
case .service: break
}
case .cores:
let preferredCore = Settings.preferredCore(for: .ds)
cell.detailTextLabel?.text = preferredCore?.metadata?.name.value ?? preferredCore?.name ?? NSLocalizedString("Unknown", comment: "")
case .controllerOpacity, .gameAudio, .hapticFeedback, .hapticTouch, .advanced, .patreon, .credits: break
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let cell = tableView.cellForRow(at: indexPath)
let section = Section(rawValue: indexPath.section)!
switch section
{
case .controllers: self.performSegue(withIdentifier: Segue.controllers.rawValue, sender: cell)
case .controllerSkins: self.performSegue(withIdentifier: Segue.controllerSkins.rawValue, sender: cell)
case .cores: self.performSegue(withIdentifier: Segue.dsSettings.rawValue, sender: cell)
case .controllerOpacity, .gameAudio, .hapticFeedback, .hapticTouch, .syncing: break
case .advanced: self.showExperimentalFeatures()
case .patreon:
let patreonURL = URL(string: "altstore://patreon")!
UIApplication.shared.open(patreonURL, options: [:]) { (success) in
guard !success else { return }
let patreonURL = URL(string: "https://www.patreon.com/rileytestut")!
let safariViewController = SFSafariViewController(url: patreonURL)
safariViewController.preferredControlTintColor = .deltaPurple
self.present(safariViewController, animated: true, completion: nil)
}
tableView.deselectRow(at: indexPath, animated: true)
case .credits:
let row = CreditsRow(rawValue: indexPath.row)!
switch row
{
case .riley: self.openTwitter(username: "rileytestut")
case .shane: self.openTwitter(username: "shanegillio")
case .caroline: self.openTwitter(username: "1carolinemoore")
case .grant: self.openTwitter(username: "grantgliner")
case .litRitt: self.openTwitter(username: "lit_ritt")
case .contributors:
guard #available(iOS 14, *) else { return }
self.showContributors()
case .softwareLicenses: break
}
}
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
primary:
switch Section(rawValue: indexPath.section)!
{
case .credits:
let row = CreditsRow(rawValue: indexPath.row)!
switch row
{
case .grant:
// Hide row on iOS 14 and above
guard #available(iOS 14, *) else { break primary }
return 0.0
case .litRitt:
// Hide row on iOS 14 and above
guard #available(iOS 14, *) else { break primary }
return 0.0
case .contributors:
// Hide row on iOS 13 and below
guard #unavailable(iOS 14) else { break primary }
return 0.0
default: break
}
default: break
}
return super.tableView(tableView, heightForRowAt: indexPath)
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?
{
let section = Section(rawValue: section)!
guard !isSectionHidden(section) else { return nil }
switch section
{
case .hapticTouch where self.view.traitCollection.forceTouchCapability == .available: return NSLocalizedString("3D Touch", comment: "")
default: return super.tableView(tableView, titleForHeaderInSection: section.rawValue)
}
}
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String?
{
let section = Section(rawValue: section)!
if isSectionHidden(section)
{
return nil
}
else
{
return super.tableView(tableView, titleForFooterInSection: section.rawValue)
}
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
{
let section = Section(rawValue: section)!
if isSectionHidden(section)
{
return 1
}
else
{
return super.tableView(tableView, heightForHeaderInSection: section.rawValue)
}
}
override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat
{
let section = Section(rawValue: section)!
if isSectionHidden(section)
{
return 1
}
else
{
return super.tableView(tableView, heightForFooterInSection: section.rawValue)
}
}
}