Merge branch 'ipad' into develop
# Conflicts: # Delta.xcodeproj/project.pbxproj
This commit is contained in:
commit
7c934cebe1
@ -1 +1 @@
|
||||
Subproject commit e2b3f0e46b4c64670e13fd0466ebdac719f84555
|
||||
Subproject commit 2a6779e1271bc5d2e09aea2aa41fa6a0b75b62aa
|
||||
@ -168,6 +168,7 @@
|
||||
D524F4A1273DE9A100D500B2 /* AltKit in Frameworks */ = {isa = PBXBuildFile; productRef = D524F4A0273DE9A100D500B2 /* AltKit */; };
|
||||
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 */; };
|
||||
D5011C48281B6E8B00A0760B /* CharacterSet+Filename.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5011C47281B6E8B00A0760B /* CharacterSet+Filename.swift */; };
|
||||
/* End PBXBuildFile 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>"; };
|
||||
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>"; };
|
||||
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; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
@ -411,6 +413,7 @@
|
||||
BFE56E1823EB7BE00014FECD /* UIImage+SymbolFallback.swift */,
|
||||
D524F4A2273DE9C000D500B2 /* ProcessInfo+JIT.swift */,
|
||||
D524F4A4273DEBB400D500B2 /* ServerManager+Delta.swift */,
|
||||
D5011C47281B6E8B00A0760B /* CharacterSet+Filename.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
@ -1126,6 +1129,7 @@
|
||||
BF525EEA1FF6CD12004AA849 /* DeepLink.swift in Sources */,
|
||||
BF8A334621A4926F00A42FD4 /* GameSyncStatusViewController.swift in Sources */,
|
||||
BF59427E1E09BC830051894B /* Game.swift in Sources */,
|
||||
D5011C48281B6E8B00A0760B /* CharacterSet+Filename.swift in Sources */,
|
||||
BFE593CC21F3F8C2003412A6 /* _GameSave.swift in Sources */,
|
||||
BF63A1A321A4AAAE00EE8F61 /* RecordSyncStatusViewController.swift in Sources */,
|
||||
BFAA1FED1B8AA4FA00495943 /* Settings.swift in Sources */,
|
||||
@ -1453,6 +1457,7 @@
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Delta/Supporting Files/Delta-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@ -1482,6 +1487,7 @@
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Delta/Supporting Files/Delta-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
||||
@ -46,6 +46,9 @@
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="importButton" destination="FeA-O5-xd2" id="A44-3S-Okz"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="JYx-xE-nis" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
|
||||
@ -1048,6 +1048,7 @@
|
||||
<objects>
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="0QR-U9-gtx" customClass="RSTNavigationController" sceneMemberID="viewController">
|
||||
<toolbarItems/>
|
||||
<value key="contentSizeForViewInPopover" type="size" width="375" height="667"/>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" barStyle="black" prompted="NO"/>
|
||||
<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"/>
|
||||
|
||||
@ -15,6 +15,8 @@ extension UIActivity.ActivityType
|
||||
|
||||
class CopyDeepLinkActivity: UIActivity
|
||||
{
|
||||
private var deepLink: URL?
|
||||
|
||||
override class var activityCategory: UIActivity.Category {
|
||||
return .action
|
||||
}
|
||||
@ -28,7 +30,7 @@ class CopyDeepLinkActivity: UIActivity
|
||||
}
|
||||
|
||||
override var activityImage: UIImage? {
|
||||
return UIImage(named: "Link")
|
||||
return UIImage(symbolNameIfAvailable: "link") ?? UIImage(named: "Link")
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
let deepLink = URL(action: .launchGame(identifier: game.identifier))
|
||||
UIPasteboard.general.url = deepLink
|
||||
self.deepLink = URL(action: .launchGame(identifier: game.identifier))
|
||||
}
|
||||
|
||||
override func perform()
|
||||
{
|
||||
if let deepLink = self.deepLink
|
||||
{
|
||||
UIPasteboard.general.url = deepLink
|
||||
self.activityDidFinish(true)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.activityDidFinish(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
Delta/Extensions/CharacterSet+Filename.swift
Normal file
22
Delta/Extensions/CharacterSet+Filename.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@ -66,6 +66,8 @@ class GameCollectionViewController: UICollectionViewController
|
||||
private weak var _previewTransitionViewController: PreviewGameViewController?
|
||||
private weak var _previewTransitionDestinationViewController: UIViewController?
|
||||
|
||||
private weak var _popoverSourceView: UIView?
|
||||
|
||||
private var _renameAction: UIAlertAction?
|
||||
private var _changingArtworkGame: Game?
|
||||
private var _importingSaveFileGame: Game?
|
||||
@ -93,10 +95,6 @@ extension GameCollectionViewController
|
||||
self.collectionView?.prefetchDataSource = self.dataSource
|
||||
self.collectionView?.delegate = self
|
||||
|
||||
let layout = self.collectionViewLayout as! GridCollectionViewLayout
|
||||
layout.itemWidth = 90
|
||||
layout.minimumInteritemSpacing = 12
|
||||
|
||||
if #available(iOS 13, *) {}
|
||||
else
|
||||
{
|
||||
@ -105,6 +103,8 @@ extension GameCollectionViewController
|
||||
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(GameCollectionViewController.handleLongPressGesture(_:)))
|
||||
self.collectionView?.addGestureRecognizer(longPressGestureRecognizer)
|
||||
}
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool)
|
||||
@ -131,6 +131,13 @@ extension GameCollectionViewController
|
||||
super.didReceiveMemoryWarning()
|
||||
// Dispose of any resources that can be recreated.
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)
|
||||
{
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Segues -
|
||||
@ -224,6 +231,26 @@ extension GameCollectionViewController
|
||||
//MARK: - Private Methods -
|
||||
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
|
||||
func prepareDataSource()
|
||||
{
|
||||
@ -284,7 +311,19 @@ private extension GameCollectionViewController
|
||||
|
||||
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.textColor = UIColor.gray
|
||||
cell.tintColor = cell.textLabel.textColor
|
||||
@ -484,7 +523,9 @@ private extension GameCollectionViewController
|
||||
|
||||
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
|
||||
|
||||
DatabaseManager.shared.performBackgroundTask { (context) in
|
||||
@ -554,6 +595,7 @@ private extension GameCollectionViewController
|
||||
let importController = ImportController(documentTypes: [kUTTypeImage as String])
|
||||
importController.delegate = self
|
||||
importController.importOptions = [clipboardImportOption, photoLibraryImportOption, gamesDatabaseImportOption]
|
||||
importController.sourceView = self._popoverSourceView
|
||||
self.present(importController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@ -663,26 +705,36 @@ private extension GameCollectionViewController
|
||||
|
||||
func share(_ game: Game)
|
||||
{
|
||||
let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
let symbolicURL = temporaryDirectory.appendingPathComponent(game.name + "." + game.fileURL.pathExtension)
|
||||
let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
||||
|
||||
let sanitizedName = game.name.components(separatedBy: .urlFilenameAllowed.inverted).joined()
|
||||
let temporaryURL = temporaryDirectory.appendingPathComponent(sanitizedName + "." + game.fileURL.pathExtension, isDirectory: false)
|
||||
|
||||
do
|
||||
{
|
||||
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.
|
||||
try FileManager.default.createSymbolicLink(at: symbolicURL, withDestinationURL: game.fileURL)
|
||||
try FileManager.default.copyItem(at: game.fileURL, to: temporaryURL, shouldReplace: true)
|
||||
}
|
||||
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 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
|
||||
// Make sure the user either shared the game or cancelled before deleting temporaryDirectory.
|
||||
guard finished || activityType == nil else { return }
|
||||
|
||||
do
|
||||
{
|
||||
try FileManager.default.removeItem(at: temporaryDirectory)
|
||||
@ -692,6 +744,7 @@ private extension GameCollectionViewController
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
self.present(activityViewController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@ -747,8 +800,7 @@ private extension GameCollectionViewController
|
||||
{
|
||||
do
|
||||
{
|
||||
let illegalCharacterSet = CharacterSet(charactersIn: "\"\\/?<>:*|")
|
||||
let sanitizedFilename = game.name.components(separatedBy: illegalCharacterSet).joined() + "." + game.gameSaveURL.pathExtension
|
||||
let sanitizedFilename = game.name.components(separatedBy: .urlFilenameAllowed.inverted).joined()
|
||||
|
||||
let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(sanitizedFilename)
|
||||
try FileManager.default.copyItem(at: game.gameSaveURL, to: temporaryURL, shouldReplace: true)
|
||||
@ -807,6 +859,9 @@ extension GameCollectionViewController: UIViewControllerPreviewingDelegate
|
||||
|
||||
previewingContext.sourceRect = layoutAttributes.frame
|
||||
|
||||
let cell = collectionView.cellForItem(at: indexPath)
|
||||
self._popoverSourceView = cell
|
||||
|
||||
let game = self.dataSource.item(at: indexPath)
|
||||
|
||||
let gameViewController = self.makePreviewGameViewController(for: game)
|
||||
@ -974,6 +1029,9 @@ extension GameCollectionViewController
|
||||
let game = self.dataSource.item(at: indexPath)
|
||||
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
|
||||
guard let self = self else { return nil }
|
||||
|
||||
|
||||
@ -47,6 +47,7 @@ class GamesViewController: UIViewController
|
||||
private let fetchedResultsController: NSFetchedResultsController<NSFetchRequestResult>
|
||||
|
||||
private var searchController: RSTSearchController?
|
||||
private lazy var importController: ImportController = self.makeImportController()
|
||||
|
||||
private var syncingToastView: RSTToastView? {
|
||||
didSet {
|
||||
@ -58,6 +59,8 @@ class GamesViewController: UIViewController
|
||||
}
|
||||
private var syncingProgressObservation: NSKeyValueObservation?
|
||||
|
||||
@IBOutlet private var importButton: UIBarButtonItem!
|
||||
|
||||
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
|
||||
fatalError("initWithNibName: not implemented")
|
||||
}
|
||||
@ -114,10 +117,16 @@ extension GamesViewController
|
||||
let navigationBarAppearance = navigationController.navigationBar.standardAppearance.copy()
|
||||
navigationBarAppearance.backgroundEffect = UIBlurEffect(style: .dark)
|
||||
navigationController.navigationBar.standardAppearance = navigationBarAppearance
|
||||
navigationController.navigationBar.scrollEdgeAppearance = navigationBarAppearance
|
||||
|
||||
let toolbarAppearance = navigationController.toolbar.standardAppearance.copy()
|
||||
toolbarAppearance.backgroundEffect = UIBlurEffect(style: .dark)
|
||||
navigationController.toolbar.standardAppearance = toolbarAppearance
|
||||
navigationController.toolbar.standardAppearance = toolbarAppearance
|
||||
|
||||
if #available(iOS 15, *)
|
||||
{
|
||||
navigationController.toolbar.scrollEdgeAppearance = toolbarAppearance
|
||||
}
|
||||
}
|
||||
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.updateTheme()
|
||||
@ -352,8 +377,8 @@ private extension GamesViewController
|
||||
/// Importing
|
||||
extension GamesViewController: ImportControllerDelegate
|
||||
{
|
||||
@IBAction private func importFiles()
|
||||
{
|
||||
private func makeImportController() -> ImportController
|
||||
{
|
||||
var documentTypes = Set(System.registeredSystems.map { $0.gameType.rawValue })
|
||||
documentTypes.insert(kUTTypeZipArchive as String)
|
||||
documentTypes.insert("com.rileytestut.delta.skin")
|
||||
@ -373,7 +398,13 @@ extension GamesViewController: ImportControllerDelegate
|
||||
let importController = ImportController(documentTypes: documentTypes)
|
||||
importController.delegate = self
|
||||
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])
|
||||
|
||||
@ -13,7 +13,7 @@ import DeltaCore
|
||||
struct iTunesImportOption: ImportOption
|
||||
{
|
||||
let title = NSLocalizedString("iTunes", comment: "")
|
||||
let image: UIImage? = nil
|
||||
let image: UIImage? = UIImage(symbolNameIfAvailable: "music.note")
|
||||
|
||||
private let presentingViewController: UIViewController
|
||||
|
||||
|
||||
@ -37,7 +37,10 @@ class ImportController: NSObject
|
||||
var delegate: ImportControllerDelegate?
|
||||
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.
|
||||
// Calling dismiss on presentingViewController in that case would dismiss presentingViewController, which is bad.
|
||||
@ -61,26 +64,54 @@ class ImportController: NSObject
|
||||
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)?)
|
||||
{
|
||||
self.presentingViewController = presentingViewController
|
||||
|
||||
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
alertController.addAction(UIAlertAction.cancel)
|
||||
let actions = self.makeActions()
|
||||
|
||||
if let importOptions = self.importOptions
|
||||
if actions.count > 1
|
||||
{
|
||||
for importOption in importOptions
|
||||
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
alertController.addAction(UIAlertAction.cancel)
|
||||
|
||||
let alertActions = actions.map { UIAlertAction($0) }
|
||||
for action in alertActions
|
||||
{
|
||||
alertController.add(importOption) { [unowned self] (urls) in
|
||||
self.finish(with: urls, errors: [])
|
||||
}
|
||||
alertController.addAction(action)
|
||||
}
|
||||
|
||||
let filesAction = UIAlertAction(title: NSLocalizedString("Files", comment: ""), style: .default) { (action) in
|
||||
self.presentDocumentBrowser()
|
||||
if let sourceView = self.sourceView
|
||||
{
|
||||
alertController.popoverPresentationController?.sourceView = sourceView.superview
|
||||
alertController.popoverPresentationController?.sourceRect = sourceView.frame
|
||||
}
|
||||
else
|
||||
{
|
||||
alertController.popoverPresentationController?.barButtonItem = self.barButtonItem
|
||||
}
|
||||
alertController.addAction(filesAction)
|
||||
|
||||
self.presentedViewController = alertController
|
||||
self.presentingViewController?.present(alertController, animated: true, completion: nil)
|
||||
@ -198,7 +229,7 @@ private var ImportControllerKey: UInt8 = 0
|
||||
|
||||
extension UIViewController
|
||||
{
|
||||
fileprivate(set) var importController: ImportController?
|
||||
fileprivate var importController: ImportController?
|
||||
{
|
||||
set
|
||||
{
|
||||
|
||||
@ -93,14 +93,6 @@ extension SaveStatesViewController
|
||||
self.collectionView?.dataSource = 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
|
||||
{
|
||||
case .saving:
|
||||
@ -113,8 +105,7 @@ extension SaveStatesViewController
|
||||
self.navigationItem.rightBarButtonItems?.removeFirst()
|
||||
}
|
||||
|
||||
// Manually update prototype cell properties
|
||||
self.prototypeCellWidthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionViewLayout.itemWidth)
|
||||
self.prototypeCellWidthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: 0)
|
||||
self.prototypeCellWidthConstraint.isActive = true
|
||||
|
||||
self.prepareEmulatorCoreSaveState()
|
||||
@ -238,6 +229,26 @@ private extension SaveStatesViewController
|
||||
}
|
||||
|
||||
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 -
|
||||
|
||||
@ -40,6 +40,8 @@ class ControllerInputsViewController: UIViewController
|
||||
|
||||
private var activeCalloutView: InputCalloutView?
|
||||
|
||||
private var _didLayoutSubviews = false
|
||||
|
||||
@IBOutlet private var actionsMenuViewControllerHeightConstraint: NSLayoutConstraint!
|
||||
@IBOutlet private var cancelTapGestureRecognizer: UITapGestureRecognizer!
|
||||
|
||||
@ -65,7 +67,15 @@ class ControllerInputsViewController: UIViewController
|
||||
|
||||
self.gameViewController.controllerView.addReceiver(self)
|
||||
|
||||
self.navigationController?.navigationBar.barStyle = .black
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
@ -403,6 +430,7 @@ private extension ControllerInputsViewController
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
alertController.popoverPresentationController?.barButtonItem = sender
|
||||
alertController.addAction(.cancel)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Reset Controls to Defaults", comment: ""), style: .destructive, handler: { (action) in
|
||||
reset()
|
||||
@ -562,3 +590,15 @@ extension ControllerInputsViewController: SMCalloutViewDelegate
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,9 +108,18 @@ extension ControllersSettingsViewController
|
||||
switch identifier
|
||||
{
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -296,6 +305,8 @@ extension ControllersSettingsViewController
|
||||
{
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
|
||||
{
|
||||
let previousGameController = self.gameController
|
||||
|
||||
switch Section(rawValue: indexPath.section)!
|
||||
{
|
||||
case .localDevice: self.gameController = self.localDeviceController
|
||||
@ -310,7 +321,7 @@ extension ControllersSettingsViewController
|
||||
|
||||
let previousIndexPath: IndexPath?
|
||||
|
||||
if let gameController = self.gameController
|
||||
if let gameController = previousGameController
|
||||
{
|
||||
if gameController == self.localDeviceController
|
||||
{
|
||||
|
||||
@ -215,6 +215,8 @@
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<true/>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleLightContent</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
@ -223,6 +225,13 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportsDocumentBrowser</key>
|
||||
<true/>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user