From 45c18cc8e247c7732e1b47051d76fe367c6802e6 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Wed, 15 Mar 2017 13:13:20 -0600 Subject: [PATCH] Adds ability to search games database to change game artwork Searching database is done via GamesDatabaseBrowserViewController --- Delta.xcodeproj/project.pbxproj | 22 ++- Delta/Base.lproj/Main.storyboard | 82 +++++++- Delta/Database/DatabaseManager.swift | 45 +++-- Delta/Database/GamesDatabase.swift | 48 ----- Delta/Database/OpenVGDB/GameMetadata.swift | 16 ++ Delta/Database/OpenVGDB/GamesDatabase.swift | 175 ++++++++++++++++++ .../GamesDatabaseBrowserViewController.swift | 143 ++++++++++++++ .../GameCollectionViewController.swift | 47 ++++- .../Game Selection/GamesViewController.swift | 4 - 9 files changed, 496 insertions(+), 86 deletions(-) delete mode 100644 Delta/Database/GamesDatabase.swift create mode 100644 Delta/Database/OpenVGDB/GameMetadata.swift create mode 100644 Delta/Database/OpenVGDB/GamesDatabase.swift create mode 100644 Delta/Database/OpenVGDB/GamesDatabaseBrowserViewController.swift diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index 72de1c4..d77061f 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -76,6 +76,8 @@ BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF797A2C1C2D339F00F1A000 /* UILabel+FontSize.swift */; }; BF7AE8081C2E858400B1B5BC /* PauseMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7AE8041C2E858400B1B5BC /* PauseMenuViewController.swift */; }; BF7AE80A1C2E8C7600B1B5BC /* UIColor+Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7AE8091C2E8C7600B1B5BC /* UIColor+Delta.swift */; }; + BF95E2771E4977BF0030E7AD /* GameMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF95E2761E4977BF0030E7AD /* GameMetadata.swift */; }; + BF95E2791E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF95E2781E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift */; }; BF99A5971DC2F9C400468E9E /* ControllerSkinTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF99A5961DC2F9C400468E9E /* ControllerSkinTableViewCell.swift */; }; BF99C6941D0A9AA600BA92BC /* SNESDeltaCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFC134E01AAD82460087AD7B /* SNESDeltaCore.framework */; }; BF99C6951D0A9AA600BA92BC /* SNESDeltaCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFC134E01AAD82460087AD7B /* SNESDeltaCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -156,7 +158,7 @@ BF5942681E09BBD00051894B /* GridCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GridCollectionViewCell.swift; path = "Components/Collection View/GridCollectionViewCell.swift"; sourceTree = ""; }; BF5942691E09BBD00051894B /* GridCollectionViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GridCollectionViewLayout.swift; path = "Components/Collection View/GridCollectionViewLayout.swift"; sourceTree = ""; }; BF59426D1E09BC5D0051894B /* DatabaseManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DatabaseManager.swift; path = Database/DatabaseManager.swift; sourceTree = ""; }; - BF59426E1E09BC5D0051894B /* GamesDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GamesDatabase.swift; path = Database/GamesDatabase.swift; sourceTree = ""; }; + BF59426E1E09BC5D0051894B /* GamesDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GamesDatabase.swift; path = Database/OpenVGDB/GamesDatabase.swift; sourceTree = ""; }; BF5942721E09BC700051894B /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; name = Model.xcdatamodel; path = Database/Model/Model.xcdatamodel; sourceTree = ""; }; BF5942771E09BC830051894B /* Cheat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Cheat.swift; path = Database/Model/Human/Cheat.swift; sourceTree = ""; }; BF5942781E09BC830051894B /* ControllerSkin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ControllerSkin.swift; path = Database/Model/Human/ControllerSkin.swift; sourceTree = ""; }; @@ -184,6 +186,8 @@ 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 = ""; }; BF7AE8091C2E8C7600B1B5BC /* UIColor+Delta.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Delta.swift"; sourceTree = ""; }; + BF95E2761E4977BF0030E7AD /* GameMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GameMetadata.swift; path = Database/OpenVGDB/GameMetadata.swift; sourceTree = ""; }; + BF95E2781E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GamesDatabaseBrowserViewController.swift; path = Database/OpenVGDB/GamesDatabaseBrowserViewController.swift; sourceTree = ""; }; BF99A5961DC2F9C400468E9E /* ControllerSkinTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ControllerSkinTableViewCell.swift; path = "Controller Skins/ControllerSkinTableViewCell.swift"; sourceTree = ""; }; BF9F4FCE1AAD7B87004C9500 /* DeltaCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = DeltaCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BFAA1FEC1B8AA4FA00495943 /* Settings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; @@ -323,8 +327,8 @@ isa = PBXGroup; children = ( BF59426D1E09BC5D0051894B /* DatabaseManager.swift */, - BF59426E1E09BC5D0051894B /* GamesDatabase.swift */, BF5942711E09BC690051894B /* Model */, + BF95E2751E49763D0030E7AD /* OpenVGDB */, ); name = Database; sourceTree = ""; @@ -413,6 +417,16 @@ name = Segues; sourceTree = ""; }; + BF95E2751E49763D0030E7AD /* OpenVGDB */ = { + isa = PBXGroup; + children = ( + BF59426E1E09BC5D0051894B /* GamesDatabase.swift */, + BF95E2761E4977BF0030E7AD /* GameMetadata.swift */, + BF95E2781E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift */, + ); + name = OpenVGDB; + sourceTree = ""; + }; BF9F4FCD1AAD7B25004C9500 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -730,6 +744,7 @@ BF5942701E09BC5D0051894B /* GamesDatabase.swift in Sources */, BF34FA071CF0F510006624C7 /* EditCheatViewController.swift in Sources */, BF5942881E09BC8B0051894B /* _Game.swift in Sources */, + BF95E2771E4977BF0030E7AD /* GameMetadata.swift in Sources */, BF11734D1DA32A5200047DF8 /* GameType+Localization.swift in Sources */, BFFC461F1D59823500AF2CC6 /* GamesStoryboardSegue.swift in Sources */, BF2B98E61C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift in Sources */, @@ -741,6 +756,7 @@ BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */, BF5942641E09BBB10051894B /* LoadControllerSkinImageOperation.swift in Sources */, BF5942871E09BC8B0051894B /* _ControllerSkin.swift in Sources */, + BF95E2791E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift in Sources */, BFD097211D3A01B8005A44C2 /* SaveStatesViewController.swift in Sources */, BF3540021C5DA3D500C1184C /* PauseStoryboardSegue.swift in Sources */, BFC314771E0C8CFC0056E3A8 /* GameType+Delta.swift in Sources */, @@ -855,6 +871,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 8.3; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; + OTHER_SWIFT_FLAGS = "-Xfrontend -debug-time-function-bodies"; PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.Delta; PRODUCT_NAME = Delta; SDKROOT = iphoneos; @@ -897,6 +914,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 8.3; MTL_ENABLE_DEBUG_INFO = NO; + OTHER_SWIFT_FLAGS = "-Xfrontend -debug-time-function-bodies"; PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.Delta; PRODUCT_NAME = Delta; SDKROOT = iphoneos; diff --git a/Delta/Base.lproj/Main.storyboard b/Delta/Base.lproj/Main.storyboard index 6ed1f2e..216262c 100644 --- a/Delta/Base.lproj/Main.storyboard +++ b/Delta/Base.lproj/Main.storyboard @@ -1,11 +1,11 @@ - + - + @@ -48,11 +48,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -84,13 +132,14 @@ - + + - + @@ -219,13 +268,13 @@ - + - + @@ -235,13 +284,32 @@ - + + + + + + + + + + + + + + + + + + + + diff --git a/Delta/Database/DatabaseManager.swift b/Delta/Database/DatabaseManager.swift index ccb77d2..2db6281 100644 --- a/Delta/Database/DatabaseManager.swift +++ b/Delta/Database/DatabaseManager.swift @@ -57,7 +57,7 @@ final class DatabaseManager: NSPersistentContainer { static let shared = DatabaseManager() - fileprivate let gamesDatabase: GamesDatabase? + fileprivate var gamesDatabase: GamesDatabase? = nil private init() { @@ -66,22 +66,6 @@ final class DatabaseManager: NSPersistentContainer let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL) else { fatalError("Core Data model cannot be found. Aborting.") } - do - { - if let gamesDatabaseURL = Bundle.main.url(forResource: "openvgdb", withExtension: "sqlite") - { - self.gamesDatabase = try GamesDatabase(fileURL: gamesDatabaseURL) - } - else - { - self.gamesDatabase = nil - } - } - catch - { - self.gamesDatabase = nil - print(error) - } super.init(name: "Delta", managedObjectModel: managedObjectModel) @@ -135,6 +119,21 @@ private extension DatabaseManager print("Failed to import standard controller skins:", error) } + do + { + if !FileManager.default.fileExists(atPath: DatabaseManager.gamesDatabaseURL.path) + { + guard let bundleURL = Bundle.main.url(forResource: "openvgdb", withExtension: "sqlite") else { throw GamesDatabase.Error.doesNotExist } + try FileManager.default.copyItem(at: bundleURL, to: DatabaseManager.gamesDatabaseURL) + } + + self.gamesDatabase = try GamesDatabase() + } + catch + { + print(error) + } + completion() } @@ -179,11 +178,13 @@ extension DatabaseManager let filename = identifier + "." + url.pathExtension let game = Game.insertIntoManagedObjectContext(context) - game.name = url.deletingPathExtension().lastPathComponent game.identifier = identifier game.filename = filename - game.artworkURL = self.gamesDatabase?.artworkURL(for: game) + let databaseMetadata = self.gamesDatabase?.metadata(for: game) + game.name = databaseMetadata?.name ?? url.deletingPathExtension().lastPathComponent + game.artworkURL = databaseMetadata?.artworkURL + let gameCollection = GameCollection.gameSystemCollectionForPathExtension(url.pathExtension, inManagedObjectContext: context) game.type = GameType(rawValue: gameCollection.identifier) game.gameCollections.insert(gameCollection) @@ -439,6 +440,12 @@ extension DatabaseManager return databaseDirectoryURL } + + class var gamesDatabaseURL: URL + { + let gamesDatabaseURL = self.defaultDirectoryURL().appendingPathComponent("openvgdb.sqlite") + return gamesDatabaseURL + } class var gamesDirectoryURL: URL { diff --git a/Delta/Database/GamesDatabase.swift b/Delta/Database/GamesDatabase.swift deleted file mode 100644 index 0006ff0..0000000 --- a/Delta/Database/GamesDatabase.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// GamesDatabase.swift -// Delta -// -// Created by Riley Testut on 11/16/16. -// Copyright © 2016 Riley Testut. All rights reserved. -// - -import Foundation -import SQLite - -class GamesDatabase -{ - private let connection: Connection - - init(fileURL: URL) throws - { - self.connection = try Connection(fileURL.path) - } - - func artworkURL(for game: Game) -> URL? - { - let roms = Table("ROMs") - let releases = Table("RELEASES") - - let hash = Expression("romHashSHA1") - let romID = Expression("romID") - let artworkAddress = Expression("releaseCoverFront") - - let gameHash = game.identifier.uppercased() - let query = roms.select(artworkAddress).filter(hash == gameHash).join(releases, on: roms[romID] == releases[romID]) - - do - { - if let row = try self.connection.pluck(query), let address = row[artworkAddress] - { - let url = URL(string: address) - return url - } - } - catch - { - print(error) - } - - return nil - } -} diff --git a/Delta/Database/OpenVGDB/GameMetadata.swift b/Delta/Database/OpenVGDB/GameMetadata.swift new file mode 100644 index 0000000..584dac4 --- /dev/null +++ b/Delta/Database/OpenVGDB/GameMetadata.swift @@ -0,0 +1,16 @@ +// +// GameMetadata.swift +// Delta +// +// Created by Riley Testut on 2/6/17. +// Copyright © 2017 Riley Testut. All rights reserved. +// + +import Foundation + +// Must be a class (not struct) so it can be used with Objective-C generics +class GameMetadata +{ + var name: String? + var artworkURL: URL? +} diff --git a/Delta/Database/OpenVGDB/GamesDatabase.swift b/Delta/Database/OpenVGDB/GamesDatabase.swift new file mode 100644 index 0000000..97d00bc --- /dev/null +++ b/Delta/Database/OpenVGDB/GamesDatabase.swift @@ -0,0 +1,175 @@ +// +// GamesDatabase.swift +// Delta +// +// Created by Riley Testut on 11/16/16. +// Copyright © 2016 Riley Testut. All rights reserved. +// + +import Foundation +import SQLite + +extension ExpressionType +{ + static var name: SQLite.Expression { + return SQLite.Expression("releaseTitleName") + } + + static var artworkAddress: SQLite.Expression { + return SQLite.Expression("releaseCoverFront") + } + + static var hash: SQLite.Expression { + return SQLite.Expression("romHashSHA1") + } + + static var romID: SQLite.Expression { + return SQLite.Expression("romID") + } +} + +extension Table +{ + static var roms: Table { + return Table("ROMs") + } + + static var releases: Table { + return Table("RELEASES") + } +} + +extension VirtualTable +{ + static var search: VirtualTable { + return VirtualTable("Search") + } +} + +extension GamesDatabase +{ + enum Error: Swift.Error + { + case doesNotExist + case connection(Swift.Error) + } +} + +class GamesDatabase +{ + fileprivate let connection: Connection + + init() throws + { + let fileURL = DatabaseManager.gamesDatabaseURL + + do + { + self.connection = try Connection(fileURL.path) + } + catch + { + throw Error.connection(error) + } + } + + func metadataResults(forGameName gameName: String) -> [GameMetadata] + { + let name = Expression.name + let artworkAddress = Expression.artworkAddress + + let query = VirtualTable.search.select(name, artworkAddress).filter(name.match(gameName + "*")) + + do + { + let rows = try self.connection.prepare(query) + + let results = rows.map { row -> GameMetadata in + let metadata = GameMetadata() + metadata.name = row[name] + + if let address = row[artworkAddress] + { + metadata.artworkURL = URL(string: address) + } + + return metadata + } + + return results + } + + catch SQLite.Result.error(_, let code, _) where code == 1 + { + // Table does not exist + + if self.prepareFTS() + { + return self.metadataResults(forGameName: gameName) + } + } + catch + { + print(error) + } + + return [] + } + + func metadata(for game: Game) -> GameMetadata? + { + let name = Expression.name + let artworkAddress = Expression.artworkAddress + let hash = Expression.hash + let romID = Expression.romID + + let gameHash = game.identifier.uppercased() + let query = Table.roms.select(name, artworkAddress).filter(hash == gameHash).join(Table.releases, on: Table.roms[romID] == Table.releases[romID]) + + do + { + if let row = try self.connection.pluck(query) + { + let metadata = GameMetadata() + metadata.name = row[name] + + if let address = row[artworkAddress] + { + metadata.artworkURL = URL(string: address) + } + + return metadata + } + } + catch + { + print(error) + } + + return nil + } +} + +private extension GamesDatabase +{ + func prepareFTS() -> Bool + { + let name = Expression.name + let artworkAddress = Expression.artworkAddress + + do + { + try self.connection.run(VirtualTable.search.create(.FTS4([name, artworkAddress], tokenize: .Unicode61()))) + + let update = VirtualTable.search.insert(Table.releases.select(name, artworkAddress)) + _ = try self.connection.run(update) + } + catch + { + print(error) + return false + } + + return true + } +} diff --git a/Delta/Database/OpenVGDB/GamesDatabaseBrowserViewController.swift b/Delta/Database/OpenVGDB/GamesDatabaseBrowserViewController.swift new file mode 100644 index 0000000..12b1d3e --- /dev/null +++ b/Delta/Database/OpenVGDB/GamesDatabaseBrowserViewController.swift @@ -0,0 +1,143 @@ +// +// GamesDatabaseBrowserViewController.swift +// Delta +// +// Created by Riley Testut on 2/6/17. +// Copyright © 2017 Riley Testut. All rights reserved. +// + +import UIKit +import Roxas + +class GamesDatabaseBrowserViewController: UITableViewController +{ + var selectionHandler: ((GameMetadata) -> Void)? + + fileprivate let database: GamesDatabase? + fileprivate let dataSource: RSTArrayTableViewDataSource + + fileprivate let operationQueue = RSTOperationQueue() + fileprivate let imageCache = NSCache() + + override init(style: UITableViewStyle) { + fatalError() + } + + required init?(coder aDecoder: NSCoder) + { + do + { + self.database = try GamesDatabase() + } + catch + { + self.database = nil + print(error) + } + + self.dataSource = RSTArrayTableViewDataSource(items: []) + + let titleText = NSLocalizedString("Games Database", comment: "") + let detailText = NSLocalizedString("To search the database, type the name of a game in the search bar.", comment: "") + + let placeholderView = RSTBackgroundView() + placeholderView.textLabel.text = titleText + placeholderView.detailTextLabel.text = detailText + self.dataSource.placeholderView = placeholderView + + super.init(coder: aDecoder) + + self.dataSource.cellConfigurationHandler = self.configure(cell:with:for:) + + if let database = self.database + { + self.dataSource.searchController.searchHandler = { [unowned database, unowned dataSource] (searchValue, previousSearchValue) in + + return RSTBlockOperation(executionBlock: { [unowned database, unowned dataSource] (operation) in + let results = database.metadataResults(forGameName: searchValue.text) + + guard !operation.isCancelled else { return } + + dataSource.items = results + + if searchValue.text == "" + { + rst_dispatch_sync_on_main_thread { + placeholderView.textLabel.text = titleText + placeholderView.detailTextLabel.text = detailText + } + } + else + { + rst_dispatch_sync_on_main_thread { + placeholderView.textLabel.text = NSLocalizedString("No Games Found", comment: "") + placeholderView.detailTextLabel.text = NSLocalizedString("Please make sure the name is correct, or try searching for another game.", comment: "") + } + } + }) + } + } + + self.definesPresentationContext = true + } + + override func viewDidLoad() + { + super.viewDidLoad() + + self.tableView.dataSource = self.dataSource + self.tableView.tableHeaderView = self.dataSource.searchController.searchBar + self.tableView.rowHeight = 64 + } + + override func didReceiveMemoryWarning() + { + super.didReceiveMemoryWarning() + } + + func configure(cell: UITableViewCell, with metadata: GameMetadata, for indexPath: IndexPath) + { + cell.textLabel?.text = metadata.name ?? NSLocalizedString("Unknown", comment: "") + cell.textLabel?.numberOfLines = 2 + + cell.imageView?.image = #imageLiteral(resourceName: "BoxArt") + cell.imageView?.contentMode = .scaleAspectFit + + if let artworkURL = metadata.artworkURL + { + let operation = LoadImageURLOperation(url: artworkURL) + operation.resultsCache = self.imageCache + operation.resultHandler = { (image, error) in + if let image = image + { + DispatchQueue.main.async { + cell.imageView?.image = image + cell.imageView?.superview?.layoutIfNeeded() + } + } + } + + self.operationQueue.addOperation(operation, forKey: indexPath as NSIndexPath) + } + } +} + +extension GamesDatabaseBrowserViewController +{ + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) + { + if self.dataSource.searchController.presentingViewController != nil + { + self.dataSource.searchController.dismiss(animated: true, completion: nil) + } + + let metadata = self.dataSource.item(at: indexPath) + self.selectionHandler?(metadata) + } + + override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) + { + let operation = self.operationQueue[indexPath as NSIndexPath] + operation?.cancel() + } +} diff --git a/Delta/Game Selection/GameCollectionViewController.swift b/Delta/Game Selection/GameCollectionViewController.swift index f057612..b48c7c6 100644 --- a/Delta/Game Selection/GameCollectionViewController.swift +++ b/Delta/Game Selection/GameCollectionViewController.swift @@ -117,7 +117,7 @@ extension GameCollectionViewController switch identifier { - case "showSaveStates": + case "saveStates": let game = sender as! Game let saveStatesViewController = (segue.destination as! UINavigationController).topViewController as! SaveStatesViewController @@ -126,6 +126,24 @@ extension GameCollectionViewController 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 @@ -166,7 +184,14 @@ extension GameCollectionViewController default: break } - + } + + @IBAction private func unwindFromSaveStatesViewController(with segue: UIStoryboardSegue) + { + } + + @IBAction private func unwindFromGamesDatabaseBrowser(with segue: UIStoryboardSegue) + { } } @@ -179,6 +204,7 @@ private extension GameCollectionViewController let fetchRequest = Game.rst_fetchRequest() as! NSFetchRequest fetchRequest.predicate = NSPredicate(format: "ANY %K == %@", #keyPath(Game.gameCollections), self.gameCollection) fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(Game.name), ascending: true)] + fetchRequest.returnsObjectsAsFaults = false self.dataSource = RSTFetchedResultsCollectionViewDataSource(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext) self.dataSource.cellConfigurationHandler = { [unowned self] (cell, item, indexPath) in @@ -205,7 +231,7 @@ private extension GameCollectionViewController 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 @@ -250,6 +276,10 @@ private extension GameCollectionViewController self.rename(game) }) + let changeArtworkAction = Action(title: NSLocalizedString("Change Artwork", comment: ""), style: .default) { [unowned self] action in + self.changeArtwork(for: game) + } + let shareAction = Action(title: NSLocalizedString("Share", comment: ""), style: .default, action: { [unowned self] action in self.share(game) }) @@ -264,8 +294,8 @@ private extension GameCollectionViewController switch game.type { - case GameType.unknown: return [cancelAction, renameAction, shareAction, deleteAction] - default: return [cancelAction, renameAction, shareAction, saveStatesAction, deleteAction] + case GameType.unknown: return [cancelAction, renameAction, changeArtworkAction, shareAction, deleteAction] + default: return [cancelAction, renameAction, changeArtworkAction, shareAction, saveStatesAction, deleteAction] } } @@ -289,7 +319,7 @@ private extension GameCollectionViewController func viewSaveStates(for game: Game) { - self.performSegue(withIdentifier: "showSaveStates", sender: game) + self.performSegue(withIdentifier: "saveStates", sender: game) } func rename(_ game: Game) @@ -330,6 +360,11 @@ private extension GameCollectionViewController self._renameAction = nil } + func changeArtwork(for game: Game) + { + self.performSegue(withIdentifier: "gamesDatabaseBrowser", sender: game) + } + func share(_ game: Game) { let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) diff --git a/Delta/Game Selection/GamesViewController.swift b/Delta/Game Selection/GamesViewController.swift index 125b505..30d3811 100644 --- a/Delta/Game Selection/GamesViewController.swift +++ b/Delta/Game Selection/GamesViewController.swift @@ -142,10 +142,6 @@ extension GamesViewController @IBAction private func unwindFromSettingsViewController(_ segue: UIStoryboardSegue) { } - - @IBAction private func unwindFromSaveStatesViewController(_ segue: UIStoryboardSegue) - { - } } // MARK: - UI -