// // MelonDSCoreSettingsViewController.swift // Delta // // Created by Riley Testut on 4/13/20. // Copyright © 2020 Riley Testut. All rights reserved. // import UIKit import SafariServices import MobileCoreServices import DeltaCore import MelonDSDeltaCore import struct DSDeltaCore.DS import Roxas private extension MelonDSCoreSettingsViewController { enum Section: Int { case general case dsBIOS case dsiBIOS case changeCore } enum DSBIOS: Int { case bios7 case bios9 case firmware var fileURL: URL { switch self { case .bios7: return MelonDSEmulatorBridge.shared.bios7URL case .bios9: return MelonDSEmulatorBridge.shared.bios9URL case .firmware: return MelonDSEmulatorBridge.shared.firmwareURL } } } enum DSiBIOS: Int { case bios7 case bios9 case firmware case nand var fileURL: URL { switch self { case .bios7: return MelonDSEmulatorBridge.shared.dsiBIOS7URL case .bios9: return MelonDSEmulatorBridge.shared.dsiBIOS9URL case .firmware: return MelonDSEmulatorBridge.shared.dsiFirmwareURL case .nand: return MelonDSEmulatorBridge.shared.dsiNANDURL } } } } class MelonDSCoreSettingsViewController: UITableViewController { private var importDestinationURL: URL? override func viewDidLoad() { super.viewDidLoad() if let navigationController = self.navigationController, navigationController.viewControllers.first != self { self.navigationItem.rightBarButtonItem = nil } NotificationCenter.default.addObserver(self, selector: #selector(MelonDSCoreSettingsViewController.willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.tableView.reloadData() } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) if let core = Delta.registeredCores[.ds] { DatabaseManager.shared.performBackgroundTask { (context) in // Prepare database in case we changed/updated cores. DatabaseManager.shared.prepare(core, in: context) context.saveWithErrorLogging() } } } } private extension MelonDSCoreSettingsViewController { func openMetadataURL(for key: DeltaCoreMetadata.Key) { guard let metadata = Settings.preferredCore(for: .ds)?.metadata else { return } let item = metadata[key] guard let url = item?.url else { if let indexPath = self.tableView.indexPathForSelectedRow { self.tableView.deselectRow(at: indexPath, animated: true) } return } let safariViewController = SFSafariViewController(url: url) safariViewController.preferredControlTintColor = .deltaPurple self.present(safariViewController, animated: true, completion: nil) } func locateBIOS(for destinationURL: URL) { self.importDestinationURL = destinationURL var supportedTypes = [kUTTypeItem as String, kUTTypeContent as String, "com.apple.macbinary-archive" /* System UTI for .bin */] // Explicitly support files with .bin and .rom extensions. if let binTypes = UTTypeCreateAllIdentifiersForTag(kUTTagClassFilenameExtension, "bin" as CFString, nil)?.takeRetainedValue() { let types = (binTypes as NSArray).map { $0 as! String } supportedTypes.append(contentsOf: types) } if let romTypes = UTTypeCreateAllIdentifiersForTag(kUTTagClassFilenameExtension, "rom" as CFString, nil)?.takeRetainedValue() { let types = (romTypes as NSArray).map { $0 as! String } supportedTypes.append(contentsOf: types) } let documentPicker = UIDocumentPickerViewController(documentTypes: supportedTypes, in: .import) documentPicker.delegate = self if #available(iOS 13.0, *) { documentPicker.overrideUserInterfaceStyle = .dark } self.present(documentPicker, animated: true, completion: nil) } func changeCore() { let alertController = UIAlertController(title: NSLocalizedString("Change Emulator Core", comment: ""), message: NSLocalizedString("Save states are not compatible between different emulator cores. Make sure to use in-game saves in order to keep using your save data.\n\nYour existing save states will not be deleted and will be available whenever you switch cores again.", comment: ""), preferredStyle: .actionSheet) var desmumeActionTitle = DS.core.metadata?.name.value ?? DS.core.name var melonDSActionTitle = MelonDS.core.metadata?.name.value ?? MelonDS.core.name if Settings.preferredCore(for: .ds) == DS.core { desmumeActionTitle += " ✓" } else { melonDSActionTitle += " ✓" } alertController.addAction(UIAlertAction(title: desmumeActionTitle, style: .default, handler: { (action) in Settings.setPreferredCore(DS.core, for: .ds) self.tableView.reloadData() })) alertController.addAction(UIAlertAction(title: melonDSActionTitle, style: .default, handler: { (action) in Settings.setPreferredCore(MelonDS.core, for: .ds) self.tableView.reloadData() })) alertController.addAction(.cancel) self.present(alertController, animated: true, completion: nil) if let indexPath = self.tableView.indexPathForSelectedRow { self.tableView.deselectRow(at: indexPath, animated: true) } } @objc func willEnterForeground(_ notification: Notification) { self.tableView.reloadData() } } extension MelonDSCoreSettingsViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = super.tableView(tableView, cellForRowAt: indexPath) switch Section(rawValue: indexPath.section)! { case .general: let key = DeltaCoreMetadata.Key.allCases[indexPath.row] let item = Settings.preferredCore(for: .ds)?.metadata?[key] cell.detailTextLabel?.text = item?.value ?? NSLocalizedString("-", comment: "") cell.detailTextLabel?.textColor = .gray if item?.url != nil { cell.accessoryType = .disclosureIndicator cell.selectionStyle = .default } else { cell.accessoryType = .none cell.selectionStyle = .none } cell.contentView.isHidden = (item == nil) case .dsBIOS: let bios = DSBIOS(rawValue: indexPath.row)! if FileManager.default.fileExists(atPath: bios.fileURL.path) { cell.accessoryType = .checkmark cell.detailTextLabel?.text = nil cell.detailTextLabel?.textColor = .gray } else { cell.accessoryType = .disclosureIndicator cell.detailTextLabel?.text = NSLocalizedString("Required", comment: "") cell.detailTextLabel?.textColor = .red } cell.selectionStyle = .default case .dsiBIOS: let bios = DSiBIOS(rawValue: indexPath.row)! if FileManager.default.fileExists(atPath: bios.fileURL.path) { cell.accessoryType = .checkmark cell.detailTextLabel?.text = nil cell.detailTextLabel?.textColor = .gray } else { cell.accessoryType = .disclosureIndicator cell.detailTextLabel?.text = NSLocalizedString("Required", comment: "") cell.detailTextLabel?.textColor = .red } cell.selectionStyle = .default case .changeCore: break } return cell } override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { guard let core = Settings.preferredCore(for: .ds) else { return } let key = DeltaCoreMetadata.Key.allCases[indexPath.row] let lastKey = DeltaCoreMetadata.Key.allCases.reversed().first { core.metadata?[$0] != nil } if key == lastKey { // Hide separator for last visible row in case we've hidden additional rows. cell.separatorInset.left = 0 } else { cell.separatorInset.left = self.view.layoutMargins.left } } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { switch Section(rawValue: indexPath.section)! { case .general: let key = DeltaCoreMetadata.Key.allCases[indexPath.row] self.openMetadataURL(for: key) case .dsBIOS: let bios = DSBIOS(rawValue: indexPath.row)! self.locateBIOS(for: bios.fileURL) case .dsiBIOS: let bios = DSiBIOS(rawValue: indexPath.row)! self.locateBIOS(for: bios.fileURL) case .changeCore: self.changeCore() } } override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { switch Section(rawValue: section)! { case .dsBIOS, .dsiBIOS: guard Settings.preferredCore(for: .ds) == MelonDS.core else { return nil } default: break } return super.tableView(tableView, titleForHeaderInSection: section) } override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { switch Section(rawValue: section)! { case .dsBIOS, .dsiBIOS: guard Settings.preferredCore(for: .ds) == MelonDS.core else { return nil } default: break } return super.tableView(tableView, titleForFooterInSection: section) } override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { switch Section(rawValue: indexPath.section)! { case .general: let key = DeltaCoreMetadata.Key.allCases[indexPath.row] guard Settings.preferredCore(for: .ds)?.metadata?[key] != nil else { return 0 } case .dsBIOS, .dsiBIOS: guard Settings.preferredCore(for: .ds) == MelonDS.core else { return 0 } default: break } return super.tableView(tableView, heightForRowAt: indexPath) } override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { switch Section(rawValue: section)! { case .dsBIOS, .dsiBIOS: guard Settings.preferredCore(for: .ds) == MelonDS.core else { return 1 } default: break } return super.tableView(tableView, heightForHeaderInSection: section) } override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { switch Section(rawValue: section)! { case .dsBIOS, .dsiBIOS: guard Settings.preferredCore(for: .ds) == MelonDS.core else { return 1 } default: break } return super.tableView(tableView, heightForFooterInSection: section) } } extension MelonDSCoreSettingsViewController: UIDocumentPickerDelegate { func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { self.importDestinationURL = nil self.tableView.reloadData() // Reloading index path causes cell to disappear... } func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { defer { self.importDestinationURL = nil self.tableView.reloadData() // Reloading index path causes cell to disappear... } guard let fileURL = urls.first, let destinationURL = self.importDestinationURL else { return } do { try FileManager.default.copyItem(at: fileURL, to: destinationURL, shouldReplace: true) } catch { let title = String(format: NSLocalizedString("Could not import %@.", comment: ""), fileURL.lastPathComponent) let alertController = UIAlertController(title: title, message: error.localizedDescription, preferredStyle: .alert) alertController.addAction(.ok) self.present(alertController, animated: true, completion: nil) } } }