Adds ability to search games database to change game artwork

Searching database is done via GamesDatabaseBrowserViewController
This commit is contained in:
Riley Testut 2017-03-15 13:13:20 -06:00
parent 54da484423
commit 45c18cc8e2
9 changed files with 496 additions and 86 deletions

View File

@ -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 = "<group>"; };
BF5942691E09BBD00051894B /* GridCollectionViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GridCollectionViewLayout.swift; path = "Components/Collection View/GridCollectionViewLayout.swift"; sourceTree = "<group>"; };
BF59426D1E09BC5D0051894B /* DatabaseManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DatabaseManager.swift; path = Database/DatabaseManager.swift; sourceTree = "<group>"; };
BF59426E1E09BC5D0051894B /* GamesDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GamesDatabase.swift; path = Database/GamesDatabase.swift; sourceTree = "<group>"; };
BF59426E1E09BC5D0051894B /* GamesDatabase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GamesDatabase.swift; path = Database/OpenVGDB/GamesDatabase.swift; sourceTree = "<group>"; };
BF5942721E09BC700051894B /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; name = Model.xcdatamodel; path = Database/Model/Model.xcdatamodel; sourceTree = "<group>"; };
BF5942771E09BC830051894B /* Cheat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Cheat.swift; path = Database/Model/Human/Cheat.swift; sourceTree = "<group>"; };
BF5942781E09BC830051894B /* ControllerSkin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ControllerSkin.swift; path = Database/Model/Human/ControllerSkin.swift; sourceTree = "<group>"; };
@ -184,6 +186,8 @@
BF797A2C1C2D339F00F1A000 /* UILabel+FontSize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UILabel+FontSize.swift"; sourceTree = "<group>"; };
BF7AE8041C2E858400B1B5BC /* PauseMenuViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PauseMenuViewController.swift; path = "Pause Menu/PauseMenuViewController.swift"; sourceTree = "<group>"; };
BF7AE8091C2E8C7600B1B5BC /* UIColor+Delta.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Delta.swift"; sourceTree = "<group>"; };
BF95E2761E4977BF0030E7AD /* GameMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GameMetadata.swift; path = Database/OpenVGDB/GameMetadata.swift; sourceTree = "<group>"; };
BF95E2781E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GamesDatabaseBrowserViewController.swift; path = Database/OpenVGDB/GamesDatabaseBrowserViewController.swift; sourceTree = "<group>"; };
BF99A5961DC2F9C400468E9E /* ControllerSkinTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ControllerSkinTableViewCell.swift; path = "Controller Skins/ControllerSkinTableViewCell.swift"; sourceTree = "<group>"; };
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 = "<group>"; };
@ -323,8 +327,8 @@
isa = PBXGroup;
children = (
BF59426D1E09BC5D0051894B /* DatabaseManager.swift */,
BF59426E1E09BC5D0051894B /* GamesDatabase.swift */,
BF5942711E09BC690051894B /* Model */,
BF95E2751E49763D0030E7AD /* OpenVGDB */,
);
name = Database;
sourceTree = "<group>";
@ -413,6 +417,16 @@
name = Segues;
sourceTree = "<group>";
};
BF95E2751E49763D0030E7AD /* OpenVGDB */ = {
isa = PBXGroup;
children = (
BF59426E1E09BC5D0051894B /* GamesDatabase.swift */,
BF95E2761E4977BF0030E7AD /* GameMetadata.swift */,
BF95E2781E4982A10030E7AD /* GamesDatabaseBrowserViewController.swift */,
);
name = OpenVGDB;
sourceTree = "<group>";
};
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;

