Merge branch 'ipad' into develop

# Conflicts:
#	Delta.xcodeproj/project.pbxproj
This commit is contained in:
Riley Testut 2022-05-31 17:51:31 -07:00
commit 7c934cebe1
14 changed files with 286 additions and 49 deletions

@ -1 +1 @@
Subproject commit e2b3f0e46b4c64670e13fd0466ebdac719f84555 Subproject commit 2a6779e1271bc5d2e09aea2aa41fa6a0b75b62aa

View File

@ -168,6 +168,7 @@
D524F4A1273DE9A100D500B2 /* AltKit in Frameworks */ = {isa = PBXBuildFile; productRef = D524F4A0273DE9A100D500B2 /* AltKit */; }; D524F4A1273DE9A100D500B2 /* AltKit in Frameworks */ = {isa = PBXBuildFile; productRef = D524F4A0273DE9A100D500B2 /* AltKit */; };
D524F4A3273DE9C000D500B2 /* ProcessInfo+JIT.swift in Sources */ = {isa = PBXBuildFile; fileRef = D524F4A2273DE9C000D500B2 /* ProcessInfo+JIT.swift */; }; D524F4A3273DE9C000D500B2 /* ProcessInfo+JIT.swift in Sources */ = {isa = PBXBuildFile; fileRef = D524F4A2273DE9C000D500B2 /* ProcessInfo+JIT.swift */; };
D524F4A5273DEBB400D500B2 /* ServerManager+Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = D524F4A4273DEBB400D500B2 /* ServerManager+Delta.swift */; }; D524F4A5273DEBB400D500B2 /* ServerManager+Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = D524F4A4273DEBB400D500B2 /* ServerManager+Delta.swift */; };
D5011C48281B6E8B00A0760B /* CharacterSet+Filename.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5011C47281B6E8B00A0760B /* CharacterSet+Filename.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -363,6 +364,7 @@
C786AF1D2DDB6223BE2063CC /* Pods-Delta.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Delta.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Delta/Pods-Delta.debug.xcconfig"; sourceTree = "<group>"; }; C786AF1D2DDB6223BE2063CC /* Pods-Delta.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Delta.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Delta/Pods-Delta.debug.xcconfig"; sourceTree = "<group>"; };
D524F4A2273DE9C000D500B2 /* ProcessInfo+JIT.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+JIT.swift"; sourceTree = "<group>"; }; D524F4A2273DE9C000D500B2 /* ProcessInfo+JIT.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+JIT.swift"; sourceTree = "<group>"; };
D524F4A4273DEBB400D500B2 /* ServerManager+Delta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ServerManager+Delta.swift"; sourceTree = "<group>"; }; D524F4A4273DEBB400D500B2 /* ServerManager+Delta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ServerManager+Delta.swift"; sourceTree = "<group>"; };
D5011C47281B6E8B00A0760B /* CharacterSet+Filename.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CharacterSet+Filename.swift"; sourceTree = "<group>"; };
DC866E433B3BA9AE18ABA1EC /* libPods-Delta.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Delta.a"; sourceTree = BUILT_PRODUCTS_DIR; }; DC866E433B3BA9AE18ABA1EC /* libPods-Delta.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Delta.a"; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -411,6 +413,7 @@
BFE56E1823EB7BE00014FECD /* UIImage+SymbolFallback.swift */, BFE56E1823EB7BE00014FECD /* UIImage+SymbolFallback.swift */,
D524F4A2273DE9C000D500B2 /* ProcessInfo+JIT.swift */, D524F4A2273DE9C000D500B2 /* ProcessInfo+JIT.swift */,
D524F4A4273DEBB400D500B2 /* ServerManager+Delta.swift */, D524F4A4273DEBB400D500B2 /* ServerManager+Delta.swift */,
D5011C47281B6E8B00A0760B /* CharacterSet+Filename.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1126,6 +1129,7 @@
BF525EEA1FF6CD12004AA849 /* DeepLink.swift in Sources */, BF525EEA1FF6CD12004AA849 /* DeepLink.swift in Sources */,
BF8A334621A4926F00A42FD4 /* GameSyncStatusViewController.swift in Sources */, BF8A334621A4926F00A42FD4 /* GameSyncStatusViewController.swift in Sources */,
BF59427E1E09BC830051894B /* Game.swift in Sources */, BF59427E1E09BC830051894B /* Game.swift in Sources */,
D5011C48281B6E8B00A0760B /* CharacterSet+Filename.swift in Sources */,
BFE593CC21F3F8C2003412A6 /* _GameSave.swift in Sources */, BFE593CC21F3F8C2003412A6 /* _GameSave.swift in Sources */,
BF63A1A321A4AAAE00EE8F61 /* RecordSyncStatusViewController.swift in Sources */, BF63A1A321A4AAAE00EE8F61 /* RecordSyncStatusViewController.swift in Sources */,
BFAA1FED1B8AA4FA00495943 /* Settings.swift in Sources */, BFAA1FED1B8AA4FA00495943 /* Settings.swift in Sources */,
@ -1453,6 +1457,7 @@
SWIFT_OBJC_BRIDGING_HEADER = "Delta/Supporting Files/Delta-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Delta/Supporting Files/Delta-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
}; };
name = Debug; name = Debug;
}; };
@ -1482,6 +1487,7 @@
SWIFT_OBJC_BRIDGING_HEADER = "Delta/Supporting Files/Delta-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Delta/Supporting Files/Delta-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
}; };
name = Release; name = Release;
}; };

