diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index 910f76b..4c3449a 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -36,6 +36,10 @@ BF15AF841F54B43B009B6AAB /* ActionInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF15AF831F54B43B009B6AAB /* ActionInput.swift */; }; BF18B61F1E2985F900F70067 /* UIAlertController+Importing.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF18B61E1E2985F900F70067 /* UIAlertController+Importing.swift */; }; BF1DAD5D1D9F576000E752A7 /* SystemControllerSkinsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1DAD5C1D9F576000E752A7 /* SystemControllerSkinsViewController.swift */; }; + BF1F45A421AF274D00EF9895 /* SyncResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1F45A321AF274D00EF9895 /* SyncResultViewController.swift */; }; + BF1F45AB21AF4B5800EF9895 /* SyncResultsViewController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF1F45AA21AF4B5800EF9895 /* SyncResultsViewController.storyboard */; }; + BF1F45AD21AF57BA00EF9895 /* HarmonyMetadataKey+Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1F45AC21AF57BA00EF9895 /* HarmonyMetadataKey+Keys.swift */; }; + BF1F45BF21AF676F00EF9895 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1F45BE21AF676F00EF9895 /* Box.swift */; }; BF27CC8E1BC9FEA200A20D89 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BF6BB2451BB73FE800CCF94A /* Assets.xcassets */; }; BF2B98E61C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF2B98E51C97E32F00F6D57D /* SaveStatesCollectionHeaderView.swift */; }; BF31878B1D489AAA00BD020D /* CheatValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF31878A1D489AAA00BD020D /* CheatValidator.swift */; }; @@ -180,6 +184,10 @@ BF15AF831F54B43B009B6AAB /* ActionInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionInput.swift; sourceTree = ""; }; BF18B61E1E2985F900F70067 /* UIAlertController+Importing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Importing.swift"; sourceTree = ""; }; BF1DAD5C1D9F576000E752A7 /* SystemControllerSkinsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemControllerSkinsViewController.swift; sourceTree = ""; }; + BF1F45A321AF274D00EF9895 /* SyncResultViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncResultViewController.swift; sourceTree = ""; }; + BF1F45AA21AF4B5800EF9895 /* SyncResultsViewController.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = SyncResultsViewController.storyboard; sourceTree = ""; }; + BF1F45AC21AF57BA00EF9895 /* HarmonyMetadataKey+Keys.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HarmonyMetadataKey+Keys.swift"; sourceTree = ""; }; + BF1F45BE21AF676F00EF9895 /* Box.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = ""; }; BF27CC861BC9E3C600A20D89 /* Delta.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Delta.entitlements; sourceTree = ""; }; BF27CC8A1BC9FE4D00A20D89 /* Pods.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Pods.framework; path = "Pods/../build/Debug-appletvos/Pods.framework"; sourceTree = ""; }; BF27CC941BCB7B7A00A20D89 /* GameController.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GameController.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS9.0.sdk/System/Library/Frameworks/GameController.framework; sourceTree = DEVELOPER_DIR; }; @@ -327,6 +335,7 @@ BFC6F7B71F435BC500221B96 /* Input+Display.swift */, BF6424841F5CBDC900D6AB44 /* UIView+ParentViewController.swift */, BFC3627F21ADE2BA00EF2BE6 /* UIAlertController+Error.swift */, + BF1F45AC21AF57BA00EF9895 /* HarmonyMetadataKey+Keys.swift */, ); path = Extensions; sourceTree = ""; @@ -404,6 +413,7 @@ isa = PBXGroup; children = ( BF4828871F90290F00028B97 /* Action.swift */, + BF1F45BE21AF676F00EF9895 /* Box.swift */, BFE0229C1F5B56840052D888 /* Popover Menu */, BF5942671E09BBB70051894B /* Collection View */, BF71CF881FE90471001F1613 /* Table View */, @@ -623,6 +633,8 @@ isa = PBXGroup; children = ( BFAB9F7C219A43380080EC7D /* SyncManager.swift */, + BF1F45A321AF274D00EF9895 /* SyncResultViewController.swift */, + BF1F45AA21AF4B5800EF9895 /* SyncResultsViewController.storyboard */, ); path = Syncing; sourceTree = ""; @@ -851,6 +863,7 @@ BF02D5DA1DDEBB3000A5E131 /* openvgdb.sqlite in Resources */, BF71CF8A1FE904B1001F1613 /* GameTableViewCell.xib in Resources */, BFFC46461D59861000AF2CC6 /* LaunchScreen.storyboard in Resources */, + BF1F45AB21AF4B5800EF9895 /* SyncResultsViewController.storyboard in Resources */, BF353FF61C5D837600C1184C /* PauseMenu.storyboard in Resources */, BF27CC8E1BC9FEA200A20D89 /* Assets.xcassets in Resources */, ); @@ -974,6 +987,7 @@ BF04E6FF1DB8625C000F35D3 /* ControllerSkinsViewController.swift in Sources */, BF5942891E09BC8B0051894B /* _GameCollection.swift in Sources */, BF34FA111CF1899D006624C7 /* CheatTextView.swift in Sources */, + BF1F45AD21AF57BA00EF9895 /* HarmonyMetadataKey+Keys.swift in Sources */, BF71CF871FE90006001F1613 /* AppIconShortcutsViewController.swift in Sources */, BF1DAD5D1D9F576000E752A7 /* SystemControllerSkinsViewController.swift in Sources */, BFE022A01F5B57FF0052D888 /* PopoverMenuButton.swift in Sources */, @@ -983,6 +997,7 @@ BF8DDD241F4F6C880088A21B /* InputCalloutView.swift in Sources */, BF5942701E09BC5D0051894B /* GamesDatabase.swift in Sources */, BF34FA071CF0F510006624C7 /* EditCheatViewController.swift in Sources */, + BF1F45A421AF274D00EF9895 /* SyncResultViewController.swift in Sources */, BF5942881E09BC8B0051894B /* _Game.swift in Sources */, BF95E2771E4977BF0030E7AD /* GameMetadata.swift in Sources */, BFFC461F1D59823500AF2CC6 /* GamesStoryboardSegue.swift in Sources */, @@ -991,6 +1006,7 @@ BF353FFF1C5DA3C500C1184C /* PausePresentationController.swift in Sources */, BFFC464C1D5998D600AF2CC6 /* CheatTableViewCell.swift in Sources */, BF5942941E09BD1A0051894B /* NSManagedObject+Conveniences.swift in Sources */, + BF1F45BF21AF676F00EF9895 /* Box.swift in Sources */, BF7AE80A1C2E8C7600B1B5BC /* UIColor+Delta.swift in Sources */, BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */, BF80E1D21F13117000847008 /* ControllerInputsViewController.swift in Sources */, diff --git a/Delta/Components/Box.swift b/Delta/Components/Box.swift new file mode 100644 index 0000000..7777f05 --- /dev/null +++ b/Delta/Components/Box.swift @@ -0,0 +1,19 @@ +// +// Box.swift +// Delta +// +// Created by Riley Testut on 11/28/18. +// Copyright © 2018 Riley Testut. All rights reserved. +// + +import Foundation + +class Box +{ + let value: T + + init(_ value: T) + { + self.value = value + } +} diff --git a/Delta/Database/Model/Human/Cheat.swift b/Delta/Database/Model/Human/Cheat.swift index 80bd858..edc63c7 100644 --- a/Delta/Database/Model/Human/Cheat.swift +++ b/Delta/Database/Model/Human/Cheat.swift @@ -43,5 +43,14 @@ extension Cheat: Syncable public var syncableRelationships: Set { return [\Cheat.game as AnyKeyPath] - } + } + + public var syncableMetadata: [HarmonyMetadataKey : String] { + guard let game = self.game else { return [:] } + return [.gameID: game.identifier, .gameName: game.name] + } + + public var syncableLocalizedName: String? { + return self.name + } } diff --git a/Delta/Database/Model/Human/ControllerSkin.swift b/Delta/Database/Model/Human/ControllerSkin.swift index f3f4a35..6bd7b4d 100644 --- a/Delta/Database/Model/Human/ControllerSkin.swift +++ b/Delta/Database/Model/Human/ControllerSkin.swift @@ -109,4 +109,8 @@ extension ControllerSkin: Syncable public var isSyncingEnabled: Bool { return !self.isStandard } + + public var syncableLocalizedName: String? { + return self.name + } } diff --git a/Delta/Database/Model/Human/Game.swift b/Delta/Database/Model/Human/Game.swift index 0498bd5..67ea655 100644 --- a/Delta/Database/Model/Human/Game.swift +++ b/Delta/Database/Model/Human/Game.swift @@ -133,4 +133,8 @@ extension Game: Syncable public var syncableRelationships: Set { return [\Game.gameCollection] } + + public var syncableLocalizedName: String? { + return self.name + } } diff --git a/Delta/Database/Model/Human/GameCollection.swift b/Delta/Database/Model/Human/GameCollection.swift index f57e24b..ea4c38e 100644 --- a/Delta/Database/Model/Human/GameCollection.swift +++ b/Delta/Database/Model/Human/GameCollection.swift @@ -39,4 +39,8 @@ extension GameCollection: Syncable public var syncableKeys: Set { return [\GameCollection.index as AnyKeyPath] } + + public var syncableLocalizedName: String? { + return self.name + } } diff --git a/Delta/Database/Model/Human/GameControllerInputMapping.swift b/Delta/Database/Model/Human/GameControllerInputMapping.swift index 03793ab..530204c 100644 --- a/Delta/Database/Model/Human/GameControllerInputMapping.swift +++ b/Delta/Database/Model/Human/GameControllerInputMapping.swift @@ -96,4 +96,8 @@ extension GameControllerInputMapping: Syncable \GameControllerInputMapping.gameType, \GameControllerInputMapping.playerIndex] } + + public var syncableLocalizedName: String? { + return self.name + } } diff --git a/Delta/Database/Model/Human/SaveState.swift b/Delta/Database/Model/Human/SaveState.swift index 1eaac9e..f676648 100644 --- a/Delta/Database/Model/Human/SaveState.swift +++ b/Delta/Database/Model/Human/SaveState.swift @@ -125,4 +125,13 @@ extension SaveState: Syncable public var isSyncingEnabled: Bool { return self.type != .auto && self.type != .quick } + + public var syncableMetadata: [HarmonyMetadataKey : String] { + guard let game = self.game else { return [:] } + return [.gameID: game.identifier, .gameName: game.name] + } + + public var syncableLocalizedName: String? { + return self.localizedName + } } diff --git a/Delta/Extensions/HarmonyMetadataKey+Keys.swift b/Delta/Extensions/HarmonyMetadataKey+Keys.swift new file mode 100644 index 0000000..aed531e --- /dev/null +++ b/Delta/Extensions/HarmonyMetadataKey+Keys.swift @@ -0,0 +1,15 @@ +// +// HarmonyMetadataKey+Keys.swift +// Harmony +// +// Created by Riley Testut on 11/5/18. +// Copyright © 2018 Riley Testut. All rights reserved. +// + +import Harmony + +extension HarmonyMetadataKey +{ + static let gameID = HarmonyMetadataKey("gameID") + static let gameName = HarmonyMetadataKey("gameName") +} diff --git a/Delta/Game Selection/GamesViewController.swift b/Delta/Game Selection/GamesViewController.swift index d12debf..e0d8796 100644 --- a/Delta/Game Selection/GamesViewController.swift +++ b/Delta/Game Selection/GamesViewController.swift @@ -404,19 +404,30 @@ private extension GamesViewController @objc func syncingDidFinish(_ notification: Notification) { DispatchQueue.main.async { - guard let result = notification.userInfo?[SyncCoordinator.syncResultKey] as? Result<[Result]> else { return } + guard let result = notification.userInfo?[SyncCoordinator.syncResultKey] as? SyncResult else { return } let toastView: RSTToastView switch result { case .success: toastView = RSTToastView(text: NSLocalizedString("Sync Complete", comment: ""), detailText: nil) - case .failure(let error): toastView = RSTToastView(error: error) + case .failure(let error as HarmonyError): toastView = RSTToastView(text: NSLocalizedString("Sync Failed", comment: ""), detailText: error.failureReason) + case .failure(let error): toastView = RSTToastView(text: NSLocalizedString("Sync Failed", comment: ""), detailText: error.localizedDescription) } + toastView.addTarget(self, action: #selector(GamesViewController.presentSyncResultsViewController), for: .touchUpInside) + toastView.show(in: self.view, duration: 2.0) } } + + @objc func presentSyncResultsViewController() + { + guard let result = SyncManager.shared.previousSyncResult else { return } + + let navigationController = SyncResultViewController.make(result: result) + self.present(navigationController, animated: true, completion: nil) + } } //MARK: - UIPageViewController - diff --git a/Delta/Syncing/SyncManager.swift b/Delta/Syncing/SyncManager.swift index f39fb86..f3795de 100644 --- a/Delta/Syncing/SyncManager.swift +++ b/Delta/Syncing/SyncManager.swift @@ -9,6 +9,39 @@ import Harmony import Harmony_Drive +extension SyncManager +{ + enum RecordType: String, Hashable + { + case game = "Game" + case gameCollection = "GameCollection" + case cheat = "Cheat" + case saveState = "SaveState" + case controllerSkin = "ControllerSkin" + case gameControllerInputMapping = "GameControllerInputMapping" + + var localizedName: String { + switch self + { + case .game: return NSLocalizedString("Game", comment: "") + case .gameCollection: return NSLocalizedString("Game Collection", comment: "") + case .cheat: return NSLocalizedString("Cheat", comment: "") + case .saveState: return NSLocalizedString("Save State", comment: "") + case .controllerSkin: return NSLocalizedString("Controller Skin", comment: "") + case .gameControllerInputMapping: return NSLocalizedString("Game Controller Input Mapping", comment: "") + } + } + } +} + +extension Syncable where Self: NSManagedObject +{ + var recordType: SyncManager.RecordType { + let recordType = SyncManager.RecordType(rawValue: self.syncableType)! + return recordType + } +} + final class SyncManager { static let shared = SyncManager() @@ -21,6 +54,8 @@ final class SyncManager return self.syncCoordinator.recordController } + private(set) var previousSyncResult: SyncResult? + private(set) var isAuthenticated = false let syncCoordinator = SyncCoordinator(service: DriveService.shared, persistentContainer: DatabaseManager.shared) @@ -28,6 +63,8 @@ final class SyncManager private init() { DriveService.shared.clientID = "457607414709-5puj6lcv779gpu3ql43e6k3smjj40dmu.apps.googleusercontent.com" + + NotificationCenter.default.addObserver(self, selector: #selector(SyncManager.syncingDidFinish(_:)), name: SyncCoordinator.didFinishSyncingNotification, object: nil) } } @@ -47,7 +84,7 @@ extension SyncManager self.isAuthenticated = true } - catch let error as AuthenticationError where error.code == .noSavedCredentials + catch let error as _AuthenticationError where error.code == .noSavedCredentials { // Ignore } @@ -89,3 +126,12 @@ extension SyncManager self.syncCoordinator.sync() } } + +private extension SyncManager +{ + @objc func syncingDidFinish(_ notification: Notification) + { + guard let result = notification.userInfo?[SyncCoordinator.syncResultKey] as? SyncResult else { return } + self.previousSyncResult = result + } +} diff --git a/Delta/Syncing/SyncResultViewController.swift b/Delta/Syncing/SyncResultViewController.swift new file mode 100644 index 0000000..01548e2 --- /dev/null +++ b/Delta/Syncing/SyncResultViewController.swift @@ -0,0 +1,329 @@ +// +// SyncResultViewController.swift +// Delta +// +// Created by Riley Testut on 11/28/18. +// Copyright © 2018 Riley Testut. All rights reserved. +// + +import UIKit + +import Roxas +import Harmony + +@objc(SyncResultTableViewCell) +private class SyncResultTableViewCell: UITableViewCell +{ + @IBOutlet var nameLabel: UILabel! + @IBOutlet var errorLabel: UILabel! +} + +extension SyncResultViewController +{ + private enum Group: Hashable + { + case game(RecordID) + case saveState(gameID: RecordID) + case cheat(gameID: RecordID) + case controllerSkin + case gameControllerInputMapping + case gameCollection + case other + + var sortIndex: Int { + switch self + { + case .game: return 0 + case .saveState: return 1 + case .cheat: return 2 + case .controllerSkin: return 3 + case .gameControllerInputMapping: return 4 + case .gameCollection: return 5 + case .other: return 6 + } + } + } +} + +class SyncResultViewController: UITableViewController +{ + private(set) var result: Result<[Record: Result]>! + + private lazy var dataSource = self.makeDataSource() + + private lazy var sortedErrors = self.processResults() + private lazy var gameNamesByRecordID = self.fetchGameNames() + + private init() + { + super.init(style: .grouped) + } + + required init?(coder aDecoder: NSCoder) + { + super.init(coder: aDecoder) + } + + override func viewDidLoad() + { + super.viewDidLoad() + + self.tableView.dataSource = self.dataSource + } +} + +extension SyncResultViewController +{ + class func make(result: Result<[Record: Result]>) -> UINavigationController + { + let storyboard = UIStoryboard(name: "SyncResultsViewController", bundle: nil) + + let navigationController = storyboard.instantiateInitialViewController() as! UINavigationController + + let syncResultViewController = navigationController.viewControllers[0] as! SyncResultViewController + syncResultViewController.result = result + + return navigationController + } +} + +private extension SyncResultViewController +{ + func makeDataSource() -> RSTCompositeTableViewDataSource> + { + let dataSources = self.sortedErrors.map { (_, errors) -> RSTArrayTableViewDataSource> in + let dataSource = RSTArrayTableViewDataSource>(items: errors.map(Box.init)) + dataSource.cellConfigurationHandler = { (cell, error, indexPath) in + let cell = cell as! SyncResultTableViewCell + + let title: String? + let errorMessage: String? + + switch error.value + { + case let error as RecordError: + guard let recordType = SyncManager.RecordType(rawValue: error.record.recordID.type) else { return } + + switch recordType + { + case .game: title = NSLocalizedString("Game", comment: "") + case .saveState, .cheat, .controllerSkin, .gameCollection, .gameControllerInputMapping: title = error.record.localizedName ?? recordType.localizedName + } + + switch error + { + case .filesFailed(_, let errors): + var messages = [String]() + + for error in errors + { + messages.append(error.localizedDescription) + } + + errorMessage = messages.joined(separator: "\n") + + default: errorMessage = error.failureReason + } + + case let error as HarmonyError: + title = error.failureDescription + errorMessage = error.failureReason + + case let error: + assertionFailure("Only HarmonyErrors should be thrown by syncing logic.") + + title = nil + errorMessage = error.localizedDescription + } + + cell.nameLabel.text = title + cell.errorLabel.text = errorMessage + } + + return dataSource + } + + let placeholderView = RSTPlaceholderView() + placeholderView.textLabel.text = NSLocalizedString("Sync Successful", comment: "") + placeholderView.detailTextLabel.text = NSLocalizedString("There were no errors during last sync.", comment: "") + + let compositeDataSource = RSTCompositeTableViewDataSource(dataSources: dataSources) + compositeDataSource.proxy = self + compositeDataSource.placeholderView = placeholderView + return compositeDataSource + } + + private func processResults() -> [(group: Group, errors: [Error])] + { + var errors = [Error]() + + do + { + try self.result.verify() + } + catch SyncError.partial(let recordResults) + { + for (_, result) in recordResults + { + guard case .failure(let error) = result else { continue } + errors.append(error) + } + } + catch SyncError.cancelled + { + // Do nothing + } + catch let error as SyncError + { + let error = error.underlyingError ?? error + errors.append(error) + } + catch + { + assertionFailure("Non-SyncError thrown by sync result.") + errors.append(error) + } + + var errorsByGroup = [Group: [Error]]() + + for error in errors + { + let group: Group + + switch error + { + case let error as RecordError: + guard let recordType = SyncManager.RecordType(rawValue: error.record.recordID.type) else { continue } + + switch recordType + { + case .game: group = .game(error.record.recordID) + case .gameCollection: group = .gameCollection + case .controllerSkin: group = .controllerSkin + case .gameControllerInputMapping: group = .gameControllerInputMapping + + case .saveState: + guard let gameID = error.record.metadata?[.gameID] else { continue } + + let recordID = RecordID(type: SyncManager.RecordType.game.rawValue, identifier: gameID) + group = .saveState(gameID: recordID) + + case .cheat: + guard let gameID = error.record.metadata?[.gameID] else { continue } + + let recordID = RecordID(type: SyncManager.RecordType.game.rawValue, identifier: gameID) + group = .cheat(gameID: recordID) + } + + default: group = .other + } + + errorsByGroup[group, default: []].append(error) + } + + let sortedErrors = errorsByGroup.sorted { (a, b) in + let groupA = a.key + let groupB = b.key + + // Sort initially by game, then sort by type. + // This way games and their associated records (such as save states) are visually grouped together. + switch (groupA, groupB) + { + // Game-related records, but different game identifiers, so sort by game identifiers (implicitly grouping related game records together). + // Using `fallthrough` for these cases seg faults the compiler (as of Swift 4.2.1), so we just duplicate the return expression. + case (.game(let a), .game(let b)) where a != b: return a.identifier < b.identifier + case (.game(let a), .saveState(let b)) where a != b: return a.identifier < b.identifier + case (.game(let a), .cheat(let b)) where a != b: return a.identifier < b.identifier + case (.saveState(let a), .game(let b)) where a != b: return a.identifier < b.identifier + case (.saveState(let a), .saveState(let b)) where a != b: return a.identifier < b.identifier + case (.saveState(let a), .cheat(let b)) where a != b: return a.identifier < b.identifier + case (.cheat(let a), .game(let b)) where a != b: return a.identifier < b.identifier + case (.cheat(let a), .saveState(let b)) where a != b: return a.identifier < b.identifier + case (.cheat(let a), .cheat(let b)) where a != b: return a.identifier < b.identifier + + // Otherwise, just return their relative ordering. + case (.game, _): fallthrough + case (.saveState, _): fallthrough + case (.cheat, _): fallthrough + case (.controllerSkin, _): fallthrough + case (.gameControllerInputMapping, _): fallthrough + case (.gameCollection, _): fallthrough + case (.other, _): return groupA.sortIndex < groupB.sortIndex + } + } + + return sortedErrors.map { (group: $0.key, errors: $0.value) } + } + + func fetchGameNames() -> [RecordID: String] + { + let fetchRequest = Game.fetchRequest() as NSFetchRequest + fetchRequest.propertiesToFetch = [#keyPath(Game.name), #keyPath(Game.identifier)] + + do + { + let games = try DatabaseManager.shared.viewContext.fetch(fetchRequest) + + let gameNames = Dictionary(uniqueKeysWithValues: games.map { (RecordID(type: SyncManager.RecordType.game.rawValue, identifier: $0.identifier), $0.name) }) + return gameNames + } + catch + { + print("Failed to fetch game names.", error) + + return [:] + } + } +} + +private extension SyncResultViewController +{ + @IBAction func dismiss() + { + self.presentingViewController?.dismiss(animated: true, completion: nil) + } +} + +extension SyncResultViewController +{ + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? + { + let section = self.sortedErrors[section] + + switch section.group + { + case .controllerSkin: return NSLocalizedString("Controller Skins", comment: "") + case .gameCollection: return NSLocalizedString("Game Collections", comment: "") + case .gameControllerInputMapping: return NSLocalizedString("Input Mappings", comment: "") + case .other: return NSLocalizedString("Misc.", comment: "") + + case .game: + guard let error = section.errors.first as? RecordError else { return nil } + return error.record.localizedName + + case .saveState(let gameID): + guard let error = section.errors.first as? RecordError else { return nil } + + if let gameName = self.gameNamesByRecordID[gameID] ?? error.record.metadata?[.gameName] + { + return gameName + " - " + NSLocalizedString("Save States", comment: "") + } + else + { + return NSLocalizedString("Save States", comment: "") + } + + case .cheat(let gameID): + guard let error = section.errors.first as? RecordError else { return nil } + + if let gameName = self.gameNamesByRecordID[gameID] ?? error.record.metadata?[.gameName] + { + return gameName + " - " + NSLocalizedString("Cheats", comment: "") + } + else + { + return NSLocalizedString("Cheats", comment: "") + } + } + } +} diff --git a/Delta/Syncing/SyncResultsViewController.storyboard b/Delta/Syncing/SyncResultsViewController.storyboard new file mode 100644 index 0000000..ff1a967 --- /dev/null +++ b/Delta/Syncing/SyncResultsViewController.storyboard @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/External/Harmony b/External/Harmony index c562f42..7f28cf5 160000 --- a/External/Harmony +++ b/External/Harmony @@ -1 +1 @@ -Subproject commit c562f4213f8e7f351873435a7add8d755d45f350 +Subproject commit 7f28cf57b0f68a26e342d392c94de18b29db9acd