View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11760" systemVersion="16B2657" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="SPq-Bk-fQl">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11762" systemVersion="16C68" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="SPq-Bk-fQl">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11755"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@ -48,11 +48,59 @@
</connections>
</barButtonItem>
</navigationItem>
<connections>
<segue destination="6bq-zy-UZU" kind="presentation" identifier="gamesDatabaseBrowser" id="7TT-mP-bjt"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="JYx-xE-nis" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1036" y="1002"/>
</scene>
<!--Games Database-->
<scene sceneID="S7I-gw-igt">
<objects>
<tableViewController id="SB6-jW-dhZ" customClass="GamesDatabaseBrowserViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" id="bJf-Sa-ZOX">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="aZq-iY-vhF" style="IBUITableViewCellStyleDefault" id="4cJ-4B-Kgt">
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="4cJ-4B-Kgt" id="7ze-s0-mpI">
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="aZq-iY-vhF">
<rect key="frame" x="15" y="0.0" width="345" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="SB6-jW-dhZ" id="2aq-ZA-84E"/>
<outlet property="delegate" destination="SB6-jW-dhZ" id="WgY-cp-m7K"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Games Database" id="rwF-kd-avR">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="BnB-5n-Rff">
<connections>
<segue destination="mUU-ug-yNs" kind="unwind" unwindAction="unwindFromGamesDatabaseBrowserWith:" id="zdg-Az-WwQ"/>
</connections>
</barButtonItem>
</navigationItem>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="f3a-hX-Qnu" userLabel="First Responder" sceneMemberID="firstResponder"/>
<exit id="mUU-ug-yNs" userLabel="Exit" sceneMemberID="exit"/>
</objects>
<point key="canvasLocation" x="2652" y="1002"/>
</scene>
<!--Game Collection View Controller-->
<scene sceneID="qNA-NP-TiF">
<objects>
@ -84,13 +132,14 @@
</collectionView>
<connections>
<segue destination="X2o-q6-XD5" kind="unwind" identifier="unwindFromGames" unwindAction="unwindFromGamesViewControllerWith:" id="k8C-Xn-maU"/>
<segue destination="MPk-bF-nkj" kind="presentation" identifier="showSaveStates" customClass="SaveStatesStoryboardSegue" customModule="Delta" customModuleProvider="target" id="1Xp-2J-0cq"/>
<segue destination="MPk-bF-nkj" kind="presentation" identifier="saveStates" customClass="SaveStatesStoryboardSegue" customModule="Delta" customModuleProvider="target" id="1Xp-2J-0cq"/>
<segue destination="6bq-zy-UZU" kind="presentation" identifier="gamesDatabaseBrowser" id="mzX-Bb-MaX"/>
</connections>
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="bW1-t8-idm" userLabel="First Responder" sceneMemberID="firstResponder"/>
<exit id="X2o-q6-XD5" userLabel="Exit" sceneMemberID="exit"/>
</objects>
<point key="canvasLocation" x="1764" y="1719"/>
<point key="canvasLocation" x="1855" y="1719"/>
</scene>
<!--Launch View Controller-->
<scene sceneID="p7y-IT-nlb">
@ -219,13 +268,13 @@
<nil name="viewControllers"/>
<connections>
<segue destination="Eae-Qk-9MI" kind="relationship" relationship="rootViewController" id="1Jh-Zf-ntp"/>
<segue destination="WQV-Du-4IA" kind="unwind" identifier="unwindFromSaveStates" customClass="SaveStatesStoryboardUnwindSegue" customModule="Delta" customModuleProvider="target" unwindAction="unwindFromSaveStatesViewController:" id="dwO-iv-XDr"/>
<segue destination="WQV-Du-4IA" kind="unwind" identifier="unwindFromSaveStates" customClass="SaveStatesStoryboardUnwindSegue" customModule="Delta" customModuleProvider="target" unwindAction="unwindFromSaveStatesViewControllerWith:" id="dwO-iv-XDr"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="htj-tq-2KP" userLabel="First Responder" sceneMemberID="firstResponder"/>
<exit id="WQV-Du-4IA" userLabel="Exit" sceneMemberID="exit"/>
</objects>
<point key="canvasLocation" x="2562" y="1718"/>
<point key="canvasLocation" x="2652" y="1718"/>
</scene>
<!--saveStatesViewController-->
<scene sceneID="f1R-Kb-FOU">
@ -235,13 +284,32 @@
</viewControllerPlaceholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="eln-PZ-00u" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="3319" y="1717"/>
<point key="canvasLocation" x="3409" y="1716"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="REv-V5-eEz">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="6bq-zy-UZU" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="uzY-vR-coL">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="SB6-jW-dhZ" kind="relationship" relationship="rootViewController" id="b0w-Fq-hrk"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Hr9-N6-XXA" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1854" y="1002"/>
</scene>
</scenes>
<resources>
<image name="Settings_Button" width="22" height="22"/>
</resources>
<inferredMetricsTieBreakers>
<segue reference="mzX-Bb-MaX"/>
<segue reference="Tey-6Z-UHp"/>
</inferredMetricsTieBreakers>
</document>

View File

@ -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
{

View File

@ -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<String>("romHashSHA1")
let romID = Expression<Int>("romID")
let artworkAddress = Expression<String?>("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
}
}

View File

@ -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?
}

View File

@ -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<String?> {
return SQLite.Expression<String?>("releaseTitleName")
}
static var artworkAddress: SQLite.Expression<String?> {
return SQLite.Expression<String?>("releaseCoverFront")
}
static var hash: SQLite.Expression<String> {
return SQLite.Expression<String>("romHashSHA1")
}
static var romID: SQLite.Expression<Int> {
return SQLite.Expression<Int>("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<Any>.name
let artworkAddress = Expression<Any>.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<Any>.name
let artworkAddress = Expression<Any>.artworkAddress
let hash = Expression<Any>.hash
let romID = Expression<Any>.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<Any>.name
let artworkAddress = Expression<Any>.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
}
}

View File

@ -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<GameMetadata>
fileprivate let operationQueue = RSTOperationQueue()
fileprivate let imageCache = NSCache<NSURL, UIImage>()
override init(style: UITableViewStyle) {
fatalError()
}
required init?(coder aDecoder: NSCoder)
{
do
{
self.database = try GamesDatabase()
}
catch
{
self.database = nil
print(error)
}
self.dataSource = RSTArrayTableViewDataSource<GameMetadata>(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()
}
}

View File

@ -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<Game>
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)

View File

@ -142,10 +142,6 @@ extension GamesViewController
@IBAction private func unwindFromSettingsViewController(_ segue: UIStoryboardSegue)
{
}
@IBAction private func unwindFromSaveStatesViewController(_ segue: UIStoryboardSegue)
{
}
}
// MARK: - UI -