View File

@ -46,6 +46,9 @@
</connections> </connections>
</barButtonItem> </barButtonItem>
</navigationItem> </navigationItem>
<connections>
<outlet property="importButton" destination="FeA-O5-xd2" id="A44-3S-Okz"/>
</connections>
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="JYx-xE-nis" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="JYx-xE-nis" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>

View File

@ -1048,6 +1048,7 @@
<objects> <objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="0QR-U9-gtx" customClass="RSTNavigationController" sceneMemberID="viewController"> <navigationController automaticallyAdjustsScrollViewInsets="NO" id="0QR-U9-gtx" customClass="RSTNavigationController" sceneMemberID="viewController">
<toolbarItems/> <toolbarItems/>
<value key="contentSizeForViewInPopover" type="size" width="375" height="667"/>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" barStyle="black" prompted="NO"/> <simulatedNavigationBarMetrics key="simulatedTopBarMetrics" barStyle="black" prompted="NO"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" id="Y5H-O6-CQ5"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" id="Y5H-O6-CQ5">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/> <rect key="frame" x="0.0" y="0.0" width="375" height="44"/>

View File

@ -15,6 +15,8 @@ extension UIActivity.ActivityType
class CopyDeepLinkActivity: UIActivity class CopyDeepLinkActivity: UIActivity
{ {
private var deepLink: URL?
override class var activityCategory: UIActivity.Category { override class var activityCategory: UIActivity.Category {
return .action return .action
} }
@ -28,7 +30,7 @@ class CopyDeepLinkActivity: UIActivity
} }
override var activityImage: UIImage? { override var activityImage: UIImage? {
return UIImage(named: "Link") return UIImage(symbolNameIfAvailable: "link") ?? UIImage(named: "Link")
} }
override func canPerform(withActivityItems activityItems: [Any]) -> Bool override func canPerform(withActivityItems activityItems: [Any]) -> Bool
@ -47,7 +49,19 @@ class CopyDeepLinkActivity: UIActivity
{ {
guard let game = activityItems.first(where: { $0 is Game }) as? Game else { return } guard let game = activityItems.first(where: { $0 is Game }) as? Game else { return }
let deepLink = URL(action: .launchGame(identifier: game.identifier)) self.deepLink = URL(action: .launchGame(identifier: game.identifier))
}
override func perform()
{
if let deepLink = self.deepLink
{
UIPasteboard.general.url = deepLink UIPasteboard.general.url = deepLink
self.activityDidFinish(true)
}
else
{
self.activityDidFinish(false)
}
} }
} }

View File

@ -0,0 +1,22 @@
//
// CharacterSet+Filename.swift
// Delta
//
// Created by Riley Testut on 4/28/22.
// Copyright © 2022 Riley Testut. All rights reserved.
//
import Foundation
extension CharacterSet
{
// Different than .urlPathAllowed
// Copied from https://stackoverflow.com/a/39443252
static var urlFilenameAllowed: CharacterSet {
var illegalCharacters = CharacterSet(charactersIn: ":/")
illegalCharacters.formUnion(.newlines)
illegalCharacters.formUnion(.illegalCharacters)
illegalCharacters.formUnion(.controlCharacters)
return illegalCharacters.inverted
}
}

