Skip to content

Commit 1dbf488

Browse files
authored
Merge pull request #9 from gabrieloc/feature/drag-drop-roms
Accept ROM Document Type
2 parents 8dc064d + 79a5c5a commit 1dbf488

File tree

9 files changed

+182
-20
lines changed

9 files changed

+182
-20
lines changed

gambatte_watchOS/Game.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ public struct Game {
3939
public let path: String
4040

4141
public init?(dictionary: [String: Any]) {
42-
guard let name = dictionary["name"] as? String,
43-
let path = dictionary["path"] as? String
42+
guard let name = dictionary["name"] as? String, !name.isEmpty,
43+
let path = dictionary["path"] as? String, !path.isEmpty
4444
else {
4545
return nil
4646
}

gambatte_watchOS/GameLoader.swift

+25-7
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ public class GameLoader: NSObject {
7676

7777
var activationCompletion: ((Error?) -> Void)?
7878

79-
var gameResponse: ((String) -> Void)?
79+
fileprivate var gameResponse: ((String) -> Void)?
80+
81+
var gamesUpdated: (([Game]) -> Void)?
8082

8183
static let shared = GameLoader()
8284

@@ -110,13 +112,12 @@ public class GameLoader: NSObject {
110112

111113
func requestGames(_ success: @escaping (([Game]) -> Void), failure: @escaping ((Error) -> Void)) {
112114

113-
let replyHandler: (([String: Any]) -> Void) = { (response) in
114-
guard let gamesRaw = response["games"] as? [[String: Any]] else {
115+
let replyHandler: (([String: Any]) -> Void) = { [unowned self] (response) in
116+
if let games = self.createGames(from: response) {
117+
success(games)
118+
} else {
115119
failure(GameLoaderError(reason: "bad response"))
116-
return
117120
}
118-
let games = gamesRaw.flatMap { Game(dictionary: $0) }
119-
success(games)
120121
}
121122

122123
session.sendMessage(["requestGames": 1], replyHandler: replyHandler, errorHandler: failure)
@@ -146,7 +147,11 @@ public class GameLoader: NSObject {
146147
func cachedURL(for game: Game) -> URL? {
147148
do {
148149
let contents = try FileManager.default.contentsOfDirectory(at: cacheURL!, includingPropertiesForKeys: nil, options: [.skipsSubdirectoryDescendants, .skipsHiddenFiles])
149-
return contents.first(where: { $0.lastPathComponent.contains(game.name) })
150+
return contents.first(where: { url -> Bool in
151+
let matchingName = url.lastPathComponent.contains(game.name)
152+
let matchingExtension = (url.pathExtension == URL(string: game.path)!.pathExtension)// .isValidROMExtension
153+
return matchingName && matchingExtension
154+
})
150155
} catch {
151156
return nil
152157
}
@@ -155,10 +160,23 @@ public class GameLoader: NSObject {
155160
var cacheURL: URL? {
156161
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
157162
}
163+
164+
func createGames(from response: [String: Any]) -> [Game]? {
165+
guard let gamesRaw = response["games"] as? [[String: Any]] else {
166+
return nil
167+
}
168+
return gamesRaw.flatMap { Game(dictionary: $0) }
169+
}
158170
}
159171

160172
extension GameLoader: WCSessionDelegate {
161173

174+
public func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
175+
if let games = createGames(from: message), let handler = gamesUpdated {
176+
handler(games)
177+
}
178+
}
179+
162180
public func session(_ session: WCSession, didReceive file: WCSessionFile) {
163181
print("received \(file)")
164182

giovanni WatchKit Extension/GameplayController.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,9 @@ class GameplayController: WKInterfaceController {
204204
// }
205205
// lastSnapshot = snapshot
206206

207-
self.image.setImage(snapshot)
207+
DispatchQueue.main.async {
208+
self.image.setImage(snapshot)
209+
}
208210
tick = 0
209211
}
210212

giovanni WatchKit Extension/LibraryController.swift

+17-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class LibraryController: WKInterfaceController {
4242
var games: [Game]? {
4343
didSet {
4444
if let games = games {
45+
noGamesFound = games.count == 0
4546
populateTable(with: games)
4647
}
4748
}
@@ -51,24 +52,39 @@ class LibraryController: WKInterfaceController {
5152

5253
override func awake(withContext context: Any?) {
5354
super.awake(withContext: context)
55+
5456
reloadGames()
5557
}
58+
5659
override func willActivate() {
5760
super.willActivate()
5861

62+
GameLoader.shared.gamesUpdated = { [unowned self] games in
63+
self.games = games
64+
}
65+
66+
guard let cacheURL = GameLoader.shared.cacheURL else {
67+
return
68+
}
69+
print("WATCH ROM URL: , \(cacheURL.absoluteString)")
5970
// Auto-loads the last played game
6071
// if let game = UserDefaults.standard.lastPlayed {
6172
// presentGame(game)
6273
// }
6374
}
6475

76+
override func didDeactivate() {
77+
super.didDeactivate()
78+
79+
GameLoader.shared.gamesUpdated = nil
80+
}
81+
6582
func reloadGames() {
6683

6784
noGamesFound = false
6885
populateTable(with: [])
6986

7087
let success: (([Game]) -> Void) = { [unowned self] games in
71-
self.noGamesFound = games.count == 0
7288
self.games = games
7389
}
7490

giovanni.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@
164164
B2DF6DA01E614C69001A811B /* GameRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2DF6D9F1E614C69001A811B /* GameRow.swift */; };
165165
B2DF6DA21E619D16001A811B /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B2DC85111E5DC6CC00CC1028 /* Interface.storyboard */; };
166166
B2FC9BDE1E608AD5004D19C0 /* GameplayController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FC9BDD1E608AD5004D19C0 /* GameplayController.swift */; };
167+
B2FCD4571E813EE900718BCF /* FileManagerAdditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2FCD4561E813EE900718BCF /* FileManagerAdditions.swift */; };
167168
/* End PBXBuildFile section */
168169

169170
/* Begin PBXContainerItemProxy section */
@@ -409,6 +410,7 @@
409410
B2DF6DD21E61C3D3001A811B /* Game.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Game.swift; path = ../gambatte_watchOS/Game.swift; sourceTree = "<group>"; };
410411
B2DF6DD31E61C3D3001A811B /* GameLoader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GameLoader.swift; path = ../gambatte_watchOS/GameLoader.swift; sourceTree = "<group>"; };
411412
B2FC9BDD1E608AD5004D19C0 /* GameplayController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameplayController.swift; sourceTree = "<group>"; };
413+
B2FCD4561E813EE900718BCF /* FileManagerAdditions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileManagerAdditions.swift; sourceTree = "<group>"; };
412414
/* End PBXFileReference section */
413415

414416
/* Begin PBXFrameworksBuildPhase section */
@@ -726,6 +728,7 @@
726728
children = (
727729
B2DC84FD1E5DC6CB00CC1028 /* AppDelegate.swift */,
728730
B2DC84FF1E5DC6CB00CC1028 /* ViewController.swift */,
731+
B2FCD4561E813EE900718BCF /* FileManagerAdditions.swift */,
729732
B2DC85011E5DC6CB00CC1028 /* Main.storyboard */,
730733
B2DC85041E5DC6CB00CC1028 /* Assets.xcassets */,
731734
B2DC85061E5DC6CB00CC1028 /* LaunchScreen.storyboard */,
@@ -1086,6 +1089,7 @@
10861089
files = (
10871090
B2DC85001E5DC6CB00CC1028 /* ViewController.swift in Sources */,
10881091
B2DC84FE1E5DC6CB00CC1028 /* AppDelegate.swift in Sources */,
1092+
B2FCD4571E813EE900718BCF /* FileManagerAdditions.swift in Sources */,
10891093
);
10901094
runOnlyForDeploymentPostprocessing = 0;
10911095
};

giovanni_iOS/AppDelegate.swift

+24
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,30 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
4444
return true
4545
}
4646

47+
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
48+
49+
guard let rootController = UIApplication.shared.keyWindow?.rootViewController as? ViewController else {
50+
return false
51+
}
52+
53+
return FileManager.default.receiveFile(at: url, completion: { (name) -> Bool in
54+
let alert = UIAlertController(title: "Received \(name)",
55+
message: "",
56+
preferredStyle: .alert)
57+
alert.addAction(UIAlertAction(title: "Close", style: .default, handler: nil))
58+
rootController.present(alert, animated: true, completion: nil)
59+
rootController.sendGamesList()
60+
return true
61+
}) { (error) -> Bool in
62+
let alert = UIAlertController(title: "Error Receiving File",
63+
message: error.localizedDescription,
64+
preferredStyle: .alert)
65+
alert.addAction(UIAlertAction(title: "Close", style: .default, handler: nil))
66+
rootController.present(alert, animated: true, completion: nil)
67+
return false
68+
}
69+
}
70+
4771
func applicationWillResignActive(_ application: UIApplication) {
4872
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
4973
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
//
2+
// FileManagerAdditions.swift
3+
// GIOVANNI
4+
//
5+
// Copyright (c) <2017>, Gabriel O'Flaherty-Chan
6+
// All rights reserved.
7+
//
8+
// Redistribution and use in source and binary forms, with or without
9+
// modification, are permitted provided that the following conditions are met:
10+
// 1. Redistributions of source code must retain the above copyright
11+
// notice, this list of conditions and the following disclaimer.
12+
// 2. Redistributions in binary form must reproduce the above copyright
13+
// notice, this list of conditions and the following disclaimer in the
14+
// documentation and/or other materials provided with the distribution.
15+
// 3. All advertising materials mentioning features or use of this software
16+
// must display the following acknowledgement:
17+
// This product includes software developed by skysent.
18+
// 4. Neither the name of the skysent nor the
19+
// names of its contributors may be used to endorse or promote products
20+
// derived from this software without specific prior written permission.
21+
//
22+
// THIS SOFTWARE IS PROVIDED BY skysent ''AS IS'' AND ANY
23+
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
24+
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
25+
// DISCLAIMED. IN NO EVENT SHALL skysent BE LIABLE FOR ANY
26+
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
27+
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28+
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
29+
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30+
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
31+
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32+
//
33+
34+
import Foundation
35+
36+
extension String {
37+
var isValidROMExtension: Bool {
38+
return ["gb", "gbc", "zip"].contains(self)
39+
}
40+
}
41+
42+
extension FileManager {
43+
44+
enum FileError: LocalizedError {
45+
case invalidExtension
46+
47+
public var errorDescription: String? {
48+
switch self {
49+
case .invalidExtension:
50+
return "Not a valid ROM file"
51+
}
52+
}
53+
}
54+
55+
var documentsDirectory: URL? {
56+
let directory: FileManager.SearchPathDirectory = .documentDirectory
57+
return FileManager.default.urls(for: directory, in: .userDomainMask).first as URL?
58+
}
59+
60+
func receiveFile(at fileURL: URL, completion: ((String) -> Bool), failure: ((Error) -> Bool)) -> Bool {
61+
62+
guard fileURL.pathExtension.isValidROMExtension else {
63+
return failure(FileError.invalidExtension)
64+
}
65+
66+
do {
67+
let name = fileURL.lastPathComponent
68+
let destinationPath = documentsDirectory!.appendingPathComponent(name)
69+
try FileManager.default.moveItem(at: fileURL, to: destinationPath)
70+
return completion(name)
71+
} catch (let error) {
72+
return failure(error)
73+
}
74+
}
75+
}

giovanni_iOS/Info.plist

+14
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@
66
<string>en</string>
77
<key>CFBundleDisplayName</key>
88
<string>GIOVANNI</string>
9+
<key>CFBundleDocumentTypes</key>
10+
<array>
11+
<dict>
12+
<key>CFBundleTypeIconFiles</key>
13+
<array/>
14+
<key>CFBundleTypeName</key>
15+
<string>ROM</string>
16+
<key>LSItemContentTypes</key>
17+
<array>
18+
<string>public.data</string>
19+
<string>public.content</string>
20+
</array>
21+
</dict>
22+
</array>
923
<key>CFBundleExecutable</key>
1024
<string>$(EXECUTABLE_NAME)</string>
1125
<key>CFBundleIdentifier</key>

giovanni_iOS/ViewController.swift

+18-9
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ class ViewController: UIViewController {
4949

5050
prepareSession()
5151

52-
if let documentsDirectory = documentsDirectory {
52+
if let documentsDirectory = FileManager.default.documentsDirectory {
5353
print("ROM URL: \(documentsDirectory)")
5454
}
5555
}
@@ -58,31 +58,29 @@ class ViewController: UIViewController {
5858
return .lightContent
5959
}
6060

61-
var documentsDirectory: URL? {
62-
let directory: FileManager.SearchPathDirectory = .documentDirectory
63-
return FileManager.default.urls(for: directory, in: .userDomainMask).first as URL?
64-
}
61+
6562

6663
func loadGames() -> [[String: String]]? {
6764

68-
guard let documentsDirectory = documentsDirectory else {
65+
guard let documentsDirectory = FileManager.default.documentsDirectory else {
6966
return nil
7067
}
7168

7269
do {
73-
let URLs = try FileManager.default.contentsOfDirectory(at: documentsDirectory, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])
70+
let URLs = try FileManager.default.contentsOfDirectory(at: documentsDirectory, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants])
7471
return encodeFiles(with: URLs)
7572
} catch {
7673
return nil
7774
}
7875
}
7976

8077
func encodeFiles(with URLs: [URL]) -> [[String: String]] {
81-
return URLs.reduce([[String: String]]()) {
78+
return URLs.filter { $0.pathExtension.isValidROMExtension }.reduce([[String: String]]()) {
8279

8380
var games = $0.0
84-
81+
8582
let path = $0.1
83+
8684
let name = path
8785
.lastPathComponent
8886
.components(separatedBy: ".")
@@ -143,4 +141,15 @@ extension ViewController: WCSessionDelegate {
143141
}
144142
}
145143

144+
func sendGamesList() {
145+
guard let games = loadGames() else {
146+
return
147+
}
148+
149+
if WCSession.default().activationState != .activated {
150+
prepareSession()
151+
}
152+
153+
WCSession.default().sendMessage(["games": games], replyHandler: nil, errorHandler: nil)
154+
}
146155
}

0 commit comments

Comments
 (0)