Merge branch 'feature/dropbox' into develop
This commit is contained in:
commit
3e5ebc7c32
@ -1 +1 @@
|
||||
Subproject commit 2768bce21839a3ef7a7118d589a210ac2ca22cd6
|
||||
Subproject commit 7ed452b7dff25a68cd503610bee1f3e36216a871
|
||||
@ -33,6 +33,8 @@
|
||||
BF1173501DA32CF600047DF8 /* ControllersSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF11734F1DA32CF600047DF8 /* ControllersSettingsViewController.swift */; };
|
||||
BF13A7561D5D29B0000BB055 /* PreviewGameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF13A7551D5D29B0000BB055 /* PreviewGameViewController.swift */; };
|
||||
BF13A7581D5D2FD9000BB055 /* EmulatorCore+Cheats.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF13A7571D5D2FD9000BB055 /* EmulatorCore+Cheats.swift */; };
|
||||
BF144C642238511400C387E1 /* Harmony_Dropbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFDE2CC6222DF345008038E0 /* Harmony_Dropbox.framework */; };
|
||||
BF144C652238511400C387E1 /* Harmony_Dropbox.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFDE2CC6222DF345008038E0 /* Harmony_Dropbox.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
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 */; };
|
||||
@ -101,6 +103,7 @@
|
||||
BF6BF3271EB87EB8008E83CD /* PhotoLibraryImportOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6BF3261EB87EB8008E83CD /* PhotoLibraryImportOption.swift */; };
|
||||
BF6EE5E91F7C5F860051AD6C /* _GameControllerInputMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6EE5E81F7C5F860051AD6C /* _GameControllerInputMapping.swift */; };
|
||||
BF6EE5EB1F7C5F8F0051AD6C /* GameControllerInputMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6EE5EA1F7C5F8F0051AD6C /* GameControllerInputMapping.swift */; };
|
||||
BF713C0822499ED4004A1A2B /* PreviousHarmony.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = BF713C0622499ED3004A1A2B /* PreviousHarmony.xcdatamodeld */; };
|
||||
BF71CF871FE90006001F1613 /* AppIconShortcutsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF71CF861FE90006001F1613 /* AppIconShortcutsViewController.swift */; };
|
||||
BF71CF8A1FE904B1001F1613 /* GameTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF71CF891FE904B1001F1613 /* GameTableViewCell.xib */; };
|
||||
BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF797A2C1C2D339F00F1A000 /* UILabel+FontSize.swift */; };
|
||||
@ -132,6 +135,10 @@
|
||||
BFD097211D3A01B8005A44C2 /* SaveStatesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3540041C5DA70400C1184C /* SaveStatesViewController.swift */; };
|
||||
BFDB3418219E4B1700595A62 /* SyncStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB3417219E4B1700595A62 /* SyncStatusViewController.swift */; };
|
||||
BFDD04F11D5E2C27002D450E /* GameCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDD04F01D5E2C27002D450E /* GameCollectionViewController.swift */; };
|
||||
BFDE2CD1222DF36A008038E0 /* SwiftyDropbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFDE2CC8222DF345008038E0 /* SwiftyDropbox.framework */; };
|
||||
BFDE2CD2222DF36A008038E0 /* SwiftyDropbox.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFDE2CC8222DF345008038E0 /* SwiftyDropbox.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BFDE2CD3222DF36A008038E0 /* Alamofire.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFDE2CC7222DF345008038E0 /* Alamofire.framework */; };
|
||||
BFDE2CD4222DF36A008038E0 /* Alamofire.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFDE2CC7222DF345008038E0 /* Alamofire.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BFE022A01F5B57FF0052D888 /* PopoverMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE0229F1F5B577D0052D888 /* PopoverMenuButton.swift */; };
|
||||
BFE4269E1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE4269D1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift */; };
|
||||
BFE593CA21F3F8B7003412A6 /* GameSave.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE593C921F3F8B7003412A6 /* GameSave.swift */; };
|
||||
@ -145,6 +152,7 @@
|
||||
BFFA4C091E8A24D600D87934 /* GameTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFA4C081E8A24D600D87934 /* GameTableViewCell.swift */; };
|
||||
BFFA71DD1AAC406100EE9DD1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFA71DC1AAC406100EE9DD1 /* AppDelegate.swift */; };
|
||||
BFFA71E21AAC406100EE9DD1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFFA71E01AAC406100EE9DD1 /* Main.storyboard */; };
|
||||
BFFBD3D9224A0756002EFC79 /* URL+ExtendedAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFBD3D8224A0756002EFC79 /* URL+ExtendedAttributes.swift */; };
|
||||
BFFC461E1D59823500AF2CC6 /* GamesPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFC461B1D59823500AF2CC6 /* GamesPresentationController.swift */; };
|
||||
BFFC461F1D59823500AF2CC6 /* GamesStoryboardSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFC461C1D59823500AF2CC6 /* GamesStoryboardSegue.swift */; };
|
||||
BFFC46201D59823500AF2CC6 /* InitialGamesStoryboardSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFFC461D1D59823500AF2CC6 /* InitialGamesStoryboardSegue.swift */; };
|
||||
@ -161,11 +169,14 @@
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
BF9F4FD01AAD7B87004C9500 /* DeltaCore.framework in Embed Frameworks */,
|
||||
BFDE2CD2222DF36A008038E0 /* SwiftyDropbox.framework in Embed Frameworks */,
|
||||
BF48F75C219A1F8A00BC2FC1 /* Harmony_Drive.framework in Embed Frameworks */,
|
||||
BF48F756219A1EF000BC2FC1 /* Harmony.framework in Embed Frameworks */,
|
||||
BFF0742D1E9DC17500ACDF4A /* GBCDeltaCore.framework in Embed Frameworks */,
|
||||
BFEC732E1AAECC4A00650035 /* Roxas.framework in Embed Frameworks */,
|
||||
BF0418151D01E93400E85BCF /* GBADeltaCore.framework in Embed Frameworks */,
|
||||
BFDE2CD4222DF36A008038E0 /* Alamofire.framework in Embed Frameworks */,
|
||||
BF144C652238511400C387E1 /* Harmony_Dropbox.framework in Embed Frameworks */,
|
||||
BF98C9832204D9AB006B95AC /* NESDeltaCore.framework in Embed Frameworks */,
|
||||
BF99C6951D0A9AA600BA92BC /* SNESDeltaCore.framework in Embed Frameworks */,
|
||||
BF072011219A3A9D00F05DA4 /* ZIPFoundation.framework in Embed Frameworks */,
|
||||
@ -268,6 +279,7 @@
|
||||
BF6EE5E81F7C5F860051AD6C /* _GameControllerInputMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _GameControllerInputMapping.swift; sourceTree = "<group>"; };
|
||||
BF6EE5EA1F7C5F8F0051AD6C /* GameControllerInputMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameControllerInputMapping.swift; sourceTree = "<group>"; };
|
||||
BF70798B1B6B464B0019077C /* ZipZap.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = ZipZap.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF713C0722499ED3004A1A2B /* Harmony.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Harmony.xcdatamodel; sourceTree = "<group>"; };
|
||||
BF71CF861FE90006001F1613 /* AppIconShortcutsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconShortcutsViewController.swift; sourceTree = "<group>"; };
|
||||
BF71CF891FE904B1001F1613 /* GameTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = GameTableViewCell.xib; sourceTree = "<group>"; };
|
||||
BF797A2C1C2D339F00F1A000 /* UILabel+FontSize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UILabel+FontSize.swift"; sourceTree = "<group>"; };
|
||||
@ -293,6 +305,9 @@
|
||||
BFCEA67D1D56FF640061A534 /* UIViewControllerContextTransitioning+Conveniences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewControllerContextTransitioning+Conveniences.swift"; sourceTree = "<group>"; };
|
||||
BFDB3417219E4B1700595A62 /* SyncStatusViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncStatusViewController.swift; sourceTree = "<group>"; };
|
||||
BFDD04F01D5E2C27002D450E /* GameCollectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameCollectionViewController.swift; sourceTree = "<group>"; };
|
||||
BFDE2CC6222DF345008038E0 /* Harmony_Dropbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Harmony_Dropbox.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BFDE2CC7222DF345008038E0 /* Alamofire.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Alamofire.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BFDE2CC8222DF345008038E0 /* SwiftyDropbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = SwiftyDropbox.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BFE0229F1F5B577D0052D888 /* PopoverMenuButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopoverMenuButton.swift; sourceTree = "<group>"; };
|
||||
BFE4269D1D9C68E600DC913F /* SaveStatesStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SaveStatesStoryboardSegue.swift; sourceTree = "<group>"; };
|
||||
BFE593C921F3F8B7003412A6 /* GameSave.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GameSave.swift; sourceTree = "<group>"; };
|
||||
@ -305,6 +320,7 @@
|
||||
BFFA71DB1AAC406100EE9DD1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
BFFA71DC1AAC406100EE9DD1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
BFFA71E11AAC406100EE9DD1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
BFFBD3D8224A0756002EFC79 /* URL+ExtendedAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+ExtendedAttributes.swift"; sourceTree = "<group>"; };
|
||||
BFFC461B1D59823500AF2CC6 /* GamesPresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesPresentationController.swift; sourceTree = "<group>"; };
|
||||
BFFC461C1D59823500AF2CC6 /* GamesStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamesStoryboardSegue.swift; sourceTree = "<group>"; };
|
||||
BFFC461D1D59823500AF2CC6 /* InitialGamesStoryboardSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InitialGamesStoryboardSegue.swift; sourceTree = "<group>"; };
|
||||
@ -321,11 +337,14 @@
|
||||
files = (
|
||||
BF9F4FCF1AAD7B87004C9500 /* DeltaCore.framework in Frameworks */,
|
||||
BFEC732D1AAECC4A00650035 /* Roxas.framework in Frameworks */,
|
||||
BFDE2CD3222DF36A008038E0 /* Alamofire.framework in Frameworks */,
|
||||
BF48F75B219A1F8A00BC2FC1 /* Harmony_Drive.framework in Frameworks */,
|
||||
BF99C6941D0A9AA600BA92BC /* SNESDeltaCore.framework in Frameworks */,
|
||||
BF98C9822204D9AB006B95AC /* NESDeltaCore.framework in Frameworks */,
|
||||
BF0418141D01E93400E85BCF /* GBADeltaCore.framework in Frameworks */,
|
||||
BF072010219A3A9D00F05DA4 /* ZIPFoundation.framework in Frameworks */,
|
||||
BFDE2CD1222DF36A008038E0 /* SwiftyDropbox.framework in Frameworks */,
|
||||
BF144C642238511400C387E1 /* Harmony_Dropbox.framework in Frameworks */,
|
||||
BFF0742C1E9DC17500ACDF4A /* GBCDeltaCore.framework in Frameworks */,
|
||||
BF48F755219A1EF000BC2FC1 /* Harmony.framework in Frameworks */,
|
||||
4FE8465FD28810191C3E5212 /* Pods_Delta.framework in Frameworks */,
|
||||
@ -353,6 +372,7 @@
|
||||
BF6424841F5CBDC900D6AB44 /* UIView+ParentViewController.swift */,
|
||||
BFC3627F21ADE2BA00EF2BE6 /* UIAlertController+Error.swift */,
|
||||
BF1F45AC21AF57BA00EF9895 /* HarmonyMetadataKey+Keys.swift */,
|
||||
BFFBD3D8224A0756002EFC79 /* URL+ExtendedAttributes.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
@ -470,6 +490,7 @@
|
||||
BF5942711E09BC690051894B /* Model */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF713C0622499ED3004A1A2B /* PreviousHarmony.xcdatamodeld */,
|
||||
BF4828811F9027B600028B97 /* Delta.xcdatamodeld */,
|
||||
BF5942741E09BC740051894B /* Human */,
|
||||
BF5942751E09BC780051894B /* Machine */,
|
||||
@ -617,6 +638,9 @@
|
||||
BF9F4FCD1AAD7B25004C9500 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BFDE2CC6222DF345008038E0 /* Harmony_Dropbox.framework */,
|
||||
BFDE2CC8222DF345008038E0 /* SwiftyDropbox.framework */,
|
||||
BFDE2CC7222DF345008038E0 /* Alamofire.framework */,
|
||||
BF98C9812204D9A1006B95AC /* NESDeltaCore.framework */,
|
||||
BF07200E219A3A9500F05DA4 /* ZIPFoundation.framework */,
|
||||
BF48F754219A1EEB00BC2FC1 /* Harmony.framework */,
|
||||
@ -859,6 +883,7 @@
|
||||
developmentRegion = English;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
English,
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
@ -1051,6 +1076,8 @@
|
||||
BF4828861F9028F500028B97 /* System.swift in Sources */,
|
||||
BF13A7561D5D29B0000BB055 /* PreviewGameViewController.swift in Sources */,
|
||||
BF6866171DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift in Sources */,
|
||||
BFFBD3D9224A0756002EFC79 /* URL+ExtendedAttributes.swift in Sources */,
|
||||
BF713C0822499ED4004A1A2B /* PreviousHarmony.xcdatamodeld in Sources */,
|
||||
BF59427D1E09BC830051894B /* ControllerSkin.swift in Sources */,
|
||||
BFAB9F7D219A43380080EC7D /* SyncManager.swift in Sources */,
|
||||
BFCEA67E1D56FF640061A534 /* UIViewControllerContextTransitioning+Conveniences.swift in Sources */,
|
||||
@ -1332,6 +1359,16 @@
|
||||
sourceTree = "<group>";
|
||||
versionGroupType = wrapper.xcdatamodel;
|
||||
};
|
||||
BF713C0622499ED3004A1A2B /* PreviousHarmony.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
BF713C0722499ED3004A1A2B /* Harmony.xcdatamodel */,
|
||||
);
|
||||
currentVersion = BF713C0722499ED3004A1A2B /* Harmony.xcdatamodel */;
|
||||
path = PreviousHarmony.xcdatamodeld;
|
||||
sourceTree = "<group>";
|
||||
versionGroupType = wrapper.xcdatamodel;
|
||||
};
|
||||
/* End XCVersionGroup section */
|
||||
};
|
||||
rootObject = BFFA71CF1AAC406100EE9DD1 /* Project object */;
|
||||
|
||||
@ -192,6 +192,10 @@
|
||||
argument = "-com.apple.CoreData.MigrationDebug 1"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-com.rileytestut.Harmony.Debug 1"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
||||
isEnabled = "YES">
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
import UIKit
|
||||
|
||||
import DeltaCore
|
||||
import Harmony_Dropbox
|
||||
|
||||
import Fabric
|
||||
import Crashlytics
|
||||
@ -112,6 +113,10 @@ extension AppDelegate
|
||||
return self.importControllerSkin(at: url)
|
||||
}
|
||||
}
|
||||
else if DropboxService.shared.handleDropboxURL(url)
|
||||
{
|
||||
return true
|
||||
}
|
||||
else
|
||||
{
|
||||
return self.deepLinkController.handle(.url(url))
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="ssH-mM-uG6">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="ssH-mM-uG6">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
@ -18,14 +18,14 @@
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="0.93725490199999995" green="0.93725490199999995" blue="0.95686274510000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<label key="tableFooterView" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Delta 0.6.0" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Str-BY-agW">
|
||||
<rect key="frame" x="0.0" y="817" width="375" height="44"/>
|
||||
<rect key="frame" x="0.0" y="861" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<sections>
|
||||
<tableViewSection headerTitle="Inputs" id="c6K-sJ-0vW">
|
||||
<tableViewSection headerTitle="Controllers" id="c6K-sJ-0vW">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="DetailCell" textLabel="tls-Hv-Rx2" detailTextLabel="vJP-Ie-a9H" style="IBUITableViewCellStyleValue1" id="jvV-ZB-Rq1">
|
||||
<rect key="frame" x="0.0" y="55.5" width="375" height="44"/>
|
||||
@ -238,17 +238,17 @@
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Syncing" id="y6U-7a-bnX" userLabel="Syncing">
|
||||
<tableViewSection headerTitle="Delta Sync" id="y6U-7a-bnX" userLabel="Syncing">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="DetailCell" textLabel="4U1-fe-PIb" detailTextLabel="kLY-5g-v8n" style="IBUITableViewCellStyleValue1" id="bwW-PG-BcV">
|
||||
<rect key="frame" x="0.0" y="611" width="375" height="44"/>
|
||||
<rect key="frame" x="0.0" y="655" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="bwW-PG-BcV" id="RNA-99-evH">
|
||||
<rect key="frame" x="0.0" y="0.0" width="341" height="43.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="349" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Service" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="4U1-fe-PIb">
|
||||
<rect key="frame" x="16" y="12" width="54" height="19.5"/>
|
||||
<rect key="frame" x="15" y="12" width="54" height="19.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
@ -268,14 +268,14 @@
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="BadgeCell" textLabel="1u0-gh-zP7" style="IBUITableViewCellStyleDefault" id="JPg-6O-DRe" customClass="BadgedTableViewCell" customModule="Delta" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="655" width="375" height="44"/>
|
||||
<rect key="frame" x="0.0" y="699" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="JPg-6O-DRe" id="zcZ-QR-Nno">
|
||||
<rect key="frame" x="0.0" y="0.0" width="341" height="43.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="349" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Status" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="1u0-gh-zP7">
|
||||
<rect key="frame" x="16" y="0.0" width="324" height="43.5"/>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Sync Status" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="1u0-gh-zP7">
|
||||
<rect key="frame" x="15" y="0.0" width="325" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
@ -292,14 +292,14 @@
|
||||
<tableViewSection headerTitle="3D Touch" id="fdp-8c-oOc">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Cell" textLabel="c5i-qG-ir9" style="IBUITableViewCellStyleDefault" id="SSL-t4-QZj">
|
||||
<rect key="frame" x="0.0" y="755" width="375" height="44"/>
|
||||
<rect key="frame" x="0.0" y="799" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="SSL-t4-QZj" id="hQB-Iy-bzy">
|
||||
<rect key="frame" x="0.0" y="0.0" width="341" height="43.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="349" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="App Icon Shortcuts" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="c5i-qG-ir9">
|
||||
<rect key="frame" x="16" y="0.0" width="324" height="43.5"/>
|
||||
<rect key="frame" x="15" y="0.0" width="325" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
@ -724,27 +724,38 @@
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
|
||||
<sections>
|
||||
<tableViewSection headerTitle="Service" id="mIB-Au-dYz">
|
||||
<tableViewSection id="m5I-He-R1D">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="QAp-WA-1g3" style="IBUITableViewCellStyleDefault" id="vkb-8K-t7E">
|
||||
<rect key="frame" x="0.0" y="55.5" width="375" height="44"/>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="none" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="SwitchCell" rowHeight="44" id="fIu-zg-60Y" customClass="SwitchTableViewCell">
|
||||
<rect key="frame" x="0.0" y="35" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="vkb-8K-t7E" id="YcK-vq-ABN">
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="fIu-zg-60Y" id="e2H-i1-YQc">
|
||||
<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="None" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="QAp-WA-1g3">
|
||||
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="vXS-JG-YrF">
|
||||
<rect key="frame" x="310" y="6.5" width="51" height="31"/>
|
||||
<connections>
|
||||
<action selector="toggleSyncing:" destination="R9m-rV-VgV" eventType="primaryActionTriggered" id="KNw-Wb-hIW"/>
|
||||
</connections>
|
||||
</switch>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
|
||||
<constraints>
|
||||
<constraint firstItem="vXS-JG-YrF" firstAttribute="trailing" secondItem="e2H-i1-YQc" secondAttribute="trailingMargin" id="G2g-ka-Gds"/>
|
||||
<constraint firstItem="vXS-JG-YrF" firstAttribute="centerY" secondItem="e2H-i1-YQc" secondAttribute="centerY" id="RDW-mS-nrf"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="switchView" destination="vXS-JG-YrF" id="RaC-P2-WCJ"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Service" id="mIB-Au-dYz">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="4fb-TC-FrG" style="IBUITableViewCellStyleDefault" id="hBZ-Fp-9Kh">
|
||||
<rect key="frame" x="0.0" y="99.5" width="375" height="44"/>
|
||||
<rect key="frame" x="0.0" y="135" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="hBZ-Fp-9Kh" id="rfN-2N-L43">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||
@ -760,18 +771,52 @@
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="qR3-zd-gmV" style="IBUITableViewCellStyleDefault" id="Kfm-x4-Gub">
|
||||
<rect key="frame" x="0.0" y="179" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Kfm-x4-Gub" id="IAV-o1-LfP">
|
||||
<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="Dropbox" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="qR3-zd-gmV">
|
||||
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Account" id="1Wk-cG-HDE">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" textLabel="AYq-XK-j5L" style="IBUITableViewCellStyleDefault" id="nrN-mu-0HX">
|
||||
<rect key="frame" x="0.0" y="199.5" width="375" height="44"/>
|
||||
<rect key="frame" x="0.0" y="279" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="nrN-mu-0HX" id="lHU-qJ-uhj">
|
||||
<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="riley@rileytestut.com" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="AYq-XK-j5L">
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Riley Testut" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="AYq-XK-j5L">
|
||||
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" textLabel="md7-6u-egL" style="IBUITableViewCellStyleDefault" id="QgL-dW-Di5">
|
||||
<rect key="frame" x="0.0" y="323" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="QgL-dW-Di5" id="N5H-Ta-K7t">
|
||||
<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="riley@rileytestut.com" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="md7-6u-egL">
|
||||
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
@ -786,7 +831,7 @@
|
||||
<tableViewSection headerTitle="" id="Jnq-12-IOu">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" textLabel="4TQ-cm-2sN" style="IBUITableViewCellStyleDefault" id="wRv-En-k1Y">
|
||||
<rect key="frame" x="0.0" y="279.5" width="375" height="44"/>
|
||||
<rect key="frame" x="0.0" y="403" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="wRv-En-k1Y" id="7QF-ID-Gu2">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||
@ -810,6 +855,9 @@
|
||||
<outlet property="delegate" destination="R9m-rV-VgV" id="qqu-iI-H9F"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<connections>
|
||||
<outlet property="syncingEnabledSwitch" destination="vXS-JG-YrF" id="xf0-Jg-iRi"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="uqz-XU-aTr" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
@ -966,7 +1014,7 @@
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Local" id="vpJ-Pg-nU9">
|
||||
<tableViewSection headerTitle="On Device" id="vpJ-Pg-nU9">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="Cell" textLabel="npF-wl-PPC" detailTextLabel="SYD-cR-5TY" style="IBUITableViewCellStyleValue1" id="9Dq-cm-tka">
|
||||
<rect key="frame" x="0.0" y="135" width="375" height="44"/>
|
||||
@ -1018,7 +1066,7 @@
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Remote" id="Bct-0y-ptf">
|
||||
<tableViewSection headerTitle="Cloud" id="Bct-0y-ptf">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="Cell" textLabel="aeh-me-gZl" detailTextLabel="0Rm-b2-HX5" style="IBUITableViewCellStyleValue1" id="djI-O4-xho">
|
||||
<rect key="frame" x="0.0" y="279" width="375" height="44"/>
|
||||
@ -1187,23 +1235,6 @@
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="gray" indentationWidth="10" reuseIdentifier="ConfirmCell" textLabel="saf-eQ-eJc" style="IBUITableViewCellStyleDefault" id="x0b-KE-P94">
|
||||
<rect key="frame" x="0.0" y="143.5" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="x0b-KE-P94" id="ufh-yE-uv2">
|
||||
<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" insetsLayoutMarginsFromSafeArea="NO" text="Restore Version" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="saf-eQ-eJc">
|
||||
<rect key="frame" x="16" y="0.0" width="343" 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="PGW-Yp-czd" id="O27-2u-XUE"/>
|
||||
@ -1211,12 +1242,21 @@
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Versions" id="GLC-Lc-6VM">
|
||||
<barButtonItem key="rightBarButtonItem" style="done" systemItem="done" id="LMc-KQ-vea">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="LMc-KQ-vea">
|
||||
<connections>
|
||||
<segue destination="oyk-u7-Dn0" kind="unwind" unwindAction="unwindToRecordSyncStatusViewController:" id="JOO-Bj-i1E"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem key="rightBarButtonItem" enabled="NO" title="Restore" style="done" id="I8z-cN-A01">
|
||||
<color key="tintColor" red="1" green="0.14901960780000001" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<connections>
|
||||
<action selector="restore:" destination="PGW-Yp-czd" id="wQ3-0j-AXC"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="restoreButton" destination="I8z-cN-A01" id="9Bi-Qg-usS"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="p6Y-9f-cL2" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<exit id="oyk-u7-Dn0" userLabel="Exit" sceneMemberID="exit"/>
|
||||
|
||||
@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14490.78" systemVersion="18C54" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="LocalRecord" representedClassName="LocalRecord" syncable="YES">
|
||||
<attribute name="modificationDate" attributeType="Date" syncable="YES"/>
|
||||
<attribute name="recordedObjectIdentifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="recordedObjectType" attributeType="String" syncable="YES"/>
|
||||
<attribute name="recordedObjectURI" attributeType="URI" syncable="YES"/>
|
||||
<attribute name="status" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="versionDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="versionIdentifier" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="managedRecord" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ManagedRecord" inverseName="localRecord" inverseEntity="ManagedRecord" syncable="YES"/>
|
||||
<relationship name="remoteFiles" toMany="YES" deletionRule="Cascade" destinationEntity="RemoteFile" inverseName="localRecord" inverseEntity="RemoteFile" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="recordedObjectType"/>
|
||||
<constraint value="recordedObjectIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="ManagedRecord" representedClassName="ManagedRecord" syncable="YES">
|
||||
<attribute name="isConflicted" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="isSyncingEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="recordedObjectIdentifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="recordedObjectType" attributeType="String" syncable="YES"/>
|
||||
<relationship name="localRecord" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="LocalRecord" inverseName="managedRecord" inverseEntity="LocalRecord" syncable="YES"/>
|
||||
<relationship name="remoteRecord" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="RemoteRecord" inverseName="managedRecord" inverseEntity="RemoteRecord" syncable="YES"/>
|
||||
<fetchIndex name="byRecordedObject">
|
||||
<fetchIndexElement property="recordedObjectType" type="Binary" order="ascending"/>
|
||||
<fetchIndexElement property="recordedObjectIdentifier" type="Binary" order="ascending"/>
|
||||
</fetchIndex>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="recordedObjectType"/>
|
||||
<constraint value="recordedObjectIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="RemoteFile" representedClassName="RemoteFile" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="remoteIdentifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="sha1Hash" attributeType="String" syncable="YES"/>
|
||||
<attribute name="size" attributeType="Integer 32" defaultValueString="0" syncable="YES"/>
|
||||
<attribute name="versionIdentifier" attributeType="String" syncable="YES"/>
|
||||
<relationship name="localRecord" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="LocalRecord" inverseName="remoteFiles" inverseEntity="LocalRecord" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="RemoteRecord" representedClassName="RemoteRecord" syncable="YES">
|
||||
<attribute name="author" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isLocked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="localizedName" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="metadata" optional="YES" attributeType="Transformable" syncable="YES"/>
|
||||
<attribute name="previousVersionDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="previousVersionIdentifier" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="recordedObjectIdentifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="recordedObjectType" attributeType="String" syncable="YES"/>
|
||||
<attribute name="status" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="versionIdentifier" attributeType="String" syncable="YES"/>
|
||||
<relationship name="managedRecord" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ManagedRecord" inverseName="remoteRecord" inverseEntity="ManagedRecord" syncable="YES"/>
|
||||
<fetchIndex name="byIdentifier">
|
||||
<fetchIndexElement property="identifier" type="Binary" order="ascending"/>
|
||||
</fetchIndex>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="recordedObjectType"/>
|
||||
<constraint value="recordedObjectIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<configuration name="External"/>
|
||||
<configuration name="Harmony">
|
||||
<memberEntity name="ManagedRecord"/>
|
||||
<memberEntity name="LocalRecord"/>
|
||||
<memberEntity name="RemoteRecord"/>
|
||||
<memberEntity name="RemoteFile"/>
|
||||
</configuration>
|
||||
<elements>
|
||||
<element name="LocalRecord" positionX="93.01953125" positionY="273.0234375" width="128" height="180"/>
|
||||
<element name="ManagedRecord" positionX="298.4375" positionY="225.49609375" width="128" height="133"/>
|
||||
<element name="RemoteFile" positionX="288" positionY="378" width="128" height="135"/>
|
||||
<element name="RemoteRecord" positionX="488.2578125" positionY="272.56640625" width="128" height="240"/>
|
||||
</elements>
|
||||
</model>
|
||||
@ -527,22 +527,32 @@ private extension GameViewController
|
||||
guard let game = self.game as? Game else { return }
|
||||
|
||||
DatabaseManager.shared.performBackgroundTask { (context) in
|
||||
let game = context.object(with: game.objectID) as! Game
|
||||
|
||||
if let gameSave = game.gameSave
|
||||
{
|
||||
gameSave.modifiedDate = Date()
|
||||
}
|
||||
else
|
||||
{
|
||||
let gameSave = GameSave(context: context)
|
||||
gameSave.identifier = game.identifier
|
||||
game.gameSave = gameSave
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
let game = context.object(with: game.objectID) as! Game
|
||||
|
||||
let hash = try RSTHasher.sha1HashOfFile(at: game.gameSaveURL)
|
||||
let previousHash = game.gameSaveURL.extendedAttribute(name: "com.rileytestut.delta.sha1Hash")
|
||||
|
||||
guard hash != previousHash else { return }
|
||||
|
||||
if let gameSave = game.gameSave
|
||||
{
|
||||
gameSave.modifiedDate = Date()
|
||||
}
|
||||
else
|
||||
{
|
||||
let gameSave = GameSave(context: context)
|
||||
gameSave.identifier = game.identifier
|
||||
game.gameSave = gameSave
|
||||
}
|
||||
|
||||
try context.save()
|
||||
try game.gameSaveURL.setExtendedAttribute(name: "com.rileytestut.delta.sha1Hash", value: hash)
|
||||
}
|
||||
catch CocoaError.fileNoSuchFile
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
45
Delta/Extensions/URL+ExtendedAttributes.swift
Normal file
45
Delta/Extensions/URL+ExtendedAttributes.swift
Normal file
@ -0,0 +1,45 @@
|
||||
//
|
||||
// URL+ExtendedAttributes.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 3/26/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension URL
|
||||
{
|
||||
func setExtendedAttribute(name: String, value: String) throws
|
||||
{
|
||||
try self.withUnsafeFileSystemRepresentation { (path) in
|
||||
let data = value.data(using: .utf8)
|
||||
let result = data?.withUnsafeBytes { (buffer) in
|
||||
setxattr(path, name, buffer.baseAddress, buffer.count, 0, 0)
|
||||
}
|
||||
|
||||
if let result = result, result < 0
|
||||
{
|
||||
throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .ENOENT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func extendedAttribute(name: String) -> String?
|
||||
{
|
||||
let value = self.withUnsafeFileSystemRepresentation { (path) -> String? in
|
||||
let size = getxattr(path, name, nil, 0, 0, 0)
|
||||
guard size >= 0 else { return nil }
|
||||
|
||||
var data = Data(count: size)
|
||||
let result = data.withUnsafeMutableBytes { getxattr(path, name, $0.baseAddress, $0.count, 0, 0) }
|
||||
|
||||
guard result >= 0 else { return nil }
|
||||
|
||||
let value = String(data: data, encoding: .utf8)!
|
||||
return value
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
@ -346,13 +346,13 @@ private extension GameCollectionViewController
|
||||
guard game.fileURL != self.activeEmulatorCore?.game.fileURL else { throw LaunchError.alreadyRunning }
|
||||
}
|
||||
|
||||
if SyncManager.shared.syncCoordinator.isSyncing
|
||||
if let coordinator = SyncManager.shared.coordinator, coordinator.isSyncing
|
||||
{
|
||||
if let gameSave = game.gameSave
|
||||
{
|
||||
do
|
||||
{
|
||||
if let record = try SyncManager.shared.recordController.fetchRecords(for: [gameSave]).first
|
||||
if let record = try coordinator.recordController.fetchRecords(for: [gameSave]).first
|
||||
{
|
||||
if record.isSyncingEnabled && !record.isConflicted && (record.localStatus == nil || record.remoteStatus == .updated)
|
||||
{
|
||||
@ -535,7 +535,7 @@ private extension GameCollectionViewController
|
||||
context.saveWithErrorLogging()
|
||||
|
||||
// Local image URLs may not change despite being a different image, so manually mark record as updated.
|
||||
SyncManager.shared.recordController.updateRecord(for: temporaryGame)
|
||||
SyncManager.shared.recordController?.updateRecord(for: temporaryGame)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.presentedViewController?.dismiss(animated: true, completion: nil)
|
||||
@ -625,7 +625,7 @@ private extension GameCollectionViewController
|
||||
|
||||
if let gameSave = game.gameSave
|
||||
{
|
||||
SyncManager.shared.recordController.updateRecord(for: gameSave)
|
||||
SyncManager.shared.recordController?.updateRecord(for: gameSave)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,7 +48,15 @@ class GamesViewController: UIViewController
|
||||
|
||||
private var searchController: RSTSearchController?
|
||||
|
||||
private var syncingToastView: RSTToastView?
|
||||
private var syncingToastView: RSTToastView? {
|
||||
didSet {
|
||||
if self.syncingToastView == nil
|
||||
{
|
||||
self.syncingProgressObservation = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
private var syncingProgressObservation: NSKeyValueObservation?
|
||||
|
||||
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
|
||||
fatalError("initWithNibName: not implemented")
|
||||
@ -382,13 +390,21 @@ private extension GamesViewController
|
||||
|
||||
func showSyncingToastViewIfNeeded()
|
||||
{
|
||||
guard SyncManager.shared.syncCoordinator.isSyncing && self.syncingToastView == nil else { return }
|
||||
|
||||
let toastView = RSTToastView(text: NSLocalizedString("Syncing...", comment: ""), detailText: nil)
|
||||
guard let coordinator = SyncManager.shared.coordinator, let syncProgress = SyncManager.shared.syncProgress, coordinator.isSyncing && self.syncingToastView == nil else { return }
|
||||
|
||||
let toastView = RSTToastView(text: NSLocalizedString("Syncing...", comment: ""), detailText: syncProgress.localizedAdditionalDescription)
|
||||
toastView.activityIndicatorView.startAnimating()
|
||||
toastView.addTarget(self, action: #selector(GamesViewController.hideSyncingToastView), for: .touchUpInside)
|
||||
toastView.show(in: self.view)
|
||||
|
||||
self.syncingProgressObservation = syncProgress.observe(\.localizedAdditionalDescription) { [weak toastView, weak self] (progress, change) in
|
||||
DispatchQueue.main.async {
|
||||
// Prevent us from updating text right as we're dismissing the toast view.
|
||||
guard self?.syncingToastView != nil else { return }
|
||||
toastView?.detailTextLabel.text = progress.localizedAdditionalDescription
|
||||
}
|
||||
}
|
||||
|
||||
self.syncingToastView = toastView
|
||||
}
|
||||
|
||||
@ -402,6 +418,7 @@ private extension GamesViewController
|
||||
case .failure(let error): toastView = RSTToastView(text: NSLocalizedString("Sync Failed", comment: ""), detailText: error.failureReason)
|
||||
}
|
||||
|
||||
toastView.textLabel.textAlignment = .center
|
||||
toastView.addTarget(self, action: #selector(GamesViewController.presentSyncResultsViewController), for: .touchUpInside)
|
||||
|
||||
toastView.show(in: self.view, duration: 2.0)
|
||||
|
||||
@ -9,6 +9,8 @@
|
||||
import UIKit
|
||||
import Roxas
|
||||
|
||||
import Harmony
|
||||
|
||||
class LaunchViewController: RSTLaunchViewController
|
||||
{
|
||||
@IBOutlet private var gameViewContainerView: UIView!
|
||||
@ -59,14 +61,9 @@ extension LaunchViewController
|
||||
}
|
||||
|
||||
let isSyncingManagerStarted = RSTLaunchCondition(condition: { self.didAttemptStartingSyncManager }) { (completionHandler) in
|
||||
SyncManager.shared.syncCoordinator.start { (error) in
|
||||
self.didAttemptStartingSyncManager = true
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
|
||||
let isRecordControllerSeeded = RSTLaunchCondition(condition: { SyncManager.shared.syncCoordinator.recordController.isSeeded }) { (completionHandler) in
|
||||
SyncManager.shared.syncCoordinator.recordController.seedFromPersistentContainer() { (result) in
|
||||
self.didAttemptStartingSyncManager = true
|
||||
|
||||
SyncManager.shared.start(service: Settings.syncingService) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success: completionHandler(nil)
|
||||
@ -75,16 +72,28 @@ extension LaunchViewController
|
||||
}
|
||||
}
|
||||
|
||||
return [isDatabaseManagerStarted, isSyncingManagerStarted, isRecordControllerSeeded]
|
||||
return [isDatabaseManagerStarted, isSyncingManagerStarted]
|
||||
}
|
||||
|
||||
override func handleLaunchError(_ error: Error)
|
||||
{
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Unable to Launch Delta", comment: ""), message: error.localizedDescription, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { (action) in
|
||||
do
|
||||
{
|
||||
throw error
|
||||
}
|
||||
catch is HarmonyError
|
||||
{
|
||||
// Ignore
|
||||
self.handleLaunchConditions()
|
||||
}))
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
catch
|
||||
{
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Unable to Launch Delta", comment: ""), message: error.localizedDescription, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Retry", comment: ""), style: .default, handler: { (action) in
|
||||
self.handleLaunchConditions()
|
||||
}))
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
override func finishLaunching()
|
||||
|
||||
@ -130,10 +130,13 @@ extension Settings
|
||||
}
|
||||
}
|
||||
|
||||
static var syncingService: SyncingService {
|
||||
get { return SyncingService(rawValue: UserDefaults.standard.syncingService) ?? .none }
|
||||
static var syncingService: SyncManager.Service? {
|
||||
get {
|
||||
guard let syncingService = UserDefaults.standard.syncingService else { return nil }
|
||||
return SyncManager.Service(rawValue: syncingService)
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.syncingService = newValue.rawValue
|
||||
UserDefaults.standard.syncingService = newValue?.rawValue
|
||||
NotificationCenter.default.post(name: .settingsDidChange, object: nil, userInfo: [NotificationUserInfoKey.name: Name.syncingService])
|
||||
}
|
||||
}
|
||||
@ -234,5 +237,5 @@ private extension UserDefaults
|
||||
@NSManaged var gameShortcutsMode: String
|
||||
@NSManaged var gameShortcutIdentifiers: [String]
|
||||
|
||||
@NSManaged var syncingService: String
|
||||
@NSManaged var syncingService: String?
|
||||
}
|
||||
|
||||
@ -131,11 +131,11 @@ private extension SettingsViewController
|
||||
self.controllerOpacitySlider.value = Float(Settings.translucentControllerSkinOpacity)
|
||||
self.updateControllerOpacityLabel()
|
||||
|
||||
self.syncingServiceLabel.text = Settings.syncingService.localizedName
|
||||
self.syncingServiceLabel.text = Settings.syncingService?.localizedName
|
||||
|
||||
do
|
||||
{
|
||||
let records = try SyncManager.shared.recordController.fetchConflictedRecords()
|
||||
let records = try SyncManager.shared.recordController?.fetchConflictedRecords() ?? []
|
||||
self.syncingConflictsCount = records.count
|
||||
}
|
||||
catch
|
||||
@ -233,7 +233,7 @@ extension SettingsViewController
|
||||
{
|
||||
case .controllers: return 1 // Temporarily hide other controller indexes until controller logic is finalized
|
||||
case .controllerSkins: return System.allCases.count
|
||||
case .syncing: return Settings.syncingService == .none ? 1 : super.tableView(tableView, numberOfRowsInSection: sectionIndex)
|
||||
case .syncing: return SyncManager.shared.coordinator?.account == nil ? 1 : super.tableView(tableView, numberOfRowsInSection: sectionIndex)
|
||||
default:
|
||||
if isSectionHidden(section)
|
||||
{
|
||||
|
||||
@ -57,7 +57,7 @@ class GameSyncStatusViewController: UITableViewController
|
||||
|
||||
do
|
||||
{
|
||||
let records = try SyncManager.shared.recordController.fetchRecords(for: [recordedObject])
|
||||
let records = try SyncManager.shared.recordController?.fetchRecords(for: [recordedObject]) ?? []
|
||||
|
||||
let recordSyncStatusViewController = segue.destination as! RecordSyncStatusViewController
|
||||
recordSyncStatusViewController.record = records.first
|
||||
@ -132,12 +132,14 @@ private extension GameSyncStatusViewController
|
||||
|
||||
func fetchRecords()
|
||||
{
|
||||
guard let recordController = SyncManager.shared.recordController else { return }
|
||||
|
||||
var recordsByObjectURI = [URL: Record<NSManagedObject>]()
|
||||
|
||||
do
|
||||
{
|
||||
let recordedObjects = ([self.game, self.game.gameSave].compactMap { $0 } + Array(self.game.saveStates) + Array(self.game.cheats)) as! [Syncable]
|
||||
let records = try SyncManager.shared.recordController.fetchRecords(for: recordedObjects)
|
||||
let records = try recordController.fetchRecords(for: recordedObjects)
|
||||
|
||||
for record in records
|
||||
{
|
||||
|
||||
@ -86,6 +86,19 @@ private extension RecordSyncStatusViewController
|
||||
{
|
||||
if let record = self.record
|
||||
{
|
||||
var title: String?
|
||||
|
||||
if let recordType = SyncManager.RecordType(rawValue: record.recordID.type)
|
||||
{
|
||||
switch recordType
|
||||
{
|
||||
case .game, .gameSave: title = recordType.localizedName
|
||||
case .cheat, .controllerSkin, .gameCollection, .gameControllerInputMapping, .saveState: break
|
||||
}
|
||||
}
|
||||
|
||||
self.title = title ?? record.localizedName
|
||||
|
||||
self.syncingEnabledSwitch.isEnabled = !record.isConflicted
|
||||
self.syncingEnabledSwitch.isOn = record.isSyncingEnabled
|
||||
|
||||
|
||||
@ -17,7 +17,6 @@ extension RecordVersionsViewController
|
||||
{
|
||||
case local
|
||||
case remote
|
||||
case confirm
|
||||
}
|
||||
|
||||
private enum Mode
|
||||
@ -42,6 +41,7 @@ class RecordVersionsViewController: UITableViewController
|
||||
var record: Record<NSManagedObject>! {
|
||||
didSet {
|
||||
self.mode = self.record.isConflicted ? .resolveConflict : .restoreVersion
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,6 +80,8 @@ class RecordVersionsViewController: UITableViewController
|
||||
|
||||
private var _progressObservation: NSKeyValueObservation?
|
||||
|
||||
@IBOutlet private var restoreButton: UIBarButtonItem!
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
@ -98,6 +100,8 @@ class RecordVersionsViewController: UITableViewController
|
||||
}
|
||||
|
||||
self.tableView.dataSource = self.dataSource
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool)
|
||||
@ -164,39 +168,32 @@ private extension RecordVersionsViewController
|
||||
let remoteVersionsLoadingDataSource = RSTCompositeTableViewDataSource(dataSources: [loadingDataSource, remoteVersionsDataSource])
|
||||
remoteVersionsLoadingDataSource.shouldFlattenSections = true
|
||||
|
||||
let restoreVersionDataSource = RSTDynamicTableViewDataSource<Version>()
|
||||
restoreVersionDataSource.numberOfSectionsHandler = { 1 }
|
||||
restoreVersionDataSource.numberOfItemsHandler = { _ in 1}
|
||||
restoreVersionDataSource.cellIdentifierHandler = { _ in "ConfirmCell" }
|
||||
restoreVersionDataSource.cellConfigurationHandler = { [weak self] (cell, _, indexPath) in
|
||||
guard let `self` = self else { return }
|
||||
|
||||
switch self.mode
|
||||
{
|
||||
case .restoreVersion:
|
||||
cell.textLabel?.text = NSLocalizedString("Restore Version", comment: "")
|
||||
cell.textLabel?.textColor = .deltaPurple
|
||||
|
||||
let isEnabled = (self._selectedVersionIndexPath?.section == Section.remote.rawValue && !self.isSyncingRecord)
|
||||
configure(cell, isSelected: false, isEnabled: isEnabled)
|
||||
|
||||
case .resolveConflict:
|
||||
cell.textLabel?.text = NSLocalizedString("Resolve Conflict", comment: "")
|
||||
cell.textLabel?.textColor = .red
|
||||
|
||||
let isEnabled = (self._selectedVersionIndexPath != nil && !self.isSyncingRecord)
|
||||
configure(cell, isSelected: false, isEnabled: isEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
let dataSource = RSTCompositeTableViewDataSource(dataSources: [localVersionsDataSource, remoteVersionsLoadingDataSource, restoreVersionDataSource])
|
||||
let dataSource = RSTCompositeTableViewDataSource(dataSources: [localVersionsDataSource, remoteVersionsLoadingDataSource])
|
||||
dataSource.proxy = self
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func update()
|
||||
{
|
||||
switch self.mode
|
||||
{
|
||||
case .restoreVersion:
|
||||
self.restoreButton.title = NSLocalizedString("Restore", comment: "")
|
||||
self.restoreButton.tintColor = .deltaPurple
|
||||
|
||||
self.restoreButton.isEnabled = (self._selectedVersionIndexPath?.section == Section.remote.rawValue)
|
||||
|
||||
case .resolveConflict:
|
||||
self.restoreButton.title = NSLocalizedString("Resolve", comment: "")
|
||||
self.restoreButton.tintColor = .red
|
||||
|
||||
self.restoreButton.isEnabled = (self._selectedVersionIndexPath != nil)
|
||||
}
|
||||
}
|
||||
|
||||
func fetchVersions()
|
||||
{
|
||||
SyncManager.shared.syncCoordinator.fetchVersions(for: self.record) { (result) in
|
||||
SyncManager.shared.coordinator?.fetchVersions(for: self.record) { (result) in
|
||||
do
|
||||
{
|
||||
let versions = try result.get().map(Version.init)
|
||||
@ -242,6 +239,8 @@ private extension RecordVersionsViewController
|
||||
|
||||
guard let indexPath = self._selectedVersionIndexPath else { return }
|
||||
|
||||
guard let coordinator = SyncManager.shared.coordinator else { return }
|
||||
|
||||
func finish<T: Error>(_ result: Result<AnyRecord, T>)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
@ -253,11 +252,12 @@ private extension RecordVersionsViewController
|
||||
self._progressObservation = nil
|
||||
|
||||
self.progressView.setHidden(true, animated: true)
|
||||
self.navigationItem.rightBarButtonItem?.isIndicatingActivity = false
|
||||
|
||||
self.update()
|
||||
|
||||
self.tableView.reloadData()
|
||||
|
||||
self.navigationItem.rightBarButtonItem?.isIndicatingActivity = false
|
||||
|
||||
switch result
|
||||
{
|
||||
case .success: self.fetchVersions()
|
||||
@ -298,23 +298,21 @@ private extension RecordVersionsViewController
|
||||
case (.restoreVersion, _):
|
||||
let version = self.dataSource.item(at: indexPath)
|
||||
|
||||
progress = SyncManager.shared.syncCoordinator.restore(self.record, to: version.version) { (result) in
|
||||
progress = coordinator.restore(self.record, to: version.version) { (result) in
|
||||
finish(result)
|
||||
}
|
||||
|
||||
case (.resolveConflict, .local):
|
||||
progress = SyncManager.shared.syncCoordinator.resolveConflictedRecord(self.record, resolution: .local) { (result) in
|
||||
progress = coordinator.resolveConflictedRecord(self.record, resolution: .local) { (result) in
|
||||
finish(result)
|
||||
}
|
||||
|
||||
case (.resolveConflict, .remote):
|
||||
let version = self.dataSource.item(at: indexPath)
|
||||
|
||||
progress = SyncManager.shared.syncCoordinator.resolveConflictedRecord(self.record, resolution: .remote(version.version)) { (result) in
|
||||
progress = coordinator.resolveConflictedRecord(self.record, resolution: .remote(version.version)) { (result) in
|
||||
finish(result)
|
||||
}
|
||||
|
||||
case (.resolveConflict, .confirm): return
|
||||
}
|
||||
|
||||
self.isSyncingRecord = true
|
||||
@ -333,15 +331,50 @@ private extension RecordVersionsViewController
|
||||
}
|
||||
}
|
||||
|
||||
private extension RecordVersionsViewController
|
||||
{
|
||||
@IBAction func restore(_ sender: UIBarButtonItem)
|
||||
{
|
||||
let message: String
|
||||
let actionTitle: String
|
||||
|
||||
switch self.mode
|
||||
{
|
||||
case .restoreVersion:
|
||||
message = NSLocalizedString("Restoring a remote version will cause any local changes to be lost.", comment: "")
|
||||
actionTitle = NSLocalizedString("Restore Version", comment: "")
|
||||
|
||||
case .resolveConflict:
|
||||
if self._selectedVersionIndexPath?.section == Section.local.rawValue
|
||||
{
|
||||
message = NSLocalizedString("The local version will be uploaded and synced to your other devices.", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
message = NSLocalizedString("Selecting a remote version will cause any local changes to be lost.", comment: "")
|
||||
}
|
||||
|
||||
actionTitle = NSLocalizedString("Resolve Conflict", comment: "")
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: nil, message: message, preferredStyle: .actionSheet)
|
||||
alertController.addAction(.cancel)
|
||||
alertController.addAction(UIAlertAction(title: actionTitle, style: .destructive) { (action) in
|
||||
self.restoreVersion()
|
||||
})
|
||||
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension RecordVersionsViewController
|
||||
{
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?
|
||||
{
|
||||
switch Section.allCases[section]
|
||||
{
|
||||
case .local: return NSLocalizedString("Local", comment: "")
|
||||
case .remote: return NSLocalizedString("Remote", comment: "")
|
||||
case .confirm: return nil
|
||||
case .local: return NSLocalizedString("On Device", comment: "")
|
||||
case .remote: return NSLocalizedString("Cloud", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
@ -351,44 +384,11 @@ extension RecordVersionsViewController
|
||||
|
||||
guard let cell = tableView.cellForRow(at: indexPath), cell.selectionStyle != .none else { return }
|
||||
|
||||
switch Section.allCases[indexPath.section]
|
||||
{
|
||||
case .local, .remote:
|
||||
let indexPaths = [indexPath, self._selectedVersionIndexPath, IndexPath(item: 0, section: Section.confirm.rawValue)].compactMap { $0 }
|
||||
self._selectedVersionIndexPath = indexPath
|
||||
|
||||
tableView.reloadRows(at: indexPaths, with: .none)
|
||||
|
||||
case .confirm:
|
||||
let message: String
|
||||
let actionTitle: String
|
||||
|
||||
switch self.mode
|
||||
{
|
||||
case .restoreVersion:
|
||||
message = NSLocalizedString("Restoring a remote version will cause any local changes to be lost.", comment: "")
|
||||
actionTitle = NSLocalizedString("Restore Version", comment: "")
|
||||
|
||||
case .resolveConflict:
|
||||
if self._selectedVersionIndexPath?.section == Section.local.rawValue
|
||||
{
|
||||
message = NSLocalizedString("The local version will be uploaded and synced to your other devices.", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
message = NSLocalizedString("Selecting a remote version will cause any local changes to be lost.", comment: "")
|
||||
}
|
||||
|
||||
actionTitle = NSLocalizedString("Resolve Conflict", comment: "")
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: nil, message: message, preferredStyle: .actionSheet)
|
||||
alertController.addAction(.cancel)
|
||||
alertController.addAction(UIAlertAction(title: actionTitle, style: .destructive) { (action) in
|
||||
self.restoreVersion()
|
||||
})
|
||||
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
let indexPaths = [indexPath, self._selectedVersionIndexPath].compactMap { $0 }
|
||||
self._selectedVersionIndexPath = indexPath
|
||||
|
||||
tableView.reloadRows(at: indexPaths, with: .none)
|
||||
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,8 +20,12 @@ class SyncStatusViewController: UITableViewController
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.definesPresentationContext = true
|
||||
|
||||
self.tableView.dataSource = self.dataSource
|
||||
self.navigationItem.searchController = self.dataSource.searchController
|
||||
|
||||
let fetchedDataSource = self.dataSource.dataSources.last
|
||||
self.navigationItem.searchController = fetchedDataSource?.searchController
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool)
|
||||
@ -116,12 +120,14 @@ private extension SyncStatusViewController
|
||||
|
||||
func fetchConflictedRecords()
|
||||
{
|
||||
guard let recordController = SyncManager.shared.recordController else { return }
|
||||
|
||||
DispatchQueue.global().async {
|
||||
do
|
||||
{
|
||||
var gameConflictsCount = [URL: Int]()
|
||||
|
||||
let records = try SyncManager.shared.recordController.fetchConflictedRecords()
|
||||
let records = try recordController.fetchConflictedRecords()
|
||||
|
||||
for record in records
|
||||
{
|
||||
|
||||
@ -13,37 +13,106 @@ import Harmony_Drive
|
||||
|
||||
import Roxas
|
||||
|
||||
enum SyncingService: String, CaseIterable
|
||||
{
|
||||
case none
|
||||
case googleDrive
|
||||
|
||||
var localizedName: String {
|
||||
switch self
|
||||
{
|
||||
case .none: return NSLocalizedString("None", comment: "")
|
||||
case .googleDrive: return NSLocalizedString("Google Drive", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SyncingServicesViewController
|
||||
{
|
||||
enum Section: Int, CaseIterable
|
||||
{
|
||||
case syncing
|
||||
case service
|
||||
case account
|
||||
case authenticate
|
||||
}
|
||||
|
||||
enum AccountRow: Int, CaseIterable
|
||||
{
|
||||
case name
|
||||
case emailAddress
|
||||
}
|
||||
}
|
||||
|
||||
class SyncingServicesViewController: UITableViewController
|
||||
{
|
||||
@IBOutlet private var syncingEnabledSwitch: UISwitch!
|
||||
|
||||
private var selectedSyncingService = Settings.syncingService
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.syncingEnabledSwitch.onTintColor = .deltaPurple
|
||||
self.syncingEnabledSwitch.isOn = (self.selectedSyncingService != nil)
|
||||
}
|
||||
}
|
||||
|
||||
private extension SyncingServicesViewController
|
||||
{
|
||||
@IBAction func toggleSyncing(_ sender: UISwitch)
|
||||
{
|
||||
if sender.isOn
|
||||
{
|
||||
self.changeService(to: SyncManager.Service.allCases.first)
|
||||
}
|
||||
else
|
||||
{
|
||||
if SyncManager.shared.coordinator?.account != nil
|
||||
{
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Disable Syncing?", comment: ""), message: NSLocalizedString("Enabling syncing again later may result in conflicts that must be resolved manually.", comment: ""), preferredStyle: .alert)
|
||||
alertController.addAction(.cancel)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Disable", comment: ""), style: .default) { (action) in
|
||||
self.changeService(to: nil)
|
||||
})
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.changeService(to: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func changeService(to service: SyncManager.Service?)
|
||||
{
|
||||
SyncManager.shared.reset(for: service) { (result) in
|
||||
DispatchQueue.main.async {
|
||||
do
|
||||
{
|
||||
try result.get()
|
||||
|
||||
let previousService = self.selectedSyncingService
|
||||
self.selectedSyncingService = service
|
||||
|
||||
// Set to non-nil if we later authenticate.
|
||||
Settings.syncingService = nil
|
||||
|
||||
if (previousService == nil && service != nil) || (previousService != nil && service == nil)
|
||||
{
|
||||
self.tableView.reloadSections(IndexSet(integersIn: Section.service.rawValue ... Section.authenticate.rawValue), with: .fade)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Unable to Change Syncing Service", comment: ""), error: error)
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension SyncingServicesViewController
|
||||
{
|
||||
func isSectionHidden(_ section: Section) -> Bool
|
||||
{
|
||||
switch section
|
||||
{
|
||||
case .account: return SyncManager.shared.syncCoordinator.account == nil
|
||||
case .service: return !self.syncingEnabledSwitch.isOn
|
||||
case .account: return !self.syncingEnabledSwitch.isOn || SyncManager.shared.coordinator?.account == nil
|
||||
case .authenticate: return !self.syncingEnabledSwitch.isOn
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
@ -51,28 +120,31 @@ class SyncingServicesViewController: UITableViewController
|
||||
|
||||
extension SyncingServicesViewController
|
||||
{
|
||||
override func numberOfSections(in tableView: UITableView) -> Int
|
||||
{
|
||||
guard Settings.syncingService != .none else { return 1 }
|
||||
|
||||
return super.numberOfSections(in: tableView)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
|
||||
{
|
||||
let cell = super.tableView(tableView, cellForRowAt: indexPath)
|
||||
|
||||
switch Section.allCases[indexPath.section]
|
||||
{
|
||||
case .syncing:
|
||||
cell.textLabel?.text = NSLocalizedString("Syncing", comment: "")
|
||||
|
||||
case .service:
|
||||
let service = SyncingService.allCases[indexPath.row]
|
||||
cell.accessoryType = (service == Settings.syncingService) ? .checkmark : .none
|
||||
let service = SyncManager.Service.allCases[indexPath.row]
|
||||
cell.accessoryType = (service == self.selectedSyncingService) ? .checkmark : .none
|
||||
|
||||
case .account:
|
||||
cell.textLabel?.text = SyncManager.shared.syncCoordinator.account?.name ?? NSLocalizedString("Unknown Account", comment: "")
|
||||
guard let account = SyncManager.shared.coordinator?.account else { return cell }
|
||||
|
||||
let row = AccountRow(rawValue: indexPath.row)!
|
||||
switch row
|
||||
{
|
||||
case .name: cell.textLabel?.text = account.name
|
||||
case .emailAddress: cell.textLabel?.text = account.emailAddress
|
||||
}
|
||||
|
||||
case .authenticate:
|
||||
if SyncManager.shared.syncCoordinator.isAuthenticated
|
||||
if SyncManager.shared.coordinator?.account != nil
|
||||
{
|
||||
cell.textLabel?.textColor = .red
|
||||
cell.textLabel?.text = NSLocalizedString("Sign Out", comment: "")
|
||||
@ -91,31 +163,40 @@ extension SyncingServicesViewController
|
||||
{
|
||||
switch Section.allCases[indexPath.section]
|
||||
{
|
||||
case .syncing: break
|
||||
|
||||
case .service:
|
||||
Settings.syncingService = SyncingService.allCases[indexPath.row]
|
||||
let syncingService = SyncManager.Service.allCases[indexPath.row]
|
||||
guard syncingService != self.selectedSyncingService else { return }
|
||||
|
||||
if Settings.syncingService == .none && self.tableView.numberOfSections > 1
|
||||
if SyncManager.shared.coordinator?.account != nil
|
||||
{
|
||||
self.tableView.deleteSections(IndexSet(integersIn: Section.account.rawValue ... Section.authenticate.rawValue), with: .fade)
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to change sync services?", comment: ""), message: NSLocalizedString("Switching back later may result in conflicts that must be resolved manually.", comment: ""), preferredStyle: .actionSheet)
|
||||
alertController.addAction(.cancel)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Change Sync Service", comment: ""), style: .destructive, handler: { (action) in
|
||||
self.changeService(to: syncingService)
|
||||
}))
|
||||
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
else if Settings.syncingService != .none && self.tableView.numberOfSections == 1
|
||||
else
|
||||
{
|
||||
self.tableView.insertSections(IndexSet(integersIn: Section.account.rawValue ... Section.authenticate.rawValue), with: .fade)
|
||||
self.changeService(to: syncingService)
|
||||
}
|
||||
|
||||
self.tableView.reloadSections(IndexSet(integer: Section.service.rawValue), with: .none)
|
||||
|
||||
case .account: break
|
||||
|
||||
case .authenticate:
|
||||
if SyncManager.shared.syncCoordinator.isAuthenticated
|
||||
case .authenticate:
|
||||
if SyncManager.shared.coordinator?.account != nil
|
||||
{
|
||||
SyncManager.shared.syncCoordinator.deauthenticate { (result) in
|
||||
SyncManager.shared.deauthenticate { (result) in
|
||||
DispatchQueue.main.async {
|
||||
do
|
||||
{
|
||||
try result.get()
|
||||
self.tableView.reloadData()
|
||||
|
||||
Settings.syncingService = nil
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -127,16 +208,21 @@ extension SyncingServicesViewController
|
||||
}
|
||||
else
|
||||
{
|
||||
SyncManager.shared.syncCoordinator.authenticate(presentingViewController: self) { (result) in
|
||||
SyncManager.shared.authenticate(presentingViewController: self) { (result) in
|
||||
DispatchQueue.main.async {
|
||||
do
|
||||
{
|
||||
_ = try result.get()
|
||||
self.tableView.reloadData()
|
||||
|
||||
Settings.syncingService = self.selectedSyncingService
|
||||
}
|
||||
catch GeneralError.cancelled.self
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Failed to Sign In", comment: ""), error: error)
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
@ -150,13 +236,11 @@ extension SyncingServicesViewController
|
||||
{
|
||||
let section = Section.allCases[section]
|
||||
|
||||
if self.isSectionHidden(section)
|
||||
switch section
|
||||
{
|
||||
return 0
|
||||
}
|
||||
else
|
||||
{
|
||||
return super.tableView(tableView, numberOfRowsInSection: section.rawValue)
|
||||
case let section where self.isSectionHidden(section): return 0
|
||||
case .account where SyncManager.shared.coordinator?.account?.emailAddress == nil: return 1
|
||||
default: return super.tableView(tableView, numberOfRowsInSection: section.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -100,7 +100,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.7.0</string>
|
||||
<string>0.7.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
@ -110,9 +110,17 @@
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>com.googleusercontent.apps.457607414709-5puj6lcv779gpu3ql43e6k3smjj40dmu</string>
|
||||
<string>com.googleusercontent.apps.457607414709-7oc45nq59frd7rre6okq22fafftd55g1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>db-f5btgysf9ma9bb6</string>
|
||||
</array>
|
||||
<key>CFBundleURLName</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>7</string>
|
||||
@ -150,7 +158,7 @@
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Press "OK" to allow Delta to use images from your Photo Library as game artwork.</string>
|
||||
<string>Press "OK" to allow Delta to use images from your Photo Library as game artwork.</string>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
@ -272,5 +280,10 @@
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>dbapi-8-emm</string>
|
||||
<string>dbapi-2</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -8,6 +8,12 @@
|
||||
|
||||
import Harmony
|
||||
import Harmony_Drive
|
||||
import Harmony_Dropbox
|
||||
|
||||
private extension UserDefaults
|
||||
{
|
||||
@NSManaged var didValidateHarmonyBetaDatabase: Bool
|
||||
}
|
||||
|
||||
extension SyncManager
|
||||
{
|
||||
@ -34,6 +40,40 @@ extension SyncManager
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Service: String, CaseIterable
|
||||
{
|
||||
case googleDrive = "com.rileytestut.Harmony.Drive"
|
||||
case dropbox = "com.rileytestut.Harmony.Dropbox"
|
||||
|
||||
var localizedName: String {
|
||||
switch self
|
||||
{
|
||||
case .googleDrive: return NSLocalizedString("Google Drive", comment: "")
|
||||
case .dropbox: return NSLocalizedString("Dropbox", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
var service: Harmony.Service {
|
||||
switch self
|
||||
{
|
||||
case .googleDrive: return DriveService.shared
|
||||
case .dropbox: return DropboxService.shared
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Error: LocalizedError
|
||||
{
|
||||
case nilService
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .nilService: return NSLocalizedString("There is no chosen service for syncing.", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Syncable where Self: NSManagedObject
|
||||
@ -48,21 +88,27 @@ final class SyncManager
|
||||
{
|
||||
static let shared = SyncManager()
|
||||
|
||||
var service: Service {
|
||||
return self.syncCoordinator.service
|
||||
var service: Service? {
|
||||
guard let service = self.coordinator?.service else { return nil }
|
||||
return Service(rawValue: service.identifier)
|
||||
}
|
||||
|
||||
var recordController: RecordController {
|
||||
return self.syncCoordinator.recordController
|
||||
var recordController: RecordController? {
|
||||
return self.coordinator?.recordController
|
||||
}
|
||||
|
||||
private(set) var syncProgress: Progress?
|
||||
|
||||
private(set) var previousSyncResult: SyncResult?
|
||||
|
||||
let syncCoordinator = SyncCoordinator(service: DriveService.shared, persistentContainer: DatabaseManager.shared)
|
||||
private(set) var coordinator: SyncCoordinator?
|
||||
|
||||
private init()
|
||||
{
|
||||
DriveService.shared.clientID = "457607414709-5puj6lcv779gpu3ql43e6k3smjj40dmu.apps.googleusercontent.com"
|
||||
DriveService.shared.clientID = "457607414709-7oc45nq59frd7rre6okq22fafftd55g1.apps.googleusercontent.com"
|
||||
|
||||
DropboxService.shared.clientID = "f5btgysf9ma9bb6"
|
||||
DropboxService.shared.preferredDirectoryName = "Delta Emulator"
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(SyncManager.syncingDidFinish(_:)), name: SyncCoordinator.didFinishSyncingNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(SyncManager.didEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
||||
@ -72,11 +118,137 @@ final class SyncManager
|
||||
|
||||
extension SyncManager
|
||||
{
|
||||
func start(service: Service?, completionHandler: @escaping (Result<Void, Swift.Error>) -> Void)
|
||||
{
|
||||
guard let service = service else { return completionHandler(.success) }
|
||||
|
||||
let coordinator = SyncCoordinator(service: service.service, persistentContainer: DatabaseManager.shared)
|
||||
|
||||
if !UserDefaults.standard.didValidateHarmonyBetaDatabase
|
||||
{
|
||||
UserDefaults.standard.didValidateHarmonyBetaDatabase = true
|
||||
|
||||
coordinator.deauthenticate { (result) in
|
||||
do
|
||||
{
|
||||
try FileManager.default.removeItem(at: RecordController.defaultDirectoryURL())
|
||||
}
|
||||
catch CocoaError.fileNoSuchFile
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to remove Harmony database.", error)
|
||||
}
|
||||
|
||||
self.start(service: service, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
coordinator.start { (result) in
|
||||
do
|
||||
{
|
||||
_ = try result.get()
|
||||
|
||||
self.coordinator = coordinator
|
||||
completionHandler(.success)
|
||||
}
|
||||
catch let authError as AuthenticationError
|
||||
{
|
||||
// Authentication failed, but otherwise started successfully so still assign self.coordinator.
|
||||
self.coordinator = coordinator
|
||||
|
||||
switch authError
|
||||
{
|
||||
case .other(ServiceError.connectionFailed):
|
||||
// Authentication failed due to network connection, but otherwise started successfully so we ignore this error.
|
||||
completionHandler(.success)
|
||||
|
||||
default:
|
||||
// Another authentication error occured, so we'll deauthenticate ourselves.
|
||||
print("SyncManager.start auth error:", authError)
|
||||
|
||||
self.deauthenticate() { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success:
|
||||
completionHandler(.success)
|
||||
|
||||
case .failure:
|
||||
// authError is more useful than result's error.
|
||||
completionHandler(.failure(authError))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("SyncManager.start error:", error)
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reset(for service: Service?, completionHandler: @escaping (Result<Void, Swift.Error>) -> Void)
|
||||
{
|
||||
if let coordinator = self.coordinator
|
||||
{
|
||||
coordinator.deauthenticate { (result) in
|
||||
self.coordinator = nil
|
||||
self.start(service: service, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
self.start(service: service, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
func authenticate(presentingViewController: UIViewController? = nil, completionHandler: @escaping (Result<Account, AuthenticationError>) -> Void)
|
||||
{
|
||||
guard let coordinator = self.coordinator else { return completionHandler(.failure(AuthenticationError(Error.nilService))) }
|
||||
|
||||
coordinator.authenticate(presentingViewController: presentingViewController) { (result) in
|
||||
do
|
||||
{
|
||||
let account = try result.get()
|
||||
|
||||
if !coordinator.recordController.isSeeded
|
||||
{
|
||||
coordinator.recordController.seedFromPersistentContainer { (result) in
|
||||
switch result
|
||||
{
|
||||
case .success: completionHandler(.success(account))
|
||||
case .failure(let error): completionHandler(.failure(AuthenticationError(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(.success(account))
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
completionHandler(.failure(AuthenticationError(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deauthenticate(completionHandler: @escaping (Result<Void, DeauthenticationError>) -> Void)
|
||||
{
|
||||
guard let coordinator = self.coordinator else { return completionHandler(.success) }
|
||||
|
||||
coordinator.deauthenticate(completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func sync()
|
||||
{
|
||||
guard Settings.syncingService != .none else { return }
|
||||
|
||||
self.syncCoordinator.sync()
|
||||
let progress = self.coordinator?.sync()
|
||||
self.syncProgress = progress
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,6 +258,10 @@ private extension SyncManager
|
||||
{
|
||||
guard let result = notification.userInfo?[SyncCoordinator.syncResultKey] as? SyncResult else { return }
|
||||
self.previousSyncResult = result
|
||||
|
||||
self.syncProgress = nil
|
||||
|
||||
print("Finished syncing!")
|
||||
}
|
||||
|
||||
@objc func didEnterBackground(_ notification: Notification)
|
||||
|
||||
@ -45,6 +45,20 @@ extension SyncResultViewController
|
||||
}
|
||||
}
|
||||
|
||||
extension Record
|
||||
{
|
||||
var localizedTitle: String {
|
||||
guard let type = SyncManager.RecordType(rawValue: self.recordID.type) else { return self.localizedName ?? NSLocalizedString("Unknown", comment: "") }
|
||||
|
||||
switch type
|
||||
{
|
||||
case .game: return NSLocalizedString("Game", comment: "")
|
||||
case .gameSave: return NSLocalizedString("Game Save", comment: "")
|
||||
case .saveState, .cheat, .controllerSkin, .gameCollection, .gameControllerInputMapping: return self.localizedName ?? type.localizedName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SyncResultViewController: UITableViewController
|
||||
{
|
||||
var result: Result<[Record<NSManagedObject>: Result<Void, RecordError>], SyncError>!
|
||||
@ -69,6 +83,11 @@ class SyncResultViewController: UITableViewController
|
||||
super.viewDidLoad()
|
||||
|
||||
self.tableView.dataSource = self.dataSource
|
||||
|
||||
if let navigationController = self.navigationController, navigationController.viewControllers.count != 1
|
||||
{
|
||||
self.navigationItem.rightBarButtonItem = nil
|
||||
}
|
||||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
|
||||
@ -114,14 +133,7 @@ private extension SyncResultViewController
|
||||
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 .gameSave: title = NSLocalizedString("Game Save", comment: "")
|
||||
case .saveState, .cheat, .controllerSkin, .gameCollection, .gameControllerInputMapping: title = error.record.localizedName ?? recordType.localizedName
|
||||
}
|
||||
title = error.record.localizedTitle
|
||||
|
||||
switch error
|
||||
{
|
||||
@ -204,11 +216,16 @@ private extension SyncResultViewController
|
||||
switch recordType
|
||||
{
|
||||
case .game: group = .game(error.record.recordID)
|
||||
case .gameSave: group = .game(error.record.recordID)
|
||||
case .gameCollection: group = .gameCollection
|
||||
case .controllerSkin: group = .controllerSkin
|
||||
case .gameControllerInputMapping: group = .gameControllerInputMapping
|
||||
|
||||
case .gameSave:
|
||||
guard let gameID = error.record.metadata?[.gameID] else { continue }
|
||||
|
||||
let recordID = RecordID(type: SyncManager.RecordType.game.rawValue, identifier: gameID)
|
||||
group = .game(recordID)
|
||||
|
||||
case .saveState:
|
||||
guard let gameID = error.record.metadata?[.gameID] else { continue }
|
||||
|
||||
@ -228,6 +245,16 @@ private extension SyncResultViewController
|
||||
errorsByGroup[group, default: []].append(error)
|
||||
}
|
||||
|
||||
for (group, errors) in errorsByGroup
|
||||
{
|
||||
let sortedErrors = errors.sorted { (a, b) -> Bool in
|
||||
guard let a = a as? RecordError, let b = b as? RecordError else { return false }
|
||||
return a.record.localizedTitle < b.record.localizedTitle
|
||||
}
|
||||
|
||||
errorsByGroup[group] = sortedErrors
|
||||
}
|
||||
|
||||
let sortedErrors = errorsByGroup.sorted { (a, b) in
|
||||
let groupA = a.key
|
||||
let groupB = b.key
|
||||
|
||||
2
External/Harmony
vendored
2
External/Harmony
vendored
@ -1 +1 @@
|
||||
Subproject commit 2cfca813e9e4d0ecbc824050ab0336cb9b7c6b37
|
||||
Subproject commit 0301f3ea5d3ede4f8eb3c06227a4f63b9b7b247c
|
||||
2
External/Roxas
vendored
2
External/Roxas
vendored
@ -1 +1 @@
|
||||
Subproject commit 1945d97204d113c635ec959f7777e7200d40cdee
|
||||
Subproject commit 8ab1eecff273609896dff9bb18f17185ffa08f26
|
||||
Loading…
Reference in New Issue
Block a user