View File

@ -66,6 +66,8 @@ class GameCollectionViewController: UICollectionViewController
private weak var _previewTransitionViewController: PreviewGameViewController? private weak var _previewTransitionViewController: PreviewGameViewController?
private weak var _previewTransitionDestinationViewController: UIViewController? private weak var _previewTransitionDestinationViewController: UIViewController?
private weak var _popoverSourceView: UIView?
private var _renameAction: UIAlertAction? private var _renameAction: UIAlertAction?
private var _changingArtworkGame: Game? private var _changingArtworkGame: Game?
private var _importingSaveFileGame: Game? private var _importingSaveFileGame: Game?
@ -93,10 +95,6 @@ extension GameCollectionViewController
self.collectionView?.prefetchDataSource = self.dataSource self.collectionView?.prefetchDataSource = self.dataSource
self.collectionView?.delegate = self self.collectionView?.delegate = self
let layout = self.collectionViewLayout as! GridCollectionViewLayout
layout.itemWidth = 90
layout.minimumInteritemSpacing = 12
if #available(iOS 13, *) {} if #available(iOS 13, *) {}
else else
{ {
@ -105,6 +103,8 @@ extension GameCollectionViewController
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(GameCollectionViewController.handleLongPressGesture(_:))) let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(GameCollectionViewController.handleLongPressGesture(_:)))
self.collectionView?.addGestureRecognizer(longPressGestureRecognizer) self.collectionView?.addGestureRecognizer(longPressGestureRecognizer)
} }
self.update()
} }
override func viewWillDisappear(_ animated: Bool) override func viewWillDisappear(_ animated: Bool)
@ -131,6 +131,13 @@ extension GameCollectionViewController
super.didReceiveMemoryWarning() super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated. // Dispose of any resources that can be recreated.
} }
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
{
super.traitCollectionDidChange(previousTraitCollection)
self.update()
}
} }
//MARK: - Segues - //MARK: - Segues -
@ -224,6 +231,26 @@ extension GameCollectionViewController
//MARK: - Private Methods - //MARK: - Private Methods -
private extension GameCollectionViewController private extension GameCollectionViewController
{ {
func update()
{
let layout = self.collectionViewLayout as! GridCollectionViewLayout
switch self.traitCollection.horizontalSizeClass
{
case .regular:
layout.itemWidth = 150
layout.minimumInteritemSpacing = 25 // 30 == only 3 games per line for iPad mini 6 in portrait
case .unspecified, .compact:
layout.itemWidth = 90
layout.minimumInteritemSpacing = 12
@unknown default: break
}
self.collectionView.reloadData()
}
//MARK: - Data Source //MARK: - Data Source
func prepareDataSource() func prepareDataSource()
{ {
@ -284,7 +311,19 @@ private extension GameCollectionViewController
cell.imageView.image = #imageLiteral(resourceName: "BoxArt") cell.imageView.image = #imageLiteral(resourceName: "BoxArt")
cell.maximumImageSize = CGSize(width: 90, height: 90) if self.traitCollection.horizontalSizeClass == .regular
{
let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .subheadline).withSymbolicTraits(.traitBold)!
cell.textLabel.font = UIFont(descriptor: fontDescriptor, size: 0)
}
else
{
cell.textLabel.font = UIFont.preferredFont(forTextStyle: .caption1)
}
let layout = self.collectionViewLayout as! GridCollectionViewLayout
cell.maximumImageSize = CGSize(width: layout.itemWidth, height: layout.itemWidth)
cell.textLabel.text = game.name cell.textLabel.text = game.name
cell.textLabel.textColor = UIColor.gray cell.textLabel.textColor = UIColor.gray
cell.tintColor = cell.textLabel.textColor cell.tintColor = cell.textLabel.textColor
@ -484,7 +523,9 @@ private extension GameCollectionViewController
func delete(_ game: Game) func delete(_ game: Game)
{ {
let confirmationAlertController = UIAlertController(title: NSLocalizedString("Are you sure you want to delete this game? All associated data, such as saves, save states, and cheat codes, will also be deleted.", comment: ""), message: nil, preferredStyle: .actionSheet) let confirmationAlertController = UIAlertController(title: NSLocalizedString("Are you sure you want to delete this game?", comment: ""),
message: NSLocalizedString("All associated data, such as saves, save states, and cheat codes, will also be deleted.", comment: ""),
preferredStyle: .alert)
confirmationAlertController.addAction(UIAlertAction(title: NSLocalizedString("Delete Game", comment: ""), style: .destructive, handler: { action in confirmationAlertController.addAction(UIAlertAction(title: NSLocalizedString("Delete Game", comment: ""), style: .destructive, handler: { action in
DatabaseManager.shared.performBackgroundTask { (context) in DatabaseManager.shared.performBackgroundTask { (context) in
@ -554,6 +595,7 @@ private extension GameCollectionViewController
let importController = ImportController(documentTypes: [kUTTypeImage as String]) let importController = ImportController(documentTypes: [kUTTypeImage as String])
importController.delegate = self importController.delegate = self
importController.importOptions = [clipboardImportOption, photoLibraryImportOption, gamesDatabaseImportOption] importController.importOptions = [clipboardImportOption, photoLibraryImportOption, gamesDatabaseImportOption]
importController.sourceView = self._popoverSourceView
self.present(importController, animated: true, completion: nil) self.present(importController, animated: true, completion: nil)
} }
@ -663,26 +705,36 @@ private extension GameCollectionViewController
func share(_ game: Game) func share(_ game: Game)
{ {
let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
let symbolicURL = temporaryDirectory.appendingPathComponent(game.name + "." + game.fileURL.pathExtension)
let sanitizedName = game.name.components(separatedBy: .urlFilenameAllowed.inverted).joined()
let temporaryURL = temporaryDirectory.appendingPathComponent(sanitizedName + "." + game.fileURL.pathExtension, isDirectory: false)
do do
{ {
try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil) try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
// Create a symbolic link so we can control the file name used when sharing. // Make a temporary copy so we can control the filename used when sharing.
// Otherwise, if we just passed in game.fileURL to UIActivityViewController, the file name would be the game's SHA1 hash. // Otherwise, if we just passed in game.fileURL to UIActivityViewController, the file name would be the game's SHA1 hash.
try FileManager.default.createSymbolicLink(at: symbolicURL, withDestinationURL: game.fileURL) try FileManager.default.copyItem(at: game.fileURL, to: temporaryURL, shouldReplace: true)
} }
catch catch
{ {
print(error) let alertController = UIAlertController(title: NSLocalizedString("Could Not Share Game", comment: ""), error: error)
self.present(alertController, animated: true, completion: nil)
return
} }
let copyDeepLinkActivity = CopyDeepLinkActivity() let copyDeepLinkActivity = CopyDeepLinkActivity()
let activityViewController = UIActivityViewController(activityItems: [symbolicURL, game], applicationActivities: [copyDeepLinkActivity]) let activityViewController = UIActivityViewController(activityItems: [temporaryURL, game], applicationActivities: [copyDeepLinkActivity])
activityViewController.popoverPresentationController?.sourceView = self._popoverSourceView?.superview
activityViewController.popoverPresentationController?.sourceRect = self._popoverSourceView?.frame ?? .zero
activityViewController.completionWithItemsHandler = { (activityType, finished, returnedItems, error) in activityViewController.completionWithItemsHandler = { (activityType, finished, returnedItems, error) in
// Make sure the user either shared the game or cancelled before deleting temporaryDirectory.
guard finished || activityType == nil else { return }
do do
{ {
try FileManager.default.removeItem(at: temporaryDirectory) try FileManager.default.removeItem(at: temporaryDirectory)
@ -692,6 +744,7 @@ private extension GameCollectionViewController
print(error) print(error)
} }
} }
self.present(activityViewController, animated: true, completion: nil) self.present(activityViewController, animated: true, completion: nil)
} }
@ -747,8 +800,7 @@ private extension GameCollectionViewController
{ {
do do
{ {
let illegalCharacterSet = CharacterSet(charactersIn: "\"\\/?<>:*|") let sanitizedFilename = game.name.components(separatedBy: .urlFilenameAllowed.inverted).joined()
let sanitizedFilename = game.name.components(separatedBy: illegalCharacterSet).joined() + "." + game.gameSaveURL.pathExtension
let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(sanitizedFilename) let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(sanitizedFilename)
try FileManager.default.copyItem(at: game.gameSaveURL, to: temporaryURL, shouldReplace: true) try FileManager.default.copyItem(at: game.gameSaveURL, to: temporaryURL, shouldReplace: true)
@ -807,6 +859,9 @@ extension GameCollectionViewController: UIViewControllerPreviewingDelegate
previewingContext.sourceRect = layoutAttributes.frame previewingContext.sourceRect = layoutAttributes.frame
let cell = collectionView.cellForItem(at: indexPath)
self._popoverSourceView = cell
let game = self.dataSource.item(at: indexPath) let game = self.dataSource.item(at: indexPath)
let gameViewController = self.makePreviewGameViewController(for: game) let gameViewController = self.makePreviewGameViewController(for: game)
@ -974,6 +1029,9 @@ extension GameCollectionViewController
let game = self.dataSource.item(at: indexPath) let game = self.dataSource.item(at: indexPath)
let actions = self.actions(for: game) let actions = self.actions(for: game)
let cell = self.collectionView.cellForItem(at: indexPath)
self._popoverSourceView = cell
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: { [weak self] in return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: { [weak self] in
guard let self = self else { return nil } guard let self = self else { return nil }

View File

@ -47,6 +47,7 @@ class GamesViewController: UIViewController
private let fetchedResultsController: NSFetchedResultsController<NSFetchRequestResult> private let fetchedResultsController: NSFetchedResultsController<NSFetchRequestResult>
private var searchController: RSTSearchController? private var searchController: RSTSearchController?
private lazy var importController: ImportController = self.makeImportController()
private var syncingToastView: RSTToastView? { private var syncingToastView: RSTToastView? {
didSet { didSet {
@ -58,6 +59,8 @@ class GamesViewController: UIViewController
} }
private var syncingProgressObservation: NSKeyValueObservation? private var syncingProgressObservation: NSKeyValueObservation?
@IBOutlet private var importButton: UIBarButtonItem!
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
fatalError("initWithNibName: not implemented") fatalError("initWithNibName: not implemented")
} }
@ -114,10 +117,16 @@ extension GamesViewController
let navigationBarAppearance = navigationController.navigationBar.standardAppearance.copy() let navigationBarAppearance = navigationController.navigationBar.standardAppearance.copy()
navigationBarAppearance.backgroundEffect = UIBlurEffect(style: .dark) navigationBarAppearance.backgroundEffect = UIBlurEffect(style: .dark)
navigationController.navigationBar.standardAppearance = navigationBarAppearance navigationController.navigationBar.standardAppearance = navigationBarAppearance
navigationController.navigationBar.scrollEdgeAppearance = navigationBarAppearance
let toolbarAppearance = navigationController.toolbar.standardAppearance.copy() let toolbarAppearance = navigationController.toolbar.standardAppearance.copy()
toolbarAppearance.backgroundEffect = UIBlurEffect(style: .dark) toolbarAppearance.backgroundEffect = UIBlurEffect(style: .dark)
navigationController.toolbar.standardAppearance = toolbarAppearance navigationController.toolbar.standardAppearance = toolbarAppearance
if #available(iOS 15, *)
{
navigationController.toolbar.scrollEdgeAppearance = toolbarAppearance
}
} }
else else
{ {
@ -126,6 +135,22 @@ extension GamesViewController
} }
} }
if #available(iOS 14, *)
{
self.importController.presentingViewController = self
let importActions = self.importController.makeActions().menuActions
let importMenu = UIMenu(title: NSLocalizedString("Import From…", comment: ""), image: UIImage(systemName: "square.and.arrow.down"), children: importActions)
self.importButton.menu = importMenu
self.importButton.action = nil
self.importButton.target = nil
}
else
{
self.importController.barButtonItem = self.importButton
}
self.prepareSearchController() self.prepareSearchController()
self.updateTheme() self.updateTheme()
@ -352,7 +377,7 @@ private extension GamesViewController
/// Importing /// Importing
extension GamesViewController: ImportControllerDelegate extension GamesViewController: ImportControllerDelegate
{ {
@IBAction private func importFiles() private func makeImportController() -> ImportController
{ {
var documentTypes = Set(System.registeredSystems.map { $0.gameType.rawValue }) var documentTypes = Set(System.registeredSystems.map { $0.gameType.rawValue })
documentTypes.insert(kUTTypeZipArchive as String) documentTypes.insert(kUTTypeZipArchive as String)
@ -373,7 +398,13 @@ extension GamesViewController: ImportControllerDelegate
let importController = ImportController(documentTypes: documentTypes) let importController = ImportController(documentTypes: documentTypes)
importController.delegate = self importController.delegate = self
importController.importOptions = [itunesImportOption] importController.importOptions = [itunesImportOption]
self.present(importController, animated: true, completion: nil)
return importController
}
@IBAction private func importFiles()
{
self.present(self.importController, animated: true, completion: nil)
} }
func importController(_ importController: ImportController, didImportItemsAt urls: Set<URL>, errors: [Error]) func importController(_ importController: ImportController, didImportItemsAt urls: Set<URL>, errors: [Error])

View File

@ -13,7 +13,7 @@ import DeltaCore
struct iTunesImportOption: ImportOption struct iTunesImportOption: ImportOption
{ {
let title = NSLocalizedString("iTunes", comment: "") let title = NSLocalizedString("iTunes", comment: "")
let image: UIImage? = nil let image: UIImage? = UIImage(symbolNameIfAvailable: "music.note")
private let presentingViewController: UIViewController private let presentingViewController: UIViewController

View File

@ -37,7 +37,10 @@ class ImportController: NSObject
var delegate: ImportControllerDelegate? var delegate: ImportControllerDelegate?
var importOptions: [ImportOption]? var importOptions: [ImportOption]?
private weak var presentingViewController: UIViewController? weak var presentingViewController: UIViewController?
weak var barButtonItem: UIBarButtonItem?
weak var sourceView: UIView?
// Store presentedViewController separately, since when we dismiss we don't know if it has already been dismissed. // Store presentedViewController separately, since when we dismiss we don't know if it has already been dismissed.
// Calling dismiss on presentingViewController in that case would dismiss presentingViewController, which is bad. // Calling dismiss on presentingViewController in that case would dismiss presentingViewController, which is bad.
@ -61,26 +64,54 @@ class ImportController: NSObject
super.init() super.init()
} }
func makeActions() -> [Action]
{
assert(self.presentingViewController != nil, "presentingViewController must be set before calling makeActions()")
var actions = (self.importOptions ?? []).map { (option) -> Action in
let action = Action(title: option.title, style: .default, image: option.image) { _ in
option.import { importedURLs in
self.finish(with: importedURLs, errors: [])
}
}
return action
}
let filesAction = Action(title: NSLocalizedString("Files", comment: ""), style: .default, image: UIImage(symbolNameIfAvailable: "doc")) { action in
self.presentDocumentBrowser()
}
actions.append(filesAction)
return actions
}
fileprivate func presentImportController(from presentingViewController: UIViewController, animated: Bool, completionHandler: (() -> Void)?) fileprivate func presentImportController(from presentingViewController: UIViewController, animated: Bool, completionHandler: (() -> Void)?)
{ {
self.presentingViewController = presentingViewController self.presentingViewController = presentingViewController
let actions = self.makeActions()
if actions.count > 1
{
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
alertController.addAction(UIAlertAction.cancel) alertController.addAction(UIAlertAction.cancel)
if let importOptions = self.importOptions let alertActions = actions.map { UIAlertAction($0) }
for action in alertActions
{ {
for importOption in importOptions alertController.addAction(action)
{
alertController.add(importOption) { [unowned self] (urls) in
self.finish(with: urls, errors: [])
}
} }
let filesAction = UIAlertAction(title: NSLocalizedString("Files", comment: ""), style: .default) { (action) in if let sourceView = self.sourceView
self.presentDocumentBrowser() {
alertController.popoverPresentationController?.sourceView = sourceView.superview
alertController.popoverPresentationController?.sourceRect = sourceView.frame
}
else
{
alertController.popoverPresentationController?.barButtonItem = self.barButtonItem
} }
alertController.addAction(filesAction)
self.presentedViewController = alertController self.presentedViewController = alertController
self.presentingViewController?.present(alertController, animated: true, completion: nil) self.presentingViewController?.present(alertController, animated: true, completion: nil)
@ -198,7 +229,7 @@ private var ImportControllerKey: UInt8 = 0
extension UIViewController extension UIViewController
{ {
fileprivate(set) var importController: ImportController? fileprivate var importController: ImportController?
{ {
set set
{ {

View File

@ -93,14 +93,6 @@ extension SaveStatesViewController
self.collectionView?.dataSource = self.dataSource self.collectionView?.dataSource = self.dataSource
self.collectionView?.prefetchDataSource = self.dataSource self.collectionView?.prefetchDataSource = self.dataSource
let collectionViewLayout = self.collectionViewLayout as! GridCollectionViewLayout
let averageHorizontalInset = (collectionViewLayout.sectionInset.left + collectionViewLayout.sectionInset.right) / 2
let portraitScreenWidth = UIScreen.main.coordinateSpace.convert(UIScreen.main.bounds, to: UIScreen.main.fixedCoordinateSpace).width
// Use dimensions that allow two cells to fill the screen horizontally with padding in portrait mode
// We'll keep the same size for landscape orientation, which will allow more to fit
collectionViewLayout.itemWidth = floor((portraitScreenWidth - (averageHorizontalInset * 3)) / 2)
switch self.mode switch self.mode
{ {
case .saving: case .saving:
@ -113,8 +105,7 @@ extension SaveStatesViewController
self.navigationItem.rightBarButtonItems?.removeFirst() self.navigationItem.rightBarButtonItems?.removeFirst()
} }
// Manually update prototype cell properties self.prototypeCellWidthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: 0)
self.prototypeCellWidthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionViewLayout.itemWidth)
self.prototypeCellWidthConstraint.isActive = true self.prototypeCellWidthConstraint.isActive = true
self.prepareEmulatorCoreSaveState() self.prepareEmulatorCoreSaveState()
@ -238,6 +229,26 @@ private extension SaveStatesViewController
} }
self.sortButton.transform = CGAffineTransform.identity.rotated(by: Settings.sortSaveStatesByOldestFirst ? 0 : .pi) self.sortButton.transform = CGAffineTransform.identity.rotated(by: Settings.sortSaveStatesByOldestFirst ? 0 : .pi)
let collectionViewLayout = self.collectionViewLayout as! GridCollectionViewLayout
if self.traitCollection.horizontalSizeClass == .regular
{
collectionViewLayout.itemWidth = 180
collectionViewLayout.minimumInteritemSpacing = 30
}
else
{
let averageHorizontalInset = (collectionViewLayout.sectionInset.left + collectionViewLayout.sectionInset.right) / 2
let portraitScreenWidth = UIScreen.main.coordinateSpace.convert(UIScreen.main.bounds, to: UIScreen.main.fixedCoordinateSpace).width
// Use dimensions that allow two cells to fill the screen horizontally with padding in portrait mode
// We'll keep the same size for landscape orientation, which will allow more to fit
collectionViewLayout.itemWidth = floor((portraitScreenWidth - (averageHorizontalInset * 3)) / 2)
}
// Manually update prototype cell properties
self.prototypeCellWidthConstraint.constant = collectionViewLayout.itemWidth
} }
//MARK: - Configure Views - //MARK: - Configure Views -

View File

@ -40,6 +40,8 @@ class ControllerInputsViewController: UIViewController
private var activeCalloutView: InputCalloutView? private var activeCalloutView: InputCalloutView?
private var _didLayoutSubviews = false
@IBOutlet private var actionsMenuViewControllerHeightConstraint: NSLayoutConstraint! @IBOutlet private var actionsMenuViewControllerHeightConstraint: NSLayoutConstraint!
@IBOutlet private var cancelTapGestureRecognizer: UITapGestureRecognizer! @IBOutlet private var cancelTapGestureRecognizer: UITapGestureRecognizer!
@ -65,7 +67,15 @@ class ControllerInputsViewController: UIViewController
self.gameViewController.controllerView.addReceiver(self) self.gameViewController.controllerView.addReceiver(self)
if let navigationController = self.navigationController, #available(iOS 13, *)
{
navigationController.overrideUserInterfaceStyle = .dark
navigationController.navigationBar.scrollEdgeAppearance = navigationController.navigationBar.standardAppearance // Fixes invisible navigation bar on iPad.
}
else
{
self.navigationController?.navigationBar.barStyle = .black self.navigationController?.navigationBar.barStyle = .black
}
NSLayoutConstraint.activate([self.gameViewController.gameView.centerYAnchor.constraint(equalTo: self.actionsMenuViewController.view.centerYAnchor)]) NSLayoutConstraint.activate([self.gameViewController.gameView.centerYAnchor.constraint(equalTo: self.actionsMenuViewController.view.centerYAnchor)])
@ -81,6 +91,23 @@ class ControllerInputsViewController: UIViewController
{ {
self.actionsMenuViewControllerHeightConstraint.constant = self.actionsMenuViewController.preferredContentSize.height self.actionsMenuViewControllerHeightConstraint.constant = self.actionsMenuViewController.preferredContentSize.height
} }
if let window = self.view.window, !_didLayoutSubviews
{
var traits = DeltaCore.ControllerSkin.Traits.defaults(for: window)
traits.orientation = .portrait
if traits.device == .ipad
{
// Use standard iPhone skins instead of iPad skins.
traits.device = .iphone
traits.displayType = .standard
}
self.gameViewController.controllerView.overrideControllerSkinTraits = traits
_didLayoutSubviews = true
}
} }
override func viewDidAppear(_ animated: Bool) override func viewDidAppear(_ animated: Bool)
@ -403,6 +430,7 @@ private extension ControllerInputsViewController
} }
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
alertController.popoverPresentationController?.barButtonItem = sender
alertController.addAction(.cancel) alertController.addAction(.cancel)
alertController.addAction(UIAlertAction(title: NSLocalizedString("Reset Controls to Defaults", comment: ""), style: .destructive, handler: { (action) in alertController.addAction(UIAlertAction(title: NSLocalizedString("Reset Controls to Defaults", comment: ""), style: .destructive, handler: { (action) in
reset() reset()
@ -562,3 +590,15 @@ extension ControllerInputsViewController: SMCalloutViewDelegate
self.toggle(calloutView) self.toggle(calloutView)
} }
} }
extension ControllerInputsViewController: UIAdaptivePresentationControllerDelegate
{
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle
{
switch (traitCollection.horizontalSizeClass, traitCollection.verticalSizeClass)
{
case (.regular, .regular): return .formSheet // Regular width and height, so display as form sheet
default: return .fullScreen // Compact width and/or height, so display full screen
}
}
}

View File

@ -108,9 +108,18 @@ extension ControllersSettingsViewController
switch identifier switch identifier
{ {
case "controllerInputsSegue": case "controllerInputsSegue":
let controllerInputsViewController = (segue.destination as! UINavigationController).topViewController as! ControllerInputsViewController let navigationController = segue.destination as! UINavigationController
let controllerInputsViewController = navigationController.topViewController as! ControllerInputsViewController
controllerInputsViewController.gameController = self.gameController controllerInputsViewController.gameController = self.gameController
if self.view.traitCollection.userInterfaceIdiom == .pad
{
// For now, only iPads can display ControllerInputsViewController as a form sheet.
navigationController.modalPresentationStyle = .formSheet
navigationController.presentationController?.delegate = controllerInputsViewController
}
default: break default: break
} }
@ -296,6 +305,8 @@ extension ControllersSettingsViewController
{ {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{ {
let previousGameController = self.gameController
switch Section(rawValue: indexPath.section)! switch Section(rawValue: indexPath.section)!
{ {
case .localDevice: self.gameController = self.localDeviceController case .localDevice: self.gameController = self.localDeviceController
@ -310,7 +321,7 @@ extension ControllersSettingsViewController
let previousIndexPath: IndexPath? let previousIndexPath: IndexPath?
if let gameController = self.gameController if let gameController = previousGameController
{ {
if gameController == self.localDeviceController if gameController == self.localDeviceController
{ {

View File

@ -215,6 +215,8 @@
<array> <array>
<string>armv7</string> <string>armv7</string>
</array> </array>
<key>UIRequiresFullScreen</key>
<true/>
<key>UIStatusBarStyle</key> <key>UIStatusBarStyle</key>
<string>UIStatusBarStyleLightContent</string> <string>UIStatusBarStyleLightContent</string>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
@ -223,6 +225,13 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportsDocumentBrowser</key> <key>UISupportsDocumentBrowser</key>
<true/> <true/>
<key>UTExportedTypeDeclarations</key> <key>UTExportedTypeDeclarations</key>