diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index 73603d3..6eddb56 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -70,6 +70,12 @@ BF5E7F461B9A652600AE44F8 /* Settings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF5E7F451B9A652600AE44F8 /* Settings.storyboard */; }; BF6866171DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6866161DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift */; }; BF696B801D9B2B02009639E0 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF696B7F1D9B2B02009639E0 /* Theme.swift */; }; + BF6BF3131EB7E47F008E83CD /* ImportOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6BF3121EB7E47F008E83CD /* ImportOption.swift */; }; + BF6BF3181EB82111008E83CD /* iTunesImportOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6BF3171EB82111008E83CD /* iTunesImportOption.swift */; }; + BF6BF31A1EB82146008E83CD /* ClipboardImportOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6BF3191EB82146008E83CD /* ClipboardImportOption.swift */; }; + BF6BF31C1EB821A0008E83CD /* GamesDatabaseImportOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6BF31B1EB821A0008E83CD /* GamesDatabaseImportOption.swift */; }; + BF6BF3211EB82362008E83CD /* GamesDatabase.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF6BF31F1EB82362008E83CD /* GamesDatabase.storyboard */; }; + BF6BF3271EB87EB8008E83CD /* PhotoLibraryImportOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6BF3261EB87EB8008E83CD /* PhotoLibraryImportOption.swift */; }; BF70798C1B6B464B0019077C /* ZipZap.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF70798B1B6B464B0019077C /* ZipZap.framework */; }; BF70798D1B6B464B0019077C /* ZipZap.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF70798B1B6B464B0019077C /* ZipZap.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF797A2C1C2D339F00F1A000 /* UILabel+FontSize.swift */; }; @@ -174,7 +180,7 @@ BF5942841E09BC8B0051894B /* _GameCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = _GameCollection.swift; path = Database/Model/Machine/_GameCollection.swift; sourceTree = ""; }; BF5942851E09BC8B0051894B /* _SaveState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = _SaveState.swift; path = Database/Model/Machine/_SaveState.swift; sourceTree = ""; }; BF59428B1E09BC930051894B /* ControllerSkinConfigurations.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ControllerSkinConfigurations.h; path = Database/Model/Misc/ControllerSkinConfigurations.h; sourceTree = ""; }; - BF59428D1E09BCFB0051894B /* ImportController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ImportController.swift; path = Components/Importing/ImportController.swift; sourceTree = ""; }; + BF59428D1E09BCFB0051894B /* ImportController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ImportController.swift; path = Importing/ImportController.swift; sourceTree = ""; }; BF59428F1E09BD1A0051894B /* NSFetchedResultsController+Conveniences.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSFetchedResultsController+Conveniences.h"; sourceTree = ""; }; BF5942901E09BD1A0051894B /* NSFetchedResultsController+Conveniences.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSFetchedResultsController+Conveniences.m"; sourceTree = ""; }; BF5942911E09BD1A0051894B /* NSManagedObject+Conveniences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObject+Conveniences.swift"; sourceTree = ""; }; @@ -185,6 +191,12 @@ BF6866161DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ControllerSkin+Configuring.swift"; sourceTree = ""; }; BF696B7F1D9B2B02009639E0 /* Theme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Theme.swift; path = Theming/Theme.swift; sourceTree = ""; }; BF6BB2451BB73FE800CCF94A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + BF6BF3121EB7E47F008E83CD /* ImportOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ImportOption.swift; path = Importing/ImportOption.swift; sourceTree = ""; }; + BF6BF3171EB82111008E83CD /* iTunesImportOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = iTunesImportOption.swift; path = "Importing/Import Options/iTunesImportOption.swift"; sourceTree = ""; }; + BF6BF3191EB82146008E83CD /* ClipboardImportOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ClipboardImportOption.swift; path = "Importing/Import Options/ClipboardImportOption.swift"; sourceTree = ""; }; + BF6BF31B1EB821A0008E83CD /* GamesDatabaseImportOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GamesDatabaseImportOption.swift; path = "Importing/Import Options/GamesDatabaseImportOption.swift"; sourceTree = ""; }; + BF6BF3201EB82362008E83CD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/GamesDatabase.storyboard; sourceTree = ""; }; + BF6BF3261EB87EB8008E83CD /* PhotoLibraryImportOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PhotoLibraryImportOption.swift; path = "Importing/Import Options/PhotoLibraryImportOption.swift"; sourceTree = ""; }; BF70798B1B6B464B0019077C /* ZipZap.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = ZipZap.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BF797A2C1C2D339F00F1A000 /* UILabel+FontSize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UILabel+FontSize.swift"; sourceTree = ""; }; BF7AE8041C2E858400B1B5BC /* PauseMenuViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PauseMenuViewController.swift; path = "Pause Menu/PauseMenuViewController.swift"; sourceTree = ""; }; @@ -203,7 +215,7 @@ BFE4269D1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SaveStatesStoryboardSegue.swift; path = Segues/SaveStatesStoryboardSegue.swift; sourceTree = ""; }; BFEC732C1AAECC4A00650035 /* Roxas.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Roxas.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BFF0742B1E9DC17500ACDF4A /* GBCDeltaCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = GBCDeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - BFF93A9F1E0FB036005EC865 /* InputStreamOutputWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InputStreamOutputWriter.swift; path = Components/Importing/InputStreamOutputWriter.swift; sourceTree = ""; }; + BFF93A9F1E0FB036005EC865 /* InputStreamOutputWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = InputStreamOutputWriter.swift; path = Database/InputStreamOutputWriter.swift; sourceTree = ""; }; BFFA4C081E8A24D600D87934 /* GameMetadataTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GameMetadataTableViewCell.swift; path = Database/OpenVGDB/GameMetadataTableViewCell.swift; sourceTree = ""; }; BFFA71D71AAC406100EE9DD1 /* Delta.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Delta.app; sourceTree = BUILT_PRODUCTS_DIR; }; BFFA71DB1AAC406100EE9DD1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -304,7 +316,6 @@ isa = PBXGroup; children = ( BF5942581E09BB810051894B /* Action.swift */, - BF59428C1E09BCE50051894B /* Importing */, BF5942671E09BBB70051894B /* Collection View */, BF5942601E09BBA80051894B /* Loading */, ); @@ -333,6 +344,7 @@ isa = PBXGroup; children = ( BF59426D1E09BC5D0051894B /* DatabaseManager.swift */, + BFF93A9F1E0FB036005EC865 /* InputStreamOutputWriter.swift */, BF5942711E09BC690051894B /* Model */, BF95E2751E49763D0030E7AD /* OpenVGDB */, ); @@ -386,7 +398,8 @@ isa = PBXGroup; children = ( BF59428D1E09BCFB0051894B /* ImportController.swift */, - BFF93A9F1E0FB036005EC865 /* InputStreamOutputWriter.swift */, + BF6BF3121EB7E47F008E83CD /* ImportOption.swift */, + BF6BF3161EB820F4008E83CD /* Import Options */, ); name = Importing; sourceTree = ""; @@ -399,6 +412,17 @@ name = Theming; sourceTree = ""; }; + BF6BF3161EB820F4008E83CD /* Import Options */ = { + isa = PBXGroup; + children = ( + BF6BF3171EB82111008E83CD /* iTunesImportOption.swift */, + BF6BF3191EB82146008E83CD /* ClipboardImportOption.swift */, + BF6BF3261EB87EB8008E83CD /* PhotoLibraryImportOption.swift */, + BF6BF31B1EB821A0008E83CD /* GamesDatabaseImportOption.swift */, + ); + name = "Import Options"; + sourceTree = ""; + }; BF7AE7FA1C2E851F00B1B5BC /* Pause Menu */ = { isa = PBXGroup; children = ( @@ -434,6 +458,7 @@ BF95E2751E49763D0030E7AD /* OpenVGDB */ = { isa = PBXGroup; children = ( + BF6BF31F1EB82362008E83CD /* GamesDatabase.storyboard */, BF59426E1E09BC5D0051894B /* GamesDatabase.swift */, BF95E2761E4977BF0030E7AD /* GameMetadata.swift */, BF95E2781E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift */, @@ -522,6 +547,7 @@ BF7AE7FA1C2E851F00B1B5BC /* Pause Menu */, BFAA1FEB1B8AA4E800495943 /* Settings */, BF59426C1E09BC450051894B /* Database */, + BF59428C1E09BCE50051894B /* Importing */, BF930FFB1EB6D6EC00E8DBA0 /* Systems */, BF5942571E09BB5D0051894B /* Components */, BF696B7E1D9B2AE6009639E0 /* Theming */, @@ -657,6 +683,7 @@ buildActionMask = 2147483647; files = ( BFFA71E21AAC406100EE9DD1 /* Main.storyboard in Resources */, + BF6BF3211EB82362008E83CD /* GamesDatabase.storyboard in Resources */, BF3540001C5DA3C500C1184C /* PausePresentationControllerContentView.xib in Resources */, BF5E7F461B9A652600AE44F8 /* Settings.storyboard in Resources */, BF02D5DA1DDEBB3000A5E131 /* openvgdb.sqlite in Resources */, @@ -750,6 +777,7 @@ buildActionMask = 2147483647; files = ( BF59426A1E09BBD00051894B /* GridCollectionViewCell.swift in Sources */, + BF6BF3181EB82111008E83CD /* iTunesImportOption.swift in Sources */, BF59427C1E09BC830051894B /* Cheat.swift in Sources */, BFBAB2E31EB685A2004E0B0E /* DeltaCoreProtocol+Delta.swift in Sources */, BF3540081C5DAFAD00C1184C /* PauseTransitionCoordinator.swift in Sources */, @@ -761,6 +789,7 @@ BF59428E1E09BCFB0051894B /* ImportController.swift in Sources */, BF930FFD1EB6D6FF00E8DBA0 /* System.swift in Sources */, BF13A7581D5D2FD9000BB055 /* EmulatorCore+Cheats.swift in Sources */, + BF6BF3131EB7E47F008E83CD /* ImportOption.swift in Sources */, BF31878B1D489AAA00BD020D /* CheatValidator.swift in Sources */, BFFC46201D59823500AF2CC6 /* InitialGamesStoryboardSegue.swift in Sources */, BF5942731E09BC700051894B /* Model.xcdatamodel in Sources */, @@ -775,6 +804,7 @@ BF5942891E09BC8B0051894B /* _GameCollection.swift in Sources */, BF34FA111CF1899D006624C7 /* CheatTextView.swift in Sources */, BF1DAD5D1D9F576000E752A7 /* SystemControllerSkinsViewController.swift in Sources */, + BF6BF31C1EB821A0008E83CD /* GamesDatabaseImportOption.swift in Sources */, BFFA4C091E8A24D600D87934 /* GameMetadataTableViewCell.swift in Sources */, BFFC46231D5984A000AF2CC6 /* LaunchViewController.swift in Sources */, BF5942701E09BC5D0051894B /* GamesDatabase.swift in Sources */, @@ -806,11 +836,13 @@ BF5942861E09BC8B0051894B /* _Cheat.swift in Sources */, BF5E7F441B9A650B00AE44F8 /* SettingsViewController.swift in Sources */, BF5942931E09BD1A0051894B /* NSFetchedResultsController+Conveniences.m in Sources */, + BF6BF3271EB87EB8008E83CD /* PhotoLibraryImportOption.swift in Sources */, BF5942661E09BBB10051894B /* LoadImageURLOperation.swift in Sources */, BF353FF21C5D7FB000C1184C /* PauseViewController.swift in Sources */, BF59425C1E09BB810051894B /* Action.swift in Sources */, BF696B801D9B2B02009639E0 /* Theme.swift in Sources */, BF7AE8081C2E858400B1B5BC /* PauseMenuViewController.swift in Sources */, + BF6BF31A1EB82146008E83CD /* ClipboardImportOption.swift in Sources */, BFF93AA01E0FB036005EC865 /* InputStreamOutputWriter.swift in Sources */, BF59427F1E09BC830051894B /* GameCollection.swift in Sources */, ); @@ -827,6 +859,14 @@ name = PauseMenu.storyboard; sourceTree = ""; }; + BF6BF31F1EB82362008E83CD /* GamesDatabase.storyboard */ = { + isa = PBXVariantGroup; + children = ( + BF6BF3201EB82362008E83CD /* Base */, + ); + name = GamesDatabase.storyboard; + sourceTree = ""; + }; BFFA71E01AAC406100EE9DD1 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( diff --git a/Delta/Base.lproj/GamesDatabase.storyboard b/Delta/Base.lproj/GamesDatabase.storyboard new file mode 100644 index 0000000..1f5db5c --- /dev/null +++ b/Delta/Base.lproj/GamesDatabase.storyboard @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Delta/Base.lproj/Main.storyboard b/Delta/Base.lproj/Main.storyboard index 95ac3a9..c740d7e 100644 --- a/Delta/Base.lproj/Main.storyboard +++ b/Delta/Base.lproj/Main.storyboard @@ -1,12 +1,11 @@ - + - - + @@ -49,86 +48,11 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -161,7 +85,6 @@ - @@ -314,31 +237,11 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/Delta/Components/Importing/ImportController.swift b/Delta/Components/Importing/ImportController.swift deleted file mode 100644 index 0991ec5..0000000 --- a/Delta/Components/Importing/ImportController.swift +++ /dev/null @@ -1,184 +0,0 @@ -// -// ImportController.swift -// Delta -// -// Created by Riley Testut on 10/10/15. -// Copyright © 2015 Riley Testut. All rights reserved. -// - -import UIKit -import ObjectiveC - -import DeltaCore - -import MobileCoreServices - -protocol ImportControllerDelegate -{ - func importController(_ importController: ImportController, didImport games: Set, with errors: Set) - func importController(_ importController: ImportController, didImport controllerSkins: Set, with errors: Set) - - /** Optional **/ - func importControllerDidCancel(_ importController: ImportController) -} - -extension ImportControllerDelegate -{ - func importControllerDidCancel(_ importController: ImportController) - { - // Empty Implementation - } -} - -class ImportController: NSObject -{ - var delegate: ImportControllerDelegate? - - fileprivate weak var presentingViewController: UIViewController? - - fileprivate func presentImportController(from presentingViewController: UIViewController, animated: Bool, completion: ((Void) -> Void)?) - { - self.presentingViewController = presentingViewController - - var documentTypes = System.supportedSystems.map { $0.gameType.rawValue } - documentTypes.append(kUTTypeDeltaControllerSkin as String) - documentTypes.append(kUTTypeZipArchive as String) - - // Add GBA4iOS's exported UTIs in case user has GBA4iOS installed (which may override Delta's UTI declarations) - documentTypes.append("com.rileytestut.gba") - documentTypes.append("com.rileytestut.gbc") - documentTypes.append("com.rileytestut.gb") - - #if os(iOS) - let documentMenuController = UIDocumentMenuViewController(documentTypes: documentTypes, in: .import) - documentMenuController.delegate = self - documentMenuController.addOption(withTitle: NSLocalizedString("iTunes", comment: ""), image: nil, order: .first) { self.importFromiTunes(nil) } - self.presentingViewController?.present(documentMenuController, animated: true, completion: nil) - #else - self.importFromiTunes(completion) - #endif - } - - private func importFromiTunes(_ completion: ((Void) -> Void)?) - { - let alertController = UIAlertController(title: NSLocalizedString("Import from iTunes?", comment: ""), message: NSLocalizedString("Delta will import the games and controller skins copied over via iTunes.", comment: ""), preferredStyle: .alert) - - let importAction = UIAlertAction(title: NSLocalizedString("Import", comment: ""), style: .default) { action in - - let documentsDirectoryURL = DatabaseManager.defaultDirectoryURL().deletingLastPathComponent() - - do - { - let contents = try FileManager.default.contentsOfDirectory(at: documentsDirectoryURL, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) - - DatabaseManager.shared.performBackgroundTask { (context) in - let controllerSkinURLs = contents.filter { $0.pathExtension.lowercased() == "deltaskin" } - self.importControllerSkins(at: Set(controllerSkinURLs)) - - let gameURLs = contents.filter { GameType(fileExtension: $0.pathExtension) != nil || $0.pathExtension.lowercased() == "zip" } - self.importGames(at: Set(gameURLs)) - } - - } - catch let error as NSError - { - print(error) - } - - self.presentingViewController?.importController = nil - - } - alertController.addAction(importAction) - - let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { action in - self.delegate?.importControllerDidCancel(self) - self.presentingViewController?.importController = nil - } - alertController.addAction(cancelAction) - - self.presentingViewController?.present(alertController, animated: true, completion: completion) - } - - fileprivate func importGames(at urls: Set) - { - DatabaseManager.shared.importGames(at: urls) { (games, errors) in - self.delegate?.importController(self, didImport: games, with: errors) - } - } - - fileprivate func importControllerSkins(at urls: Set) - { - DatabaseManager.shared.importControllerSkins(at: urls) { (controllerSkins, errors) in - self.delegate?.importController(self, didImport: controllerSkins, with: errors) - } - } -} - -#if os(iOS) - - extension ImportController: UIDocumentMenuDelegate - { - func documentMenu(_ documentMenu: UIDocumentMenuViewController, didPickDocumentPicker documentPicker: UIDocumentPickerViewController) - { - documentPicker.delegate = self - self.presentingViewController?.present(documentPicker, animated: true, completion: nil) - } - - func documentMenuWasCancelled(_ documentMenu: UIDocumentMenuViewController) - { - self.delegate?.importControllerDidCancel(self) - - self.presentingViewController?.importController = nil - } - - } - - extension ImportController: UIDocumentPickerDelegate - { - func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) - { - if url.pathExtension.lowercased() == "deltaskin" - { - self.importControllerSkins(at: [url]) - } - else - { - self.importGames(at: [url]) - } - - self.presentingViewController?.importController = nil - } - - func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) - { - self.delegate?.importControllerDidCancel(self) - - self.presentingViewController?.importController = nil - } - } - -#endif - -private var ImportControllerKey: UInt8 = 0 - -extension UIViewController -{ - fileprivate(set) var importController: ImportController? - { - set - { - objc_setAssociatedObject(self, &ImportControllerKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - } - get - { - return objc_getAssociatedObject(self, &ImportControllerKey) as? ImportController - } - } - - func present(_ importController: ImportController, animated: Bool, completion: ((Void) -> Void)?) - { - self.importController = importController - - importController.presentImportController(from: self, animated: animated, completion: completion) - } -} diff --git a/Delta/Components/Loading/LoadImageURLOperation.swift b/Delta/Components/Loading/LoadImageURLOperation.swift index 090b9f6..97036a6 100644 --- a/Delta/Components/Loading/LoadImageURLOperation.swift +++ b/Delta/Components/Loading/LoadImageURLOperation.swift @@ -74,14 +74,12 @@ class LoadImageURLOperation: RSTLoadOperation private func loadLocalImage(completion: @escaping (UIImage?, Error?) -> Void) { - let options: NSDictionary = [kCGImageSourceShouldCache as NSString: true] - - guard let imageSource = CGImageSourceCreateWithURL(self.url as CFURL, options) else { + guard let imageSource = CGImageSourceCreateWithURL(self.url as CFURL, nil) else { completion(nil, .doesNotExist) return } - guard let quartzImage = CGImageSourceCreateImageAtIndex(imageSource, 0, options) else { + guard let quartzImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) else { completion(nil, .invalid) return } diff --git a/Delta/Database/DatabaseManager.swift b/Delta/Database/DatabaseManager.swift index 61a69e5..077c3c1 100644 --- a/Delta/Database/DatabaseManager.swift +++ b/Delta/Database/DatabaseManager.swift @@ -53,7 +53,6 @@ extension DatabaseManager case (.saveFailed, _): return false } } - } } @@ -494,6 +493,14 @@ extension DatabaseManager return gameTypeDirectoryURL } + + class func artworkURL(for game: Game) -> URL + { + let gameURL = game.fileURL + + let artworkURL = gameURL.deletingPathExtension().appendingPathExtension("jpg") + return artworkURL + } } //MARK: - Private - diff --git a/Delta/Components/Importing/InputStreamOutputWriter.swift b/Delta/Database/InputStreamOutputWriter.swift similarity index 100% rename from Delta/Components/Importing/InputStreamOutputWriter.swift rename to Delta/Database/InputStreamOutputWriter.swift diff --git a/Delta/Database/Model/Human/Game.swift b/Delta/Database/Model/Human/Game.swift index 7234bb6..ad8879b 100644 --- a/Delta/Database/Model/Human/Game.swift +++ b/Delta/Database/Model/Human/Game.swift @@ -22,6 +22,37 @@ public class Game: _Game, GameProtocol return fileURL } + + public override var artworkURL: URL? { + get { + self.willAccessValue(forKey: #keyPath(Game.artworkURL)) + var artworkURL = self.primitiveValue(forKey: #keyPath(Game.artworkURL)) as? URL + self.didAccessValue(forKey: #keyPath(Game.artworkURL)) + + if let unwrappedArtworkURL = artworkURL, unwrappedArtworkURL.isFileURL + { + // Recreate the stored URL relative to current sandbox location. + artworkURL = URL(fileURLWithPath: unwrappedArtworkURL.relativePath, relativeTo: DatabaseManager.gamesDirectoryURL) + } + + return artworkURL + } + set { + self.willChangeValue(forKey: #keyPath(Game.artworkURL)) + + var artworkURL = newValue + + if let newValue = newValue, newValue.isFileURL + { + // Store a relative URL, since the sandbox location changes. + artworkURL = URL(fileURLWithPath: newValue.lastPathComponent, relativeTo: DatabaseManager.gamesDirectoryURL) + } + + self.setPrimitiveValue(artworkURL, forKey: #keyPath(Game.artworkURL)) + + self.didChangeValue(forKey: #keyPath(Game.artworkURL)) + } + } } extension Game diff --git a/Delta/Game Selection/GameCollectionViewController.swift b/Delta/Game Selection/GameCollectionViewController.swift index ebffe8c..aa8e1a7 100644 --- a/Delta/Game Selection/GameCollectionViewController.swift +++ b/Delta/Game Selection/GameCollectionViewController.swift @@ -7,6 +7,7 @@ // import UIKit +import MobileCoreServices import DeltaCore @@ -47,10 +48,14 @@ class GameCollectionViewController: UICollectionViewController fileprivate let dataSource = RSTFetchedResultsCollectionViewDataSource(fetchedResultsController: NSFetchedResultsController()) fileprivate let prototypeCell = GridCollectionViewCell() + fileprivate let imageOperationQueue = RSTOperationQueue() + fileprivate let imageCache = NSCache() + fileprivate var _performing3DTouchTransition = false fileprivate weak var _destination3DTouchTransitionViewController: UIViewController? fileprivate var _renameAction: UIAlertAction? + fileprivate var _changingArtworkGame: Game? } //MARK: - UIViewController - @@ -122,25 +127,7 @@ extension GameCollectionViewController saveStatesViewController.game = game saveStatesViewController.mode = .loading saveStatesViewController.theme = self.theme - - case "gamesDatabaseBrowser": - let game = sender as! Game - - let gamesDatabaseBrowserViewController = (segue.destination as! UINavigationController).topViewController as! GamesDatabaseBrowserViewController - gamesDatabaseBrowserViewController.selectionHandler = { (metadata) in - - DatabaseManager.shared.performBackgroundTask({ (context) in - let temporaryGame = context.object(with: game.objectID) as! Game - temporaryGame.artworkURL = metadata.artworkURL - context.saveWithErrorLogging() - - DispatchQueue.main.async { - gamesDatabaseBrowserViewController.dismiss(animated: true, completion: nil) - } - }) - - } - + case "unwindFromGames": let destinationViewController = segue.destination as! GameViewController let cell = sender as! UICollectionViewCell @@ -222,28 +209,28 @@ private extension GameCollectionViewController cell.isImageViewVibrancyEnabled = true } + cell.imageView.image = #imageLiteral(resourceName: "BoxArt") + cell.maximumImageSize = CGSize(width: 90, height: 90) cell.textLabel.text = game.name cell.textLabel.textColor = UIColor.gray if let artworkURL = game.artworkURL, !ignoreImageOperations { - cell.imageView.sd_setImage(with: artworkURL, placeholderImage: #imageLiteral(resourceName: "BoxArt"), options: .continueInBackground) { (image, error, type, url) in + let imageOperation = LoadImageURLOperation(url: artworkURL) + imageOperation.resultsCache = self.imageCache + imageOperation.resultHandler = { (image, error) in - if let error = error + if let image = image { - print(error) - } - - if image != nil - { - cell.isImageViewVibrancyEnabled = false + DispatchQueue.main.async { + cell.imageView.image = image + cell.isImageViewVibrancyEnabled = false + } } } - } - else - { - cell.imageView.image = #imageLiteral(resourceName: "BoxArt") + + self.imageOperationQueue.addOperation(imageOperation, forKey: indexPath as NSCopying) } } @@ -356,7 +343,16 @@ private extension GameCollectionViewController func changeArtwork(for game: Game) { - self.performSegue(withIdentifier: "gamesDatabaseBrowser", sender: game) + self._changingArtworkGame = game + + let clipboardImportOption = ClipboardImportOption() + let photoLibraryImportOption = PhotoLibraryImportOption(presentingViewController: self) + let gamesDatabaseImportOption = GamesDatabaseImportOption(presentingViewController: self) + + let importController = ImportController(documentTypes: [kUTTypeImage as String]) + importController.delegate = self + importController.importOptions = [clipboardImportOption, photoLibraryImportOption, gamesDatabaseImportOption] + self.present(importController, animated: true, completion: nil) } func share(_ game: Game) @@ -493,6 +489,94 @@ extension GameCollectionViewController: SaveStatesViewControllerDelegate } } +//MARK: - ImportControllerDelegate - +/// ImportControllerDelegate +extension GameCollectionViewController: ImportControllerDelegate +{ + func importController(_ importController: ImportController, didImportItemsAt urls: Set) + { + guard let game = self._changingArtworkGame else { return } + + var imageURL: URL? + + if let url = urls.first + { + if url.isFileURL + { + do + { + let imageData = try Data(contentsOf: url) + + if let image = UIImage(data: imageData) + { + let resizedImage = image.resizing(toFit: CGSize(width: 300, height: 300)) + + if let resizedData = UIImageJPEGRepresentation(resizedImage, 0.85) + { + let destinationURL = DatabaseManager.artworkURL(for: game) + try resizedData.write(to: destinationURL, options: .atomic) + + imageURL = destinationURL + } + } + } + catch + { + print(error) + } + } + else + { + imageURL = url + } + } + + if let imageURL = imageURL + { + if let previousArtworkURL = game.artworkURL as NSURL? + { + // Remove previous artwork from cache. + self.imageCache.removeObject(forKey: previousArtworkURL) + } + + DatabaseManager.shared.performBackgroundTask { (context) in + let temporaryGame = context.object(with: game.objectID) as! Game + temporaryGame.artworkURL = imageURL + context.saveWithErrorLogging() + + DispatchQueue.main.async { + self.presentedViewController?.dismiss(animated: true, completion: nil) + } + } + } + else + { + func presentAlertController() + { + let alertController = UIAlertController(title: NSLocalizedString("Unable to Change Artwork", comment: ""), message: NSLocalizedString("The image might be corrupted or in an unsupported format.", comment: ""), preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: RSTSystemLocalizedString("OK"), style: .cancel, handler: nil)) + self.present(alertController, animated: true, completion: nil) + } + + if let presentedViewController = self.presentedViewController + { + presentedViewController.dismiss(animated: true) { + presentAlertController() + } + } + else + { + presentAlertController() + } + } + } + + func importControllerDidCancel(_ importController: ImportController) + { + self.presentedViewController?.dismiss(animated: true, completion: nil) + } +} + //MARK: - UICollectionViewDelegate - /// UICollectionViewDelegate extension GameCollectionViewController @@ -545,8 +629,8 @@ extension GameCollectionViewController override func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { - let cell = cell as! GridCollectionViewCell - cell.imageView.sd_cancelCurrentImageLoad() + let operation = self.imageOperationQueue[indexPath as NSCopying] + operation?.cancel() } } diff --git a/Delta/Game Selection/GamesViewController.swift b/Delta/Game Selection/GamesViewController.swift index ccdecb4..61b233f 100644 --- a/Delta/Game Selection/GamesViewController.swift +++ b/Delta/Game Selection/GamesViewController.swift @@ -8,6 +8,7 @@ import UIKit import CoreData +import MobileCoreServices import DeltaCore @@ -266,37 +267,50 @@ extension GamesViewController: ImportControllerDelegate { @IBAction fileprivate func importFiles() { - let importController = ImportController() + var documentTypes = Set(System.supportedSystems.map { $0.gameType.rawValue }) + documentTypes.insert(kUTTypeZipArchive as String) + + // Add GBA4iOS's exported UTIs in case user has GBA4iOS installed (which may override Delta's UTI declarations) + documentTypes.insert("com.rileytestut.gba") + documentTypes.insert("com.rileytestut.gbc") + documentTypes.insert("com.rileytestut.gb") + + let itunesImportOption = iTunesImportOption(presentingViewController: self) + + let importController = ImportController(documentTypes: documentTypes) importController.delegate = self + importController.importOptions = [itunesImportOption] self.present(importController, animated: true, completion: nil) } - //MARK: - ImportControllerDelegate - @nonobjc func importController(_ importController: ImportController, didImport games: Set, with errors: Set) + func importController(_ importController: ImportController, didImportItemsAt urls: Set) { - if errors.count > 0 - { - let alertController = UIAlertController.alertController(for: .games, with: errors) - self.present(alertController, animated: true, completion: nil) + let gameURLs = urls.filter { $0.pathExtension.lowercased() != "deltaskin" } + DatabaseManager.shared.importGames(at: Set(gameURLs)) { (games, errors) in + if errors.count > 0 + { + let alertController = UIAlertController.alertController(for: .games, with: errors) + self.present(alertController, animated: true, completion: nil) + } + + if games.count > 0 + { + print("Imported Games:", games.map { $0.name }) + } } - if games.count > 0 - { - print("Imported Games:", games.map { $0.name }) - } - } - - @nonobjc func importController(_ importController: ImportController, didImport controllerSkins: Set, with errors: Set) - { - if errors.count > 0 - { - let alertController = UIAlertController.alertController(for: .controllerSkins, with: errors) - self.present(alertController, animated: true, completion: nil) - } - - if controllerSkins.count > 0 - { - print("Imported Controller Skins:", controllerSkins.map { $0.name }) + let controllerSkinURLs = urls.filter { $0.pathExtension.lowercased() == "deltaskin" } + DatabaseManager.shared.importControllerSkins(at: Set(controllerSkinURLs)) { (controllerSkins, errors) in + if errors.count > 0 + { + let alertController = UIAlertController.alertController(for: .controllerSkins, with: errors) + self.present(alertController, animated: true, completion: nil) + } + + if controllerSkins.count > 0 + { + print("Imported Controller Skins:", controllerSkins.map { $0.name }) + } } } } diff --git a/Delta/Importing/Import Options/ClipboardImportOption.swift b/Delta/Importing/Import Options/ClipboardImportOption.swift new file mode 100644 index 0000000..be859b4 --- /dev/null +++ b/Delta/Importing/Import Options/ClipboardImportOption.swift @@ -0,0 +1,39 @@ +// +// ClipboardImportOption.swift +// Delta +// +// Created by Riley Testut on 5/1/17. +// Copyright © 2017 Riley Testut. All rights reserved. +// + +import UIKit +import MobileCoreServices + +import Roxas + +struct ClipboardImportOption: ImportOption +{ + let title = NSLocalizedString("Clipboard", comment: "") + let image: UIImage? = nil + + func `import`(withCompletionHandler completionHandler: @escaping (Set?) -> Void) + { + guard UIPasteboard.general.hasImages else { return completionHandler([]) } + + guard let data = UIPasteboard.general.data(forPasteboardType: kUTTypeImage as String) else { return completionHandler([]) } + + do + { + let temporaryURL = FileManager.uniqueTemporaryURL() + try data.write(to: temporaryURL, options: .atomic) + + completionHandler([temporaryURL]) + } + catch + { + print(error) + + completionHandler([]) + } + } +} diff --git a/Delta/Importing/Import Options/GamesDatabaseImportOption.swift b/Delta/Importing/Import Options/GamesDatabaseImportOption.swift new file mode 100644 index 0000000..b82a460 --- /dev/null +++ b/Delta/Importing/Import Options/GamesDatabaseImportOption.swift @@ -0,0 +1,42 @@ +// +// GamesDatabaseImportOption.swift +// Delta +// +// Created by Riley Testut on 5/1/17. +// Copyright © 2017 Riley Testut. All rights reserved. +// + +import UIKit + +struct GamesDatabaseImportOption: ImportOption +{ + let title = NSLocalizedString("Games Database", comment: "") + let image: UIImage? = nil + + private let presentingViewController: UIViewController + + init(presentingViewController: UIViewController) + { + self.presentingViewController = presentingViewController + } + + func `import`(withCompletionHandler completionHandler: @escaping (Set?) -> Void) + { + let storyboard = UIStoryboard(name: "GamesDatabase", bundle: nil) + let navigationController = (storyboard.instantiateInitialViewController() as! UINavigationController) + + let gamesDatabaseBrowserViewController = navigationController.topViewController as! GamesDatabaseBrowserViewController + gamesDatabaseBrowserViewController.selectionHandler = { (metadata) in + if let artworkURL = metadata.artworkURL + { + completionHandler([artworkURL]) + } + else + { + completionHandler(nil) + } + } + + self.presentingViewController.present(navigationController, animated: true, completion: nil) + } +} diff --git a/Delta/Importing/Import Options/PhotoLibraryImportOption.swift b/Delta/Importing/Import Options/PhotoLibraryImportOption.swift new file mode 100644 index 0000000..e202447 --- /dev/null +++ b/Delta/Importing/Import Options/PhotoLibraryImportOption.swift @@ -0,0 +1,60 @@ +// +// PhotoLibraryImportOption.swift +// Delta +// +// Created by Riley Testut on 5/2/17. +// Copyright © 2017 Riley Testut. All rights reserved. +// + +import UIKit +import MobileCoreServices + +class PhotoLibraryImportOption: NSObject, ImportOption +{ + let title = NSLocalizedString("Photo Library", comment: "") + let image: UIImage? = nil + + private let presentingViewController: UIViewController + fileprivate var completionHandler: ((Set?) -> Void)? + + init(presentingViewController: UIViewController) + { + self.presentingViewController = presentingViewController + + super.init() + } + + func `import`(withCompletionHandler completionHandler: @escaping (Set?) -> Void) + { + self.completionHandler = completionHandler + + let imagePickerController = UIImagePickerController() + imagePickerController.delegate = self + imagePickerController.sourceType = .photoLibrary + imagePickerController.mediaTypes = [kUTTypeImage as String] + self.presentingViewController.present(imagePickerController, animated: true, completion: nil) + } +} + +extension PhotoLibraryImportOption: UIImagePickerControllerDelegate, UINavigationControllerDelegate +{ + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) + { + guard let image = info[UIImagePickerControllerOriginalImage] as? UIImage, let data = UIImageJPEGRepresentation(image, 0.85) else { + self.completionHandler?([]) + return + } + + do + { + let temporaryURL = FileManager.uniqueTemporaryURL() + try data.write(to: temporaryURL, options: .atomic) + + self.completionHandler?([temporaryURL]) + } + catch + { + self.completionHandler?([]) + } + } +} diff --git a/Delta/Importing/Import Options/iTunesImportOption.swift b/Delta/Importing/Import Options/iTunesImportOption.swift new file mode 100644 index 0000000..8b6f979 --- /dev/null +++ b/Delta/Importing/Import Options/iTunesImportOption.swift @@ -0,0 +1,73 @@ +// +// iTunesImportOption.swift +// Delta +// +// Created by Riley Testut on 5/1/17. +// Copyright © 2017 Riley Testut. All rights reserved. +// + +import UIKit + +import DeltaCore + +struct iTunesImportOption: ImportOption +{ + let title = NSLocalizedString("iTunes", comment: "") + let image: UIImage? = nil + + private let presentingViewController: UIViewController + + init(presentingViewController: UIViewController) + { + self.presentingViewController = presentingViewController + } + + func `import`(withCompletionHandler completionHandler: @escaping (Set?) -> Void) + { + let alertController = UIAlertController(title: NSLocalizedString("Import from iTunes?", comment: ""), message: NSLocalizedString("Delta will import the games and controller skins copied over via iTunes.", comment: ""), preferredStyle: .alert) + + let importAction = UIAlertAction(title: NSLocalizedString("Import", comment: ""), style: .default) { action in + + var importedURLs = Set() + + let documentsDirectoryURL = DatabaseManager.defaultDirectoryURL().deletingLastPathComponent() + + do + { + let contents = try FileManager.default.contentsOfDirectory(at: documentsDirectoryURL, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) + + let itemURLs = contents.filter { GameType(fileExtension: $0.pathExtension) != nil || $0.pathExtension.lowercased() == "zip" || $0.pathExtension.lowercased() == "deltaskin" } + + for url in itemURLs + { + let destinationURL = FileManager.uniqueTemporaryURL().appendingPathExtension(url.pathExtension) + + do + { + try FileManager.default.moveItem(at: url, to: destinationURL) + importedURLs.insert(destinationURL) + } + catch + { + print("Error importing file at URL", url, error) + } + } + + } + catch + { + print(error) + } + + completionHandler(importedURLs) + } + alertController.addAction(importAction) + + let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { action in + completionHandler(nil) + } + alertController.addAction(cancelAction) + + self.presentingViewController.present(alertController, animated: true, completion: nil) + } +} diff --git a/Delta/Importing/ImportController.swift b/Delta/Importing/ImportController.swift new file mode 100644 index 0000000..065ae7d --- /dev/null +++ b/Delta/Importing/ImportController.swift @@ -0,0 +1,135 @@ +// +// ImportController.swift +// Delta +// +// Created by Riley Testut on 10/10/15. +// Copyright © 2015 Riley Testut. All rights reserved. +// + +import UIKit +import ObjectiveC + +import DeltaCore + +import MobileCoreServices + +protocol ImportControllerDelegate +{ + func importController(_ importController: ImportController, didImportItemsAt urls: Set) + + /** Optional **/ + func importControllerDidCancel(_ importController: ImportController) +} + +extension ImportControllerDelegate +{ + func importControllerDidCancel(_ importController: ImportController) + { + // Empty Implementation + } +} + +class ImportController: NSObject +{ + let documentTypes: Set + + var delegate: ImportControllerDelegate? + var importOptions: [ImportOption]? + + init(documentTypes: Set) + { + self.documentTypes = documentTypes + + super.init() + } + + fileprivate weak var presentingViewController: UIViewController? + + fileprivate func presentImportController(from presentingViewController: UIViewController, animated: Bool, completionHandler: ((Void) -> Void)?) + { + self.presentingViewController = presentingViewController + + let documentMenuController = UIDocumentMenuViewController(documentTypes: Array(self.documentTypes), in: .import) + documentMenuController.delegate = self + + if let reversedImportOptions = self.importOptions?.reversed() + { + for importOption in reversedImportOptions + { + documentMenuController.add(importOption, order: UIDocumentMenuOrder.first) { (urls) in + if let urls = urls + { + self.delegate?.importController(self, didImportItemsAt: urls) + } + else + { + self.delegate?.importControllerDidCancel(self) + } + + self.presentingViewController?.importController = nil + } + } + } + + self.presentingViewController?.present(documentMenuController, animated: true, completion: nil) + } +} + + +extension ImportController: UIDocumentMenuDelegate +{ + func documentMenu(_ documentMenu: UIDocumentMenuViewController, didPickDocumentPicker documentPicker: UIDocumentPickerViewController) + { + documentPicker.delegate = self + self.presentingViewController?.present(documentPicker, animated: true, completion: nil) + } + + func documentMenuWasCancelled(_ documentMenu: UIDocumentMenuViewController) + { + self.delegate?.importControllerDidCancel(self) + + self.presentingViewController?.importController = nil + } +} + +extension ImportController: UIDocumentPickerDelegate +{ + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) + { + self.delegate?.importController(self, didImportItemsAt: [url]) + + self.presentingViewController?.importController = nil + } + + func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) + { + self.delegate?.importControllerDidCancel(self) + + self.presentingViewController?.importController = nil + } +} + + +private var ImportControllerKey: UInt8 = 0 + +extension UIViewController +{ + fileprivate(set) var importController: ImportController? + { + set + { + objc_setAssociatedObject(self, &ImportControllerKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + get + { + return objc_getAssociatedObject(self, &ImportControllerKey) as? ImportController + } + } + + func present(_ importController: ImportController, animated: Bool, completion: ((Void) -> Void)?) + { + self.importController = importController + + importController.presentImportController(from: self, animated: animated, completionHandler: completion) + } +} diff --git a/Delta/Importing/ImportOption.swift b/Delta/Importing/ImportOption.swift new file mode 100644 index 0000000..dfa7b9c --- /dev/null +++ b/Delta/Importing/ImportOption.swift @@ -0,0 +1,29 @@ +// +// ImportOption.swift +// Delta +// +// Created by Riley Testut on 5/1/17. +// Copyright © 2017 Riley Testut. All rights reserved. +// + +import UIKit + +import DeltaCore + +extension UIDocumentMenuViewController +{ + func add(_ importOption: ImportOption, order: UIDocumentMenuOrder, completionHandler: @escaping (Set?) -> Void) + { + self.addOption(withTitle: importOption.title, image: importOption.image, order: order) { + importOption.import(withCompletionHandler: completionHandler) + } + } +} + +protocol ImportOption +{ + var title: String { get } + var image: UIImage? { get } + + func `import`(withCompletionHandler completionHandler: @escaping (Set?) -> Void) +} diff --git a/Delta/Supporting Files/Info.plist b/Delta/Supporting Files/Info.plist index a092f0b..6c68b74 100644 --- a/Delta/Supporting Files/Info.plist +++ b/Delta/Supporting Files/Info.plist @@ -126,6 +126,8 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + NSPhotoLibraryUsageDescription + Press "OK" to allow Delta to use images from your Photo Library as game artwork. UTExportedTypeDeclarations diff --git a/Delta/Systems/System.swift b/Delta/Systems/System.swift index 92690d5..e6824d0 100644 --- a/Delta/Systems/System.swift +++ b/Delta/Systems/System.swift @@ -16,7 +16,7 @@ extension GameType { init?(fileExtension: String) { - switch fileExtension + switch fileExtension.lowercased() { case "smc", "sfc", "fig": self = .snes case "gba": self = .gba