Merge branch 'feature/dropbox' into develop

This commit is contained in:
Riley Testut 2019-03-26 11:17:55 -07:00
commit 3e5ebc7c32
23 changed files with 814 additions and 240 deletions

@ -1 +1 @@
Subproject commit 2768bce21839a3ef7a7118d589a210ac2ca22cd6
Subproject commit 7ed452b7dff25a68cd503610bee1f3e36216a871

View File

@ -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 */;

View File

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

View File

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

View File

@ -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"/>

View File

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

View File

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

View 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
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()
}
}

View File

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

View File

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

View File

@ -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 &quot;OK&quot; 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>

View File

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

View File

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

@ -1 +1 @@
Subproject commit 2cfca813e9e4d0ecbc824050ab0336cb9b7c6b37
Subproject commit 0301f3ea5d3ede4f8eb3c06227a4f63b9b7b247c

2
External/Roxas vendored

@ -1 +1 @@
Subproject commit 1945d97204d113c635ec959f7777e7200d40cdee
Subproject commit 8ab1eecff273609896dff9bb18f17185ffa08f26