Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4926a6626 | ||
|
|
52d9e97f64 | ||
|
|
9963bb84b4 | ||
|
|
ec336c39db | ||
|
|
1d37a1dae8 | ||
|
|
066993db67 | ||
|
|
ed5f0174d7 | ||
|
|
adb4d21f99 | ||
|
|
9dd4d01b66 | ||
|
|
5f2de79221 | ||
|
|
7ea9369356 | ||
|
|
995dd6afdf | ||
|
|
c5333ad118 | ||
|
|
47b96d1cf6 | ||
|
|
3e97e5c951 | ||
|
|
aea0527828 | ||
|
|
a79924d340 | ||
|
|
5ffccbb884 | ||
|
|
41bb827d7b | ||
|
|
a87fea63ad | ||
|
|
4b159603a1 | ||
|
|
a8b9bd1092 | ||
|
|
cc12806273 | ||
|
|
594ff25c5c | ||
|
|
aa5d53097f | ||
|
|
7f79e1d3a6 | ||
|
|
17f8f95727 | ||
|
|
3b208465e0 | ||
|
|
7806c85039 | ||
|
|
cfa31678ea | ||
|
|
c293c50965 | ||
|
|
785136a663 | ||
|
|
0d27892c8d | ||
|
|
3e216515b7 | ||
|
|
34afd64ae0 |
62
CONTRIBUTING.md
Normal file
62
CONTRIBUTING.md
Normal file
@ -0,0 +1,62 @@
|
||||
Thank you for your interest in contributing to Delta! Delta is an open-source project that aims to provide a seamless and enjoyable experience for playing classic video games on iOS devices. We welcome contributions from anyone who shares our vision and passion for emulation.
|
||||
|
||||
# How to Contribute
|
||||
|
||||
We are currently accepting these types of contributions for Delta:
|
||||
|
||||
- Experimental Features
|
||||
- Bug fixes
|
||||
|
||||
All features added by third-party contributors will be considered *experimental* at first, and are disabled by default unless specifically enabled by the user from the Experimental Features section in Delta's settings. Experimental Features are only available in the beta version of Delta, but once a feature has been sufficiently tested we may choose to "graduate" it into an official Delta feature, at which point it will become available to all users.
|
||||
|
||||
For more specific instructions regarding contributing features to Delta, see [ExperimentalFeatures.md](Docs/ExperimentalFeatures.md).
|
||||
|
||||
## Contribution Guidelines
|
||||
|
||||
**Check out our project board first!**
|
||||
We have categorized issues and pull requests to highlight areas where help is most needed. You are welcome to contribute something new, but keep in mind that we may focus on more highly-requested items first.
|
||||
|
||||
**Keep changes small**
|
||||
The smaller a pull request is the more likely it will be merged. Make sure your PRs are limited to just the relevant changes and nothing else. Avoid pure formatting changes to code that has not been modified otherwise.
|
||||
|
||||
**Make sure contributions are 100% complete**
|
||||
We can't accept unfinished pull requests, so please make sure your contribution is ready to be merged as-is.
|
||||
|
||||
**Extensively test your changes**
|
||||
Make sure your changes work as expected on different devices and iOS versions, and doesn't result in additional bugs.
|
||||
|
||||
**Commit only relevant changes of changed files**
|
||||
Some files, such as Storyboards and Xcode projects, often contain changes that are unrelated to your specific change. **You should almost never commit an entire Storyboard or Xcode project as-is.**
|
||||
|
||||
**Not all contributions will be accepted**
|
||||
To maximize your chances of getting your pull request accepted, make sure to read all the guidelines carefully and familiarize yourself with the project structure, coding style, and best practices.
|
||||
|
||||
## Code Style and Conventions
|
||||
|
||||
**Please make sure your code follows these guidelines and is free of compiler warnings before submitting a pull request.**
|
||||
|
||||
* All contributions should be pure Swift, no C++ or Objective-C (unless absolutely necessary)
|
||||
|
||||
* Follow the [Swift API Design Guidelines](https://swift.org/documentation/api-design-guidelines/) for writing clean, consistent, and expressive Swift code
|
||||
|
||||
* Use Allman (a.k.a. brackets on new line) brace style (exceptions: closures and single-line statements)
|
||||
|
||||
* Use 4 spaces for indentation. Do not use tabs or mix tabs and spaces.
|
||||
|
||||
* Use whitespace liberally to make code easier to read
|
||||
|
||||
* Use descriptive variable names
|
||||
|
||||
* Lean on the side of commenting (more comments make it more likely to be approved)
|
||||
|
||||
* Prefer structs over classes. Classes should be used only if you need a reference type, or to interoperate with ObjC.
|
||||
|
||||
When editing existing code, please preserve the original formatting of the code as much as possible (e.g. brace style). Do not make unnecessary changes to whitespace, indentation, line breaks or comments. This helps keep the diffs clean and easy to review.
|
||||
|
||||
For example, when editing code that uses the Allman brace style (a.k.a. brackets on new line), make sure any `if`/`else` statements you write also place the bracket on a new line.
|
||||
|
||||
## Submitting Pull Requests
|
||||
|
||||
All pull requests must adhere to the PR template, filling out each section as appropriate. **Only PRs that follow this template will be accepted.**
|
||||
|
||||
Once you've submitted your PR, we will review it and provide feedback as soon as we can.
|
||||
@ -1 +1 @@
|
||||
Subproject commit cdd384dbacd5033183bbc3697c9738e3fb0b1d07
|
||||
Subproject commit 74d2a7a6e36035cb5730d0b0cf2456cbeb6faf0c
|
||||
@ -1 +1 @@
|
||||
Subproject commit 18c595887a12ef23e0d54c63f83c91c99e7f4827
|
||||
Subproject commit efb470d7af8de4342b908dc37a8ce7ac5e24c3e8
|
||||
@ -1 +1 @@
|
||||
Subproject commit c8816c51f82210a9c4cc62b1a7c53fa21bc705ee
|
||||
Subproject commit b0f62a51b65104d782b91b21b57fea4b94a33d68
|
||||
@ -1 +1 @@
|
||||
Subproject commit bc3e0178caa29b4c1e8872133dd00aa55cc9da2a
|
||||
Subproject commit 7dae837acc0f5deb8a951d2427a4fb2887f030a0
|
||||
@ -1 +1 @@
|
||||
Subproject commit d5717291325578f64d519822aeb2be81217c67f3
|
||||
Subproject commit 3a4c916fc88404d8ace5546c9aae2cdba9e8a717
|
||||
@ -188,9 +188,12 @@
|
||||
D54A4BB329E4D27E004C7D57 /* FeatureDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54A4BB229E4D27E004C7D57 /* FeatureDetailView.swift */; };
|
||||
D54F710229E89DCB009C069A /* SettingsUserInfoKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54F710129E89DCB009C069A /* SettingsUserInfoKey.swift */; };
|
||||
D54F710429E89DFC009C069A /* NotificationName+Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54F710329E89DFC009C069A /* NotificationName+Settings.swift */; };
|
||||
D55917D82B51CF7D007B7DC0 /* ProcessInfo+visionOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55917D72B51CF7D007B7DC0 /* ProcessInfo+visionOS.swift */; };
|
||||
D55917DA2B51D097007B7DC0 /* LocalDeviceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55917D92B51D097007B7DC0 /* LocalDeviceController.swift */; };
|
||||
D55C468F29E761C000EA6DE9 /* AnyFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55C468E29E761C000EA6DE9 /* AnyFeature.swift */; };
|
||||
D55C469129E7631000EA6DE9 /* AnyOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55C469029E7631000EA6DE9 /* AnyOption.swift */; };
|
||||
D560BD8629EDC45600289847 /* ExternalDisplaySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D560BD8529EDC45600289847 /* ExternalDisplaySceneDelegate.swift */; };
|
||||
D56F7ABC2B05988700490ACB /* AttributedHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D56F7ABB2B05988700490ACB /* AttributedHeaderFooterView.swift */; };
|
||||
D57D795629F300E100BB2CF8 /* CustomTintColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A817AF29DF4E6E00904AFE /* CustomTintColor.swift */; };
|
||||
D57D795F29F315F700BB2CF8 /* FeatureDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54A4BB229E4D27E004C7D57 /* FeatureDetailView.swift */; };
|
||||
D57D796029F315F700BB2CF8 /* ExperimentalFeaturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9C00229DDED6D00A8D610 /* ExperimentalFeaturesView.swift */; };
|
||||
@ -209,6 +212,7 @@
|
||||
D5CDCCF12A859E7500E22131 /* ReviewSaveStatesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A1375A2A7D8F2600AB1372 /* ReviewSaveStatesViewController.swift */; };
|
||||
D5CDCCF22A859E7500E22131 /* RepairDatabaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A137442A7D814000AB1372 /* RepairDatabaseViewController.swift */; };
|
||||
D5CDCCF32A859E7500E22131 /* GamePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A137662A7DB37200AB1372 /* GamePickerViewController.swift */; };
|
||||
D5D39E792AF2D624004BE3F7 /* GameView+AirPlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D39E782AF2D624004BE3F7 /* GameView+AirPlay.swift */; };
|
||||
D5D78AE529F9BC3700E064F0 /* DSAirPlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D78AE429F9BC3700E064F0 /* DSAirPlay.swift */; };
|
||||
D5D78AE729F9D40A00E064F0 /* EnvironmentValues+FeatureOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D78AE629F9D40A00E064F0 /* EnvironmentValues+FeatureOption.swift */; };
|
||||
D5D797E6298D946200738869 /* Contributor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D797E5298D946200738869 /* Contributor.swift */; };
|
||||
@ -460,9 +464,12 @@
|
||||
D54F710129E89DCB009C069A /* SettingsUserInfoKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsUserInfoKey.swift; sourceTree = "<group>"; };
|
||||
D54F710329E89DFC009C069A /* NotificationName+Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationName+Settings.swift"; sourceTree = "<group>"; };
|
||||
D554C3822A58D89700E93359 /* Delta 7.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Delta 7.xcdatamodel"; sourceTree = "<group>"; };
|
||||
D55917D72B51CF7D007B7DC0 /* ProcessInfo+visionOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+visionOS.swift"; sourceTree = "<group>"; };
|
||||
D55917D92B51D097007B7DC0 /* LocalDeviceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalDeviceController.swift; sourceTree = "<group>"; };
|
||||
D55C468E29E761C000EA6DE9 /* AnyFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyFeature.swift; sourceTree = "<group>"; };
|
||||
D55C469029E7631000EA6DE9 /* AnyOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyOption.swift; sourceTree = "<group>"; };
|
||||
D560BD8529EDC45600289847 /* ExternalDisplaySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalDisplaySceneDelegate.swift; sourceTree = "<group>"; };
|
||||
D56F7ABB2B05988700490ACB /* AttributedHeaderFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedHeaderFooterView.swift; sourceTree = "<group>"; };
|
||||
D586496F297734280081477E /* CheatMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatMetadata.swift; sourceTree = "<group>"; };
|
||||
D586497129774ABD0081477E /* CheatBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatBase.swift; sourceTree = "<group>"; };
|
||||
D5864977297756CE0081477E /* CheatBaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatBaseView.swift; sourceTree = "<group>"; };
|
||||
@ -484,6 +491,7 @@
|
||||
D5CDCCC32A85765900E22131 /* OSLog+Delta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSLog+Delta.swift"; sourceTree = "<group>"; };
|
||||
D5CDCCEA2A8593FC00E22131 /* UserDefaults+Delta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Delta.swift"; sourceTree = "<group>"; };
|
||||
D5CDCCEC2A859B2B00E22131 /* SyncValidationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncValidationError.swift; sourceTree = "<group>"; };
|
||||
D5D39E782AF2D624004BE3F7 /* GameView+AirPlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GameView+AirPlay.swift"; sourceTree = "<group>"; };
|
||||
D5D78AE429F9BC3700E064F0 /* DSAirPlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DSAirPlay.swift; sourceTree = "<group>"; };
|
||||
D5D78AE629F9D40A00E064F0 /* EnvironmentValues+FeatureOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+FeatureOption.swift"; sourceTree = "<group>"; };
|
||||
D5D797E5298D946200738869 /* Contributor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contributor.swift; sourceTree = "<group>"; };
|
||||
@ -566,6 +574,8 @@
|
||||
D5CDCCC32A85765900E22131 /* OSLog+Delta.swift */,
|
||||
D5CDCCEA2A8593FC00E22131 /* UserDefaults+Delta.swift */,
|
||||
AC1AE30B2A6A068F00956EB9 /* Bundle+AppIconImage.swift */,
|
||||
D5D39E782AF2D624004BE3F7 /* GameView+AirPlay.swift */,
|
||||
D55917D72B51CF7D007B7DC0 /* ProcessInfo+visionOS.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
@ -576,6 +586,7 @@
|
||||
BF11734F1DA32CF600047DF8 /* ControllersSettingsViewController.swift */,
|
||||
BF616A121F08184A0077F8B2 /* ControllerInputsViewController.swift */,
|
||||
BF8DDD231F4F6C880088A21B /* InputCalloutView.swift */,
|
||||
D55917D92B51D097007B7DC0 /* LocalDeviceController.swift */,
|
||||
);
|
||||
path = Controllers;
|
||||
sourceTree = "<group>";
|
||||
@ -784,6 +795,7 @@
|
||||
BFFA4C081E8A24D600D87934 /* GameTableViewCell.swift */,
|
||||
BF71CF891FE904B1001F1613 /* GameTableViewCell.xib */,
|
||||
BF8A333321A484A000A42FD4 /* BadgedTableViewCell.swift */,
|
||||
D56F7ABB2B05988700490ACB /* AttributedHeaderFooterView.swift */,
|
||||
);
|
||||
path = "Table View";
|
||||
sourceTree = "<group>";
|
||||
@ -1547,6 +1559,7 @@
|
||||
BF59427C1E09BC830051894B /* Cheat.swift in Sources */,
|
||||
BFBAB2E31EB685A2004E0B0E /* DeltaCoreProtocol+Delta.swift in Sources */,
|
||||
D5CDCCF22A859E7500E22131 /* RepairDatabaseViewController.swift in Sources */,
|
||||
D55917D82B51CF7D007B7DC0 /* ProcessInfo+visionOS.swift in Sources */,
|
||||
BF3540081C5DAFAD00C1184C /* PauseTransitionCoordinator.swift in Sources */,
|
||||
BF525EEA1FF6CD12004AA849 /* DeepLink.swift in Sources */,
|
||||
AC1C991029F8B8C30020E6E4 /* ToastNotificationOptions.swift in Sources */,
|
||||
@ -1660,8 +1673,10 @@
|
||||
BF6866171DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift in Sources */,
|
||||
BF713C0822499ED4004A1A2B /* PreviousHarmony.xcdatamodeld in Sources */,
|
||||
BF59427D1E09BC830051894B /* ControllerSkin.swift in Sources */,
|
||||
D56F7ABC2B05988700490ACB /* AttributedHeaderFooterView.swift in Sources */,
|
||||
BFAB9F7D219A43380080EC7D /* SyncManager.swift in Sources */,
|
||||
BFCEA67E1D56FF640061A534 /* UIViewControllerContextTransitioning+Conveniences.swift in Sources */,
|
||||
D55917DA2B51D097007B7DC0 /* LocalDeviceController.swift in Sources */,
|
||||
BF1173501DA32CF600047DF8 /* ControllersSettingsViewController.swift in Sources */,
|
||||
BFFC461E1D59823500AF2CC6 /* GamesPresentationController.swift in Sources */,
|
||||
BF99A5971DC2F9C400468E9E /* ControllerSkinTableViewCell.swift in Sources */,
|
||||
@ -1669,6 +1684,7 @@
|
||||
BF5942861E09BC8B0051894B /* _Cheat.swift in Sources */,
|
||||
BF5E7F441B9A650B00AE44F8 /* SettingsViewController.swift in Sources */,
|
||||
BF8CA9361F5F651900499FDD /* PopoverMenuController.swift in Sources */,
|
||||
D5D39E792AF2D624004BE3F7 /* GameView+AirPlay.swift in Sources */,
|
||||
BFEF24F31F7DD4FD00454C62 /* SaveStateMigrationPolicy.swift in Sources */,
|
||||
BF5942931E09BD1A0051894B /* NSFetchedResultsController+Conveniences.m in Sources */,
|
||||
D5CDCCEE2A859DC200E22131 /* SyncValidationError.swift in Sources */,
|
||||
@ -1933,18 +1949,18 @@
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 74;
|
||||
CURRENT_PROJECT_VERSION = 85;
|
||||
DEVELOPMENT_TEAM = 6XVY5G3U44;
|
||||
INFOPLIST_FILE = "Delta/Supporting Files/Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.5b4;
|
||||
OTHER_LDFLAGS = (
|
||||
"-ld64",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 1.5;
|
||||
OTHER_LDFLAGS = (
|
||||
"-ld64",
|
||||
"$(inherited)",
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DDEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.Delta;
|
||||
PROVISIONING_PROFILE = "";
|
||||
@ -1971,18 +1987,18 @@
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 74;
|
||||
CURRENT_PROJECT_VERSION = 85;
|
||||
DEVELOPMENT_TEAM = 6XVY5G3U44;
|
||||
INFOPLIST_FILE = "Delta/Supporting Files/Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.5b4;
|
||||
OTHER_LDFLAGS = (
|
||||
"-ld64",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 1.5;
|
||||
OTHER_LDFLAGS = (
|
||||
"-ld64",
|
||||
"$(inherited)",
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DIMPACTOR";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.Delta;
|
||||
PROVISIONING_PROFILE = "";
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" 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="22155" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="ssH-mM-uG6">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22131"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
@ -19,7 +19,7 @@
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="separatorColor" systemColor="separatorColor"/>
|
||||
<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="2160" width="375" height="44"/>
|
||||
<rect key="frame" x="0.0" y="2204" 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"/>
|
||||
@ -36,15 +36,15 @@
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Player 1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="tls-Hv-Rx2">
|
||||
<rect key="frame" x="16" y="13" width="56" height="19.5"/>
|
||||
<rect key="frame" x="16" y="12" width="58.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Riley's iPhone" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="vJP-Ie-a9H">
|
||||
<rect key="frame" x="207.5" y="13" width="101" height="19.5"/>
|
||||
<rect key="frame" x="202.5" y="12" width="106" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
@ -59,15 +59,15 @@
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Player 2" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="e3u-x9-IEC">
|
||||
<rect key="frame" x="16" y="13" width="58" height="19.5"/>
|
||||
<rect key="frame" x="16" y="12" width="61" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="SteelSeries Stratus" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="2OP-A1-VYo">
|
||||
<rect key="frame" x="170" y="13" width="138.5" height="19.5"/>
|
||||
<rect key="frame" x="163.5" y="12" width="145" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
@ -82,15 +82,15 @@
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Player 3" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Cdn-11-xZe">
|
||||
<rect key="frame" x="16" y="13" width="58.5" height="19.5"/>
|
||||
<rect key="frame" x="16" y="12" width="61.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="MOGA Gamepad" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="wWc-NY-Bsd">
|
||||
<rect key="frame" x="186.5" y="13" width="122" height="19.5"/>
|
||||
<rect key="frame" x="180" y="12" width="128.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
@ -105,15 +105,15 @@
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Player 4" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Hls-3b-EaS">
|
||||
<rect key="frame" x="16" y="13" width="59" height="19.5"/>
|
||||
<rect key="frame" x="16" y="12" width="62" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Jayce's iPhone" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="hNf-uc-PLR">
|
||||
<rect key="frame" x="200" y="13" width="108.5" height="19.5"/>
|
||||
<rect key="frame" x="194.5" y="12" width="114" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
@ -386,15 +386,15 @@
|
||||
<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="13" width="54.5" height="19.5"/>
|
||||
<rect key="frame" x="16" y="12" width="57" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Google Drive" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="kLY-5g-v8n">
|
||||
<rect key="frame" x="214.5" y="13" width="94" height="19.5"/>
|
||||
<rect key="frame" x="210" y="12" width="98.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
@ -505,11 +505,38 @@
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Advanced" footerTitle="Test out new features that have been added by contributors." id="EzW-IN-YAf" userLabel="Experimental Features">
|
||||
<tableViewSection headerTitle="Advanced" footerTitle="Test out new features that have been added by contributors." id="EzW-IN-YAf">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Cell" textLabel="Hl1-if-S1K" style="IBUITableViewCellStyleDefault" id="8fe-ab-zkf">
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="gray" indentationWidth="10" reuseIdentifier="ActivityCell" id="0bj-he-6br">
|
||||
<rect key="frame" x="16" y="1570.5" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="0bj-he-6br" id="oIP-D5-Xrm">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Export Error Log…" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XPB-zq-ssX">
|
||||
<rect key="frame" x="16" y="11.5" width="138" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" name="Purple"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" misplaced="YES" hidesWhenStopped="YES" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="x5w-dE-ucW">
|
||||
<rect key="frame" x="307" y="12" width="20" height="21"/>
|
||||
<color key="color" name="Purple"/>
|
||||
</activityIndicatorView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="x5w-dE-ucW" firstAttribute="centerY" secondItem="oIP-D5-Xrm" secondAttribute="centerY" id="2Du-al-MgG"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="x5w-dE-ucW" secondAttribute="trailing" id="7ek-gm-AM9"/>
|
||||
<constraint firstItem="XPB-zq-ssX" firstAttribute="centerY" secondItem="oIP-D5-Xrm" secondAttribute="centerY" id="HsH-e7-iRR"/>
|
||||
<constraint firstItem="XPB-zq-ssX" firstAttribute="leading" secondItem="oIP-D5-Xrm" secondAttribute="leadingMargin" id="Ulm-U3-OcK"/>
|
||||
<constraint firstItem="x5w-dE-ucW" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="XPB-zq-ssX" secondAttribute="trailing" constant="8" symbolic="YES" id="vfa-y3-UZc"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Cell" textLabel="Hl1-if-S1K" style="IBUITableViewCellStyleDefault" id="8fe-ab-zkf">
|
||||
<rect key="frame" x="16" y="1614.5" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="8fe-ab-zkf" id="Py0-GQ-Z36">
|
||||
<rect key="frame" x="0.0" y="0.0" width="316.5" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
@ -528,7 +555,7 @@
|
||||
<tableViewSection headerTitle="Patreon" footerTitle="Receive early access to new features and more by becoming a patron." id="QvT-Yt-oP1">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Cell" textLabel="faT-qa-JP0" style="IBUITableViewCellStyleDefault" id="4it-3L-j8P">
|
||||
<rect key="frame" x="16" y="1706" width="343" height="44"/>
|
||||
<rect key="frame" x="16" y="1750" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="4it-3L-j8P" id="7dE-36-hzp">
|
||||
<rect key="frame" x="0.0" y="0.0" width="316.5" height="44"/>
|
||||
@ -548,7 +575,7 @@
|
||||
<tableViewSection headerTitle="Credits" id="foh-L9-g6W">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="DetailCell" textLabel="Pum-dL-hGn" detailTextLabel="WQ6-m7-zhh" style="IBUITableViewCellStyleValue1" id="BU4-ee-DGz">
|
||||
<rect key="frame" x="16" y="1834" width="343" height="44"/>
|
||||
<rect key="frame" x="16" y="1878" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="BU4-ee-DGz" id="fWf-gm-1sf">
|
||||
<rect key="frame" x="0.0" y="0.0" width="316.5" height="44"/>
|
||||
@ -571,7 +598,7 @@
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="DetailCell" textLabel="cht-lO-kpR" detailTextLabel="0pG-CT-ZWR" style="IBUITableViewCellStyleValue1" id="CV9-Df-mUX">
|
||||
<rect key="frame" x="16" y="1878" width="343" height="44"/>
|
||||
<rect key="frame" x="16" y="1922" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="CV9-Df-mUX" id="gLC-z2-rMU">
|
||||
<rect key="frame" x="0.0" y="0.0" width="316.5" height="44"/>
|
||||
@ -594,7 +621,7 @@
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="DetailCell" textLabel="gWx-Xn-5Nf" detailTextLabel="09x-GX-cpy" style="IBUITableViewCellStyleValue1" id="8qc-0t-Nte">
|
||||
<rect key="frame" x="16" y="1922" width="343" height="44"/>
|
||||
<rect key="frame" x="16" y="1966" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="8qc-0t-Nte" id="jUL-fL-i0n">
|
||||
<rect key="frame" x="0.0" y="0.0" width="316.5" height="44"/>
|
||||
@ -617,7 +644,7 @@
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="DetailCell" textLabel="zro-BX-EY9" detailTextLabel="e45-FD-ug2" style="IBUITableViewCellStyleValue1" id="Zh9-JJ-jEQ">
|
||||
<rect key="frame" x="16" y="1966" width="343" height="44"/>
|
||||
<rect key="frame" x="16" y="2010" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Zh9-JJ-jEQ" id="VCc-oJ-ODB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="316.5" height="44"/>
|
||||
@ -640,7 +667,7 @@
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="DetailCell" textLabel="w1i-mR-wOF" detailTextLabel="jRO-48-iRO" style="IBUITableViewCellStyleValue1" id="rrX-Bh-zdW">
|
||||
<rect key="frame" x="16" y="2010" width="343" height="44"/>
|
||||
<rect key="frame" x="16" y="2054" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="rrX-Bh-zdW" id="b9U-W6-LnS">
|
||||
<rect key="frame" x="0.0" y="0.0" width="316.5" height="44"/>
|
||||
@ -663,7 +690,7 @@
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="DetailCell" textLabel="g59-8E-zW7" style="IBUITableViewCellStyleDefault" id="hkv-lx-68h">
|
||||
<rect key="frame" x="16" y="2054" width="343" height="44"/>
|
||||
<rect key="frame" x="16" y="2098" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="hkv-lx-68h" id="bNT-kB-3cI">
|
||||
<rect key="frame" x="0.0" y="0.0" width="316.5" height="44"/>
|
||||
@ -679,7 +706,7 @@
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="DetailCell" textLabel="2K3-IL-94S" style="IBUITableViewCellStyleDefault" id="j7p-ZK-mHq">
|
||||
<rect key="frame" x="16" y="2098" width="343" height="44"/>
|
||||
<rect key="frame" x="16" y="2142" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="j7p-ZK-mHq" id="BqT-yP-OpS">
|
||||
<rect key="frame" x="0.0" y="0.0" width="316.5" height="44"/>
|
||||
@ -717,6 +744,7 @@
|
||||
<outlet property="buttonHapticFeedbackEnabledSwitch" destination="g7m-wj-ueP" id="nOj-sc-foi"/>
|
||||
<outlet property="controllerOpacityLabel" destination="zaz-yD-CYG" id="eUW-u9-xxx"/>
|
||||
<outlet property="controllerOpacitySlider" destination="whi-If-wFf" id="6Cx-HY-xLG"/>
|
||||
<outlet property="exportLogActivityIndicatorView" destination="x5w-dE-ucW" id="u5a-K3-2ag"/>
|
||||
<outlet property="previewsEnabledSwitch" destination="OJE-9e-9i3" id="Ndg-eN-PPs"/>
|
||||
<outlet property="respectSilentModeSwitch" destination="mlO-iy-zU2" id="TDT-cx-kCf"/>
|
||||
<outlet property="syncingServiceLabel" destination="kLY-5g-v8n" id="zzx-qM-q1g"/>
|
||||
@ -828,15 +856,15 @@
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Controller Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="VBO-V1-Wfu">
|
||||
<rect key="frame" x="16" y="13" width="118.5" height="19.5"/>
|
||||
<rect key="frame" x="16" y="12" width="124.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Player 1" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="tqn-1q-p53">
|
||||
<rect key="frame" x="271" y="13" width="56" height="19.5"/>
|
||||
<rect key="frame" x="268.5" y="12" width="58.5" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
|
||||
75
Delta/Components/Table View/AttributedHeaderFooterView.swift
Normal file
75
Delta/Components/Table View/AttributedHeaderFooterView.swift
Normal file
@ -0,0 +1,75 @@
|
||||
//
|
||||
// AttributedHeaderFooterView.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 11/15/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@available(iOS 15, *)
|
||||
final class AttributedHeaderFooterView: UITableViewHeaderFooterView
|
||||
{
|
||||
static let reuseIdentifier: String = "TextViewHeaderFooterView"
|
||||
|
||||
var attributedText: AttributedString? {
|
||||
get {
|
||||
guard let attributedText = self.textView.attributedText else { return nil }
|
||||
return AttributedString(attributedText)
|
||||
}
|
||||
set {
|
||||
guard var attributedText = newValue else {
|
||||
self.textView.attributedText = nil
|
||||
return
|
||||
}
|
||||
|
||||
var attributes = AttributeContainer()
|
||||
attributes.foregroundColor = UIColor.secondaryLabel
|
||||
attributes.font = self.textLabel?.font ?? UIFont.preferredFont(forTextStyle: .footnote)
|
||||
|
||||
attributedText.mergeAttributes(attributes, mergePolicy: .keepCurrent)
|
||||
self.textView.attributedText = NSAttributedString(attributedText)
|
||||
}
|
||||
}
|
||||
|
||||
private let textView: UITextView
|
||||
|
||||
override init(reuseIdentifier: String?)
|
||||
{
|
||||
self.textView = UITextView(frame: .zero)
|
||||
self.textView.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.textView.textContainer.lineFragmentPadding = 0
|
||||
self.textView.textContainerInset = .zero
|
||||
self.textView.isSelectable = true // Must be true to open links
|
||||
self.textView.isEditable = false
|
||||
self.textView.isScrollEnabled = false
|
||||
self.textView.backgroundColor = nil
|
||||
self.textView.textContainer.lineBreakMode = .byWordWrapping
|
||||
|
||||
super.init(reuseIdentifier: reuseIdentifier)
|
||||
|
||||
self.textView.delegate = self
|
||||
self.contentView.addSubview(self.textView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
self.textView.topAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.topAnchor),
|
||||
self.textView.bottomAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.bottomAnchor),
|
||||
self.textView.leadingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.leadingAnchor),
|
||||
self.textView.trailingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.trailingAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 15, *)
|
||||
extension AttributedHeaderFooterView: UITextViewDelegate
|
||||
{
|
||||
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool
|
||||
{
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,7 @@ import Foundation
|
||||
import GBCDeltaCore
|
||||
|
||||
import Harmony
|
||||
import Roxas
|
||||
|
||||
@objc(GameSave)
|
||||
public class GameSave: _GameSave
|
||||
@ -113,4 +114,33 @@ extension GameSave: Syncable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func resolveConflict(_ record: AnyRecord) -> ConflictResolution
|
||||
{
|
||||
// Only attempt to resolve conflicts for older GameSaves without SHA1 hash (i.e. pre-Delta 1.5)
|
||||
guard let game = self.game, self.sha1 == nil else { return .conflict }
|
||||
|
||||
do
|
||||
{
|
||||
let sha1Hash = try RSTHasher.sha1HashOfFile(at: game.gameSaveURL)
|
||||
|
||||
// resolveConflict() is called from self.managedObjectContext, so we can update `self` directly
|
||||
// and it will be automatically saved once finished conflicting records.
|
||||
self.sha1 = sha1Hash
|
||||
|
||||
// Don't update localRecord's hash here or else GameSave won't be repaired during initial sync.
|
||||
// try localRecord.updateSHA1Hash()
|
||||
}
|
||||
catch CocoaError.fileNoSuchFile
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger.sync.error("Failed to update GameSave SHA1 hash when resolving conflict. \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
|
||||
// Conflict for now, but we'll "repair" this record to hopefully resolve conflict.
|
||||
return .conflict
|
||||
}
|
||||
}
|
||||
|
||||
@ -711,12 +711,24 @@ private extension GameViewController
|
||||
{
|
||||
// AirPlaying, hide all (non-touch) screens.
|
||||
|
||||
if let traits = self.controllerView.controllerSkinTraits, let screens = self.controllerView.controllerSkin?.screens(for: traits)
|
||||
if let traits = self.controllerView.controllerSkinTraits,
|
||||
let supportedTraits = self.controllerView.controllerSkin?.supportedTraits(for: traits),
|
||||
let screens = self.controllerView.controllerSkin?.screens(for: supportedTraits)
|
||||
{
|
||||
for (screen, gameView) in zip(screens, self.gameViews)
|
||||
{
|
||||
gameView.isEnabled = screen.isTouchScreen
|
||||
gameView.isHidden = !screen.isTouchScreen
|
||||
|
||||
if gameView == self.gameView
|
||||
{
|
||||
// Always show AirPlay indicator on self.gameView
|
||||
gameView.isAirPlaying = true
|
||||
gameView.isHidden = false
|
||||
}
|
||||
else
|
||||
{
|
||||
gameView.isHidden = !screen.isTouchScreen
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -725,7 +737,8 @@ private extension GameViewController
|
||||
// Most likely this system only has 1 screen, so just hide self.gameView.
|
||||
|
||||
self.gameView.isEnabled = false
|
||||
self.gameView.isHidden = true
|
||||
self.gameView.isHidden = false
|
||||
self.gameView.isAirPlaying = true
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -736,6 +749,7 @@ private extension GameViewController
|
||||
{
|
||||
gameView.isEnabled = true
|
||||
gameView.isHidden = false
|
||||
gameView.isAirPlaying = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1252,6 +1266,8 @@ private extension GameViewController
|
||||
|
||||
// Implicitly called from updateControllerSkin()
|
||||
// self.updateExternalDisplay()
|
||||
|
||||
self.gameView?.isAirPlaying = true
|
||||
}
|
||||
|
||||
func updateExternalDisplay()
|
||||
@ -1322,6 +1338,8 @@ private extension GameViewController
|
||||
}
|
||||
|
||||
self.updateControllerSkin() // Reset TouchControllerSkin + GameViews
|
||||
|
||||
self.gameView?.isAirPlaying = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -41,6 +41,10 @@ struct ExperimentalFeatures: FeatureContainer
|
||||
options: ReviewSaveStatesOptions())
|
||||
var reviewSaveStates
|
||||
|
||||
@Feature(name: "Repair Database",
|
||||
description: "Repair invalid relationships in Delta's game database on next app launch.")
|
||||
var repairDatabase
|
||||
|
||||
@Feature(name: "Alternate App Icon",
|
||||
description: "Change the app icon.",
|
||||
options: AlternateAppIconOptions())
|
||||
|
||||
77
Delta/Extensions/GameView+AirPlay.swift
Normal file
77
Delta/Extensions/GameView+AirPlay.swift
Normal file
@ -0,0 +1,77 @@
|
||||
//
|
||||
// GameView+AirPlay.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 11/1/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ObjectiveC.runtime
|
||||
|
||||
import DeltaCore
|
||||
import Roxas
|
||||
|
||||
private var airPlayViewKey = 0
|
||||
|
||||
extension GameView
|
||||
{
|
||||
var isAirPlaying: Bool {
|
||||
get { self.airPlayView != nil }
|
||||
set {
|
||||
guard newValue != self.isAirPlaying else { return }
|
||||
|
||||
if newValue
|
||||
{
|
||||
self.showAirPlayView()
|
||||
}
|
||||
else
|
||||
{
|
||||
self.hideAirPlayView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension GameView
|
||||
{
|
||||
weak var airPlayView: UIView? {
|
||||
get { objc_getAssociatedObject(self, &airPlayViewKey) as? UIView }
|
||||
set { objc_setAssociatedObject(self, &airPlayViewKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_ASSIGN) }
|
||||
}
|
||||
|
||||
func showAirPlayView()
|
||||
{
|
||||
guard self.airPlayView == nil else { return }
|
||||
|
||||
let placeholderView = RSTPlaceholderView(frame: .zero)
|
||||
placeholderView.backgroundColor = .black
|
||||
|
||||
placeholderView.textLabel.font = UIFont.preferredFont(forTextStyle: .headline)
|
||||
placeholderView.textLabel.text = NSLocalizedString("AirPlay", comment: "")
|
||||
placeholderView.textLabel.textColor = .systemGray
|
||||
|
||||
placeholderView.detailTextLabel.font = UIFont.preferredFont(forTextStyle: .subheadline)
|
||||
placeholderView.detailTextLabel.text = NSLocalizedString("This game is playing on a second screen.", comment: "")
|
||||
placeholderView.detailTextLabel.textColor = .systemGray
|
||||
|
||||
let config = UIImage.SymbolConfiguration(pointSize: 100)
|
||||
let airPlayIcon = UIImage(systemName: "tv", withConfiguration: config)
|
||||
placeholderView.imageView.image = airPlayIcon
|
||||
placeholderView.imageView.isHidden = false
|
||||
placeholderView.imageView.tintColor = .systemGray
|
||||
|
||||
self.addSubview(placeholderView, pinningEdgesWith: .zero)
|
||||
|
||||
self.airPlayView = placeholderView
|
||||
}
|
||||
|
||||
func hideAirPlayView()
|
||||
{
|
||||
guard let airPlayView else { return }
|
||||
|
||||
airPlayView.removeFromSuperview()
|
||||
|
||||
self.airPlayView = nil
|
||||
}
|
||||
}
|
||||
25
Delta/Extensions/ProcessInfo+visionOS.swift
Normal file
25
Delta/Extensions/ProcessInfo+visionOS.swift
Normal file
@ -0,0 +1,25 @@
|
||||
//
|
||||
// ProcessInfo+visionOS.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 1/12/24.
|
||||
// Copyright © 2024 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import LocalAuthentication
|
||||
|
||||
extension ProcessInfo
|
||||
{
|
||||
var isRunningOnVisionPro: Bool {
|
||||
// Returns true even when running on iOS :/
|
||||
// guard #available(visionOS 1, *) else { return false }
|
||||
// return true
|
||||
|
||||
let context = LAContext()
|
||||
_ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) // Sets .biometryType when called.
|
||||
|
||||
// Can't reference `.opticID` due to bug with #available, so check if .biometryType isn't one of the other types instead.
|
||||
return context.biometryType != .faceID && context.biometryType != .touchID && context.biometryType != .none
|
||||
}
|
||||
}
|
||||
@ -91,10 +91,17 @@ extension GamesViewController
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
let faqButton = UIButton(type: .system)
|
||||
faqButton.addTarget(self, action: #selector(GamesViewController.openFAQ), for: .primaryActionTriggered)
|
||||
faqButton.setTitle(NSLocalizedString("Learn More…", comment: ""), for: .normal)
|
||||
faqButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .title3)
|
||||
|
||||
self.placeholderView = RSTPlaceholderView(frame: self.view.bounds)
|
||||
self.placeholderView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
self.placeholderView.textLabel.text = NSLocalizedString("No Games", comment: "")
|
||||
self.placeholderView.detailTextLabel.text = NSLocalizedString("You can import games by pressing the + button in the top right.", comment: "")
|
||||
self.placeholderView.stackView.addArrangedSubview(faqButton)
|
||||
self.placeholderView.stackView.setCustomSpacing(20.0, after: self.placeholderView.detailTextLabel)
|
||||
self.view.insertSubview(self.placeholderView, at: 0)
|
||||
|
||||
self.pageControl = UIPageControl()
|
||||
@ -350,6 +357,7 @@ private extension GamesViewController
|
||||
if let viewController = self.viewControllerForIndex(index)
|
||||
{
|
||||
self.pageViewController.view.setHidden(false, animated: animated)
|
||||
self.pageViewController.view.superview?.setHidden(false, animated: animated)
|
||||
self.placeholderView.setHidden(true, animated: animated)
|
||||
|
||||
self.pageViewController.setViewControllers([viewController], direction: .forward, animated: false, completion: nil)
|
||||
@ -368,9 +376,16 @@ private extension GamesViewController
|
||||
self.title = NSLocalizedString("Games", comment: "")
|
||||
|
||||
self.pageViewController.view.setHidden(true, animated: animated)
|
||||
self.pageViewController.view.superview?.setHidden(true, animated: animated)
|
||||
self.placeholderView.setHidden(false, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func openFAQ()
|
||||
{
|
||||
let faqURL = URL(string: "https://faq.deltaemulator.com/getting-started/importing-games")!
|
||||
UIApplication.shared.open(faqURL)
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Importing -
|
||||
|
||||
@ -78,33 +78,12 @@ extension LaunchViewController
|
||||
|
||||
// Repair database _after_ starting SyncManager so we can access RecordController.
|
||||
let isDatabaseRepaired = RSTLaunchCondition(condition: { !UserDefaults.standard.shouldRepairDatabase }) { completionHandler in
|
||||
func finish()
|
||||
{
|
||||
UserDefaults.standard.shouldRepairDatabase = false
|
||||
completionHandler(nil)
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
let fetchRequest = Game.fetchRequest()
|
||||
fetchRequest.fetchLimit = 1
|
||||
|
||||
let isDatabaseEmpty = try DatabaseManager.shared.viewContext.count(for: fetchRequest) == 0
|
||||
guard !isDatabaseEmpty else {
|
||||
// Database has no games, so no need to repair database.
|
||||
finish()
|
||||
return
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to fetch games at launch, repairing database just to be safe.", error)
|
||||
}
|
||||
|
||||
let repairViewController = RepairDatabaseViewController()
|
||||
repairViewController.completionHandler = { [weak repairViewController] in
|
||||
repairViewController?.dismiss(animated: true)
|
||||
finish()
|
||||
|
||||
UserDefaults.standard.shouldRepairDatabase = false
|
||||
completionHandler(nil)
|
||||
}
|
||||
|
||||
let navigationController = UINavigationController(rootViewController: repairViewController)
|
||||
|
||||
@ -22,22 +22,6 @@ extension ControllersSettingsViewController
|
||||
}
|
||||
}
|
||||
|
||||
private class LocalDeviceController: NSObject, GameController
|
||||
{
|
||||
var name: String {
|
||||
return UIDevice.current.name
|
||||
}
|
||||
|
||||
var playerIndex: Int? {
|
||||
set { Settings.localControllerPlayerIndex = newValue }
|
||||
get { return Settings.localControllerPlayerIndex }
|
||||
}
|
||||
|
||||
let inputType: GameControllerInputType = .standard
|
||||
|
||||
var defaultInputMapping: GameControllerInputMappingProtocol?
|
||||
}
|
||||
|
||||
class ControllersSettingsViewController: UITableViewController
|
||||
{
|
||||
var playerIndex: Int! {
|
||||
|
||||
32
Delta/Settings/Controllers/LocalDeviceController.swift
Normal file
32
Delta/Settings/Controllers/LocalDeviceController.swift
Normal file
@ -0,0 +1,32 @@
|
||||
//
|
||||
// LocalDeviceController.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 1/12/24.
|
||||
// Copyright © 2024 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import DeltaCore
|
||||
|
||||
class LocalDeviceController: NSObject, GameController
|
||||
{
|
||||
var name: String {
|
||||
if ProcessInfo.processInfo.isRunningOnVisionPro
|
||||
{
|
||||
return NSLocalizedString("Touch", comment: "")
|
||||
}
|
||||
else
|
||||
{
|
||||
return NSLocalizedString("Touch Screen", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
var playerIndex: Int? {
|
||||
set { Settings.localControllerPlayerIndex = newValue }
|
||||
get { return Settings.localControllerPlayerIndex }
|
||||
}
|
||||
|
||||
let inputType: GameControllerInputType = .standard
|
||||
|
||||
var defaultInputMapping: GameControllerInputMappingProtocol?
|
||||
}
|
||||
@ -120,6 +120,11 @@ class MelonDSCoreSettingsViewController: UITableViewController
|
||||
self.navigationItem.rightBarButtonItem = nil
|
||||
}
|
||||
|
||||
if #available(iOS 15, *)
|
||||
{
|
||||
self.tableView.register(AttributedHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: AttributedHeaderFooterView.reuseIdentifier)
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(MelonDSCoreSettingsViewController.willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||
}
|
||||
|
||||
@ -235,7 +240,8 @@ private extension MelonDSCoreSettingsViewController
|
||||
|
||||
func changeCore()
|
||||
{
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Change Emulator Core", comment: ""), message: NSLocalizedString("Save states are not compatible between different emulator cores. Make sure to use in-game saves in order to keep using your save data.\n\nYour existing save states will not be deleted and will be available whenever you switch cores again.", comment: ""), preferredStyle: .actionSheet)
|
||||
let preferredStyle: UIAlertController.Style = (self.traitCollection.horizontalSizeClass == .compact) ? .actionSheet : .alert
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Change Emulator Core", comment: ""), message: NSLocalizedString("Save states are not compatible between different emulator cores. Make sure to use in-game saves in order to keep using your save data.\n\nYour existing save states will not be deleted and will be available whenever you switch cores again.", comment: ""), preferredStyle: preferredStyle)
|
||||
|
||||
var desmumeActionTitle = DS.core.metadata?.name.value ?? DS.core.name
|
||||
var melonDSActionTitle = MelonDS.core.metadata?.name.value ?? MelonDS.core.name
|
||||
@ -313,9 +319,17 @@ extension MelonDSCoreSettingsViewController
|
||||
switch section
|
||||
{
|
||||
case _ where isSectionHidden(section): return 0
|
||||
case .general:
|
||||
guard let core = Settings.preferredCore(for: .ds) else { break }
|
||||
|
||||
let validKeys = DeltaCoreMetadata.Key.allCases.filter { core.metadata?[$0] != nil }
|
||||
return validKeys.count
|
||||
|
||||
case .airPlay where Settings.features.dsAirPlay.topScreenOnly: return 1 // Layout axis is irrelevant if only AirPlaying top screen.
|
||||
default: return super.tableView(tableView, numberOfRowsInSection: sectionIndex)
|
||||
default: break
|
||||
}
|
||||
|
||||
return super.tableView(tableView, numberOfRowsInSection: sectionIndex)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
|
||||
@ -400,24 +414,6 @@ extension MelonDSCoreSettingsViewController
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)
|
||||
{
|
||||
guard let core = Settings.preferredCore(for: .ds) else { return }
|
||||
|
||||
let key = DeltaCoreMetadata.Key.allCases[indexPath.row]
|
||||
let lastKey = DeltaCoreMetadata.Key.allCases.reversed().first { core.metadata?[$0] != nil }
|
||||
|
||||
if key == lastKey
|
||||
{
|
||||
// Hide separator for last visible row in case we've hidden additional rows.
|
||||
cell.separatorInset.left = 0
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.separatorInset.left = self.view.layoutMargins.left
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
|
||||
{
|
||||
switch Section(rawValue: indexPath.section)!
|
||||
@ -469,22 +465,43 @@ extension MelonDSCoreSettingsViewController
|
||||
case (false, .horizontal): return NSLocalizedString("When AirPlaying DS games, both screens will be placed side-by-side on the external display.", comment: "")
|
||||
}
|
||||
|
||||
default: return super.tableView(tableView, titleForFooterInSection: section.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
|
||||
{
|
||||
switch Section(rawValue: indexPath.section)!
|
||||
{
|
||||
case .general:
|
||||
let key = DeltaCoreMetadata.Key.allCases[indexPath.row]
|
||||
guard Settings.preferredCore(for: .ds)?.metadata?[key] != nil else { return 0 }
|
||||
case .dsBIOS, .dsiBIOS:
|
||||
guard #available(iOS 15, *) else { break }
|
||||
return nil
|
||||
|
||||
default: break
|
||||
}
|
||||
|
||||
return super.tableView(tableView, heightForRowAt: indexPath)
|
||||
return super.tableView(tableView, titleForFooterInSection: section.rawValue)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView?
|
||||
{
|
||||
let section = Section(rawValue: section)!
|
||||
guard !isSectionHidden(section) else { return nil }
|
||||
|
||||
switch section
|
||||
{
|
||||
case .dsBIOS, .dsiBIOS:
|
||||
guard #available(iOS 15, *), let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: AttributedHeaderFooterView.reuseIdentifier) as? AttributedHeaderFooterView else { break }
|
||||
|
||||
let systemName = (section == .dsiBIOS) ? String(localized: "DSi") : String(localized: "DS")
|
||||
|
||||
var attributedText = AttributedString(localized: "Delta requires these BIOS files in order to play Nintendo \(systemName) games.")
|
||||
attributedText += " "
|
||||
|
||||
var learnMore = AttributedString(localized: "Learn more…")
|
||||
learnMore.link = URL(string: "https://faq.deltaemulator.com/getting-started/nintendo-ds-bios-files")
|
||||
attributedText += learnMore
|
||||
|
||||
footerView.attributedText = attributedText
|
||||
|
||||
return footerView
|
||||
|
||||
default: break
|
||||
}
|
||||
|
||||
return super.tableView(tableView, viewForFooterInSection: section.rawValue)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
|
||||
@ -504,14 +521,24 @@ extension MelonDSCoreSettingsViewController
|
||||
override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat
|
||||
{
|
||||
let section = Section(rawValue: section)!
|
||||
guard !isSectionHidden(section) else { return 1 }
|
||||
|
||||
if isSectionHidden(section)
|
||||
switch section
|
||||
{
|
||||
return 1
|
||||
case .dsBIOS, .dsiBIOS: return UITableView.automaticDimension
|
||||
default: return super.tableView(tableView, heightForFooterInSection: section.rawValue)
|
||||
}
|
||||
else
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, estimatedHeightForFooterInSection section: Int) -> CGFloat
|
||||
{
|
||||
let section = Section(rawValue: section)!
|
||||
guard !isSectionHidden(section) else { return 1 }
|
||||
|
||||
switch section
|
||||
{
|
||||
return super.tableView(tableView, heightForFooterInSection: section.rawValue)
|
||||
case .dsBIOS, .dsiBIOS: return 30
|
||||
default: return UITableView.automaticDimension
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,7 +53,7 @@ struct Settings
|
||||
|
||||
static func registerDefaults()
|
||||
{
|
||||
var defaults = [#keyPath(UserDefaults.translucentControllerSkinOpacity): 0.7,
|
||||
let defaults = [#keyPath(UserDefaults.translucentControllerSkinOpacity): 0.7,
|
||||
#keyPath(UserDefaults.gameShortcutsMode): GameShortcutsMode.recent.rawValue,
|
||||
#keyPath(UserDefaults.isButtonHapticFeedbackEnabled): true,
|
||||
#keyPath(UserDefaults.isThumbstickHapticFeedbackEnabled): true,
|
||||
@ -63,12 +63,7 @@ struct Settings
|
||||
#keyPath(UserDefaults.respectSilentMode): true,
|
||||
Settings.preferredCoreSettingsKey(for: .ds): MelonDS.core.identifier] as [String : Any]
|
||||
|
||||
#if BETA
|
||||
|
||||
// Assume we need to repair database relationships until explicitly set to false.
|
||||
defaults[#keyPath(UserDefaults.shouldRepairDatabase)] = true
|
||||
|
||||
#else
|
||||
#if !BETA
|
||||
// Manually set MelonDS as preferred DS core in case DeSmuME is cached from a previous version.
|
||||
UserDefaults.standard.set(MelonDS.core.identifier, forKey: Settings.preferredCoreSettingsKey(for: .ds))
|
||||
|
||||
@ -77,6 +72,12 @@ struct Settings
|
||||
#endif
|
||||
|
||||
UserDefaults.standard.register(defaults: defaults)
|
||||
|
||||
if ExperimentalFeatures.shared.repairDatabase.isEnabled
|
||||
{
|
||||
UserDefaults.standard.shouldRepairDatabase = true
|
||||
ExperimentalFeatures.shared.repairDatabase.isEnabled = false // Disable so we only repair database once.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,8 +8,10 @@
|
||||
|
||||
import UIKit
|
||||
import SafariServices
|
||||
import QuickLook
|
||||
|
||||
import DeltaCore
|
||||
import Harmony
|
||||
|
||||
import Roxas
|
||||
|
||||
@ -43,6 +45,12 @@ private extension SettingsViewController
|
||||
case status
|
||||
}
|
||||
|
||||
enum AdvancedRow: Int, CaseIterable
|
||||
{
|
||||
case exportLog
|
||||
case experimentalFeatures
|
||||
}
|
||||
|
||||
enum CreditsRow: Int, CaseIterable
|
||||
{
|
||||
case riley
|
||||
@ -68,6 +76,7 @@ class SettingsViewController: UITableViewController
|
||||
@IBOutlet private var versionLabel: UILabel!
|
||||
|
||||
@IBOutlet private var syncingServiceLabel: UILabel!
|
||||
@IBOutlet private var exportLogActivityIndicatorView: UIActivityIndicatorView!
|
||||
|
||||
private var selectionFeedbackGenerator: UISelectionFeedbackGenerator?
|
||||
|
||||
@ -75,6 +84,8 @@ class SettingsViewController: UITableViewController
|
||||
|
||||
private var syncingConflictsCount = 0
|
||||
|
||||
private var _exportedLogURL: URL?
|
||||
|
||||
required init?(coder aDecoder: NSCoder)
|
||||
{
|
||||
super.init(coder: aDecoder)
|
||||
@ -104,6 +115,11 @@ class SettingsViewController: UITableViewController
|
||||
self.versionLabel.text = NSLocalizedString("Delta", comment: "")
|
||||
#endif
|
||||
}
|
||||
|
||||
if #available(iOS 15, *)
|
||||
{
|
||||
self.tableView.register(AttributedHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: AttributedHeaderFooterView.reuseIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool)
|
||||
@ -196,6 +212,17 @@ private extension SettingsViewController
|
||||
{
|
||||
switch section
|
||||
{
|
||||
case .hapticFeedback where !UIDevice.current.isVibrationSupported: return true
|
||||
|
||||
case .advanced:
|
||||
guard #unavailable(iOS 15) else { return false }
|
||||
|
||||
#if BETA
|
||||
return false
|
||||
#else
|
||||
return true
|
||||
#endif
|
||||
|
||||
case .hapticTouch:
|
||||
if #available(iOS 13, *)
|
||||
{
|
||||
@ -294,6 +321,55 @@ private extension SettingsViewController
|
||||
let hostingController = ExperimentalFeaturesView.makeViewController()
|
||||
self.navigationController?.pushViewController(hostingController, animated: true)
|
||||
}
|
||||
|
||||
@available(iOS 15, *)
|
||||
func exportErrorLog()
|
||||
{
|
||||
self.exportLogActivityIndicatorView.startAnimating()
|
||||
|
||||
if let indexPath = self.tableView.indexPathForSelectedRow
|
||||
{
|
||||
self.tableView.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
|
||||
Task<Void, Never>.detached(priority: .userInitiated) {
|
||||
do
|
||||
{
|
||||
let store = try OSLogStore(scope: .currentProcessIdentifier)
|
||||
|
||||
// All logs since the app launched.
|
||||
let position = store.position(timeIntervalSinceLatestBoot: 0)
|
||||
let predicate = NSPredicate(format: "subsystem IN %@", [Logger.deltaSubsystem, Logger.harmonySubsystem])
|
||||
|
||||
let entries = try store.getEntries(at: position, matching: predicate)
|
||||
.compactMap { $0 as? OSLogEntryLog }
|
||||
.map { "[\($0.date.formatted())] [\($0.category)] [\($0.level.localizedName)] \($0.composedMessage)" }
|
||||
|
||||
let outputText = entries.joined(separator: "\n")
|
||||
|
||||
let outputDirectory = FileManager.default.uniqueTemporaryURL()
|
||||
try FileManager.default.createDirectory(at: outputDirectory, withIntermediateDirectories: true)
|
||||
|
||||
let outputURL = outputDirectory.appendingPathComponent("delta.log")
|
||||
try outputText.write(to: outputURL, atomically: true, encoding: .utf8)
|
||||
|
||||
await MainActor.run {
|
||||
self._exportedLogURL = outputURL
|
||||
|
||||
let previewController = QLPreviewController()
|
||||
previewController.delegate = self
|
||||
previewController.dataSource = self
|
||||
self.present(previewController, animated: true)
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to export Harmony logs.", error)
|
||||
}
|
||||
|
||||
await self.exportLogActivityIndicatorView.stopAnimating()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension SettingsViewController
|
||||
@ -341,6 +417,9 @@ extension SettingsViewController
|
||||
case .controllers: return 4
|
||||
case .controllerSkins: return System.registeredSystems.count
|
||||
case .syncing: return SyncManager.shared.coordinator?.account == nil ? 1 : super.tableView(tableView, numberOfRowsInSection: sectionIndex)
|
||||
#if !BETA
|
||||
case .advanced: return 1
|
||||
#endif
|
||||
default:
|
||||
if isSectionHidden(section)
|
||||
{
|
||||
@ -363,7 +442,7 @@ extension SettingsViewController
|
||||
case .controllers:
|
||||
if indexPath.row == Settings.localControllerPlayerIndex
|
||||
{
|
||||
cell.detailTextLabel?.text = UIDevice.current.name
|
||||
cell.detailTextLabel?.text = LocalDeviceController().name
|
||||
}
|
||||
else if let index = ExternalGameControllerManager.shared.connectedControllers.firstIndex(where: { $0.playerIndex == indexPath.row })
|
||||
{
|
||||
@ -410,7 +489,17 @@ extension SettingsViewController
|
||||
case .controllerSkins: self.performSegue(withIdentifier: Segue.controllerSkins.rawValue, sender: cell)
|
||||
case .cores: self.performSegue(withIdentifier: Segue.dsSettings.rawValue, sender: cell)
|
||||
case .controllerOpacity, .gameAudio, .hapticFeedback, .hapticTouch, .syncing: break
|
||||
case .advanced: self.showExperimentalFeatures()
|
||||
case .advanced:
|
||||
let row = AdvancedRow(rawValue: indexPath.row)!
|
||||
switch row
|
||||
{
|
||||
case .exportLog:
|
||||
guard #available(iOS 15, *) else { return }
|
||||
self.exportErrorLog()
|
||||
|
||||
case .experimentalFeatures: self.showExperimentalFeatures()
|
||||
}
|
||||
|
||||
case .patreon:
|
||||
let patreonURL = URL(string: "altstore://patreon")!
|
||||
|
||||
@ -449,6 +538,17 @@ extension SettingsViewController
|
||||
primary:
|
||||
switch Section(rawValue: indexPath.section)!
|
||||
{
|
||||
case .advanced:
|
||||
let row = AdvancedRow(rawValue: indexPath.row)!
|
||||
switch row
|
||||
{
|
||||
case .exportLog:
|
||||
guard #unavailable(iOS 15) else { break }
|
||||
return 0.0
|
||||
|
||||
default: break
|
||||
}
|
||||
|
||||
case .credits:
|
||||
let row = CreditsRow(rawValue: indexPath.row)!
|
||||
switch row
|
||||
@ -489,17 +589,45 @@ extension SettingsViewController
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView?
|
||||
{
|
||||
let section = Section(rawValue: section)!
|
||||
guard !isSectionHidden(section) else { return nil }
|
||||
|
||||
switch section
|
||||
{
|
||||
case .controllerSkins:
|
||||
guard #available(iOS 15, *), let footerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: AttributedHeaderFooterView.reuseIdentifier) as? AttributedHeaderFooterView else { break }
|
||||
|
||||
var attributedText = AttributedString(localized: "Customize the appearance of each system.")
|
||||
attributedText += " "
|
||||
|
||||
var learnMore = AttributedString(localized: "Learn more…")
|
||||
learnMore.link = URL(string: "https://faq.deltaemulator.com/using-delta/controller-skins")
|
||||
attributedText += learnMore
|
||||
|
||||
footerView.attributedText = attributedText
|
||||
|
||||
return footerView
|
||||
|
||||
default: break
|
||||
}
|
||||
|
||||
return super.tableView(tableView, viewForFooterInSection: section.rawValue)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String?
|
||||
{
|
||||
let section = Section(rawValue: section)!
|
||||
guard !isSectionHidden(section) else { return nil }
|
||||
|
||||
if isSectionHidden(section)
|
||||
switch section
|
||||
{
|
||||
return nil
|
||||
}
|
||||
else
|
||||
{
|
||||
return super.tableView(tableView, titleForFooterInSection: section.rawValue)
|
||||
#if !BETA
|
||||
case .advanced: return nil
|
||||
#endif
|
||||
case .controllerSkins: return nil
|
||||
default: return super.tableView(tableView, titleForFooterInSection: section.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
@ -520,14 +648,47 @@ extension SettingsViewController
|
||||
override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat
|
||||
{
|
||||
let section = Section(rawValue: section)!
|
||||
guard !isSectionHidden(section) else { return 1 }
|
||||
|
||||
if isSectionHidden(section)
|
||||
switch section
|
||||
{
|
||||
return 1
|
||||
case .controllerSkins: return UITableView.automaticDimension
|
||||
default: return super.tableView(tableView, heightForFooterInSection: section.rawValue)
|
||||
}
|
||||
else
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, estimatedHeightForFooterInSection section: Int) -> CGFloat
|
||||
{
|
||||
let section = Section(rawValue: section)!
|
||||
guard !isSectionHidden(section) else { return 1 }
|
||||
|
||||
switch section
|
||||
{
|
||||
return super.tableView(tableView, heightForFooterInSection: section.rawValue)
|
||||
case .controllerSkins: return 30
|
||||
default: return UITableView.automaticDimension
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsViewController: QLPreviewControllerDataSource, QLPreviewControllerDelegate
|
||||
{
|
||||
func numberOfPreviewItems(in controller: QLPreviewController) -> Int
|
||||
{
|
||||
return 1
|
||||
}
|
||||
|
||||
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem
|
||||
{
|
||||
return (_exportedLogURL as? NSURL) ?? NSURL()
|
||||
}
|
||||
|
||||
func previewControllerDidDismiss(_ controller: QLPreviewController)
|
||||
{
|
||||
guard let exportedLogURL = _exportedLogURL else { return }
|
||||
|
||||
let parentDirectory = exportedLogURL.deletingLastPathComponent()
|
||||
try? FileManager.default.removeItem(at: parentDirectory)
|
||||
|
||||
_exportedLogURL = nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,8 +90,17 @@ private extension SyncingServicesViewController
|
||||
let previousService = self.selectedSyncingService
|
||||
self.selectedSyncingService = service
|
||||
|
||||
// Set to non-nil if we later authenticate.
|
||||
Settings.syncingService = nil
|
||||
// Same check as below when showing Sign In or Sign Out.
|
||||
if let coordinator = SyncManager.shared.coordinator, coordinator.account != nil
|
||||
{
|
||||
// Authenticated, so assign syncingService.
|
||||
Settings.syncingService = service
|
||||
}
|
||||
else
|
||||
{
|
||||
// Set to non-nil if we later authenticate.
|
||||
Settings.syncingService = nil
|
||||
}
|
||||
|
||||
if (previousService == nil && service != nil) || (previousService != nil && service == nil)
|
||||
{
|
||||
|
||||
@ -197,6 +197,22 @@
|
||||
<string>Delta uses your microphone to emulate the Nintendo DS microphone.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Press "OK" to allow Delta to use images from your Photo Library as game artwork.</string>
|
||||
<key>OSLogPreferences</key>
|
||||
<dict>
|
||||
<key>com.rileytestut.Harmony</key>
|
||||
<dict>
|
||||
<key>Sync</key>
|
||||
<dict>
|
||||
<key>Level</key>
|
||||
<dict>
|
||||
<key>Enable</key>
|
||||
<string>Info</string>
|
||||
<key>Persist</key>
|
||||
<string>Info</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
|
||||
@ -140,7 +140,7 @@ extension SyncManager
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to remove Harmony database.", error)
|
||||
Logger.sync.error("Failed to remove Harmony database. \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
|
||||
self.start(service: service, completionHandler: completionHandler)
|
||||
@ -166,11 +166,12 @@ extension SyncManager
|
||||
{
|
||||
case .other(ServiceError.connectionFailed):
|
||||
// Authentication failed due to network connection, but otherwise started successfully so we ignore this error.
|
||||
Logger.sync.error("Failed to authenticate SyncManager due to network connection (ignoring). \(authError.localizedDescription, privacy: .public)")
|
||||
completionHandler(.success)
|
||||
|
||||
default:
|
||||
// Another authentication error occured, so we'll deauthenticate ourselves.
|
||||
print("SyncManager.start auth error:", authError)
|
||||
Logger.sync.error("Failed to authenticate SyncManager. \(authError.localizedDescription, privacy: .public)")
|
||||
|
||||
self.deauthenticate() { (result) in
|
||||
switch result
|
||||
@ -187,7 +188,7 @@ extension SyncManager
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("SyncManager.start error:", error)
|
||||
Logger.sync.error("Failed to start SyncManager. \(error.localizedDescription, privacy: .public)")
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
256
Docs/ExperimentalFeatures.md
Normal file
256
Docs/ExperimentalFeatures.md
Normal file
@ -0,0 +1,256 @@
|
||||
# Overview
|
||||
|
||||
Features contributed to Delta by third-party developers are considered "experimental", and are available for testing in the beta version of Delta. Once a feature has been sufficiently tested, we may choose to "graduate" it into an official Delta feature, at which point it will become available to all users.
|
||||
|
||||
Every Experimental Feature can be thought of as a binary flag: it's either enabled or disabled. When disabled, a feature should have no impact on the rest of the app. This allows us to accept contributions freely without affecting the overall stability of Delta.
|
||||
|
||||
If a feature requires more state than binary On/Off, you can define as many "options" as needed. Options store additional data required by your feature's implementation. Options can be "hidden" (the default), but can also be automatically exposed in Delta's settings where they can be changed directly by users.
|
||||
|
||||
# Guidelines For Experimental Features
|
||||
|
||||
Keep the following in mind when contributing new Experimental Features:
|
||||
|
||||
* When a feature is disabled, it should have *no* noticeable impact on the rest of the app.
|
||||
* Avoid touching the core emulation logic.
|
||||
* Isolate your changes as much as possible from the rest of the app, preferably in separate files. We recommend using Swift extensions to add functionality to existing types (e.g. `GameViewController+ExperimentalFastForward.swift`)
|
||||
* If your change requires modifying `DeltaCore` or specific cores, make sure the naming and "shape" of any public API follows existing conventions as much as possible. In general, Experimental Features that require modifying cores have a higher bar for acceptance.
|
||||
|
||||
# Adding a New Experimental Feature
|
||||
|
||||
1. Open `Delta/Experimental Features/ExperimentalFeatures.swift`
|
||||
2. Add a new property to the `ExperimentalFeatures` struct, annotated with the `@Feature` property wrapper. You do not need to define the property's type as it will be inferred by @Feature.
|
||||
|
||||
> The property name (e.g. `variableFastForward`) will be used internally as the `UserDefaults` key for persisting data.
|
||||
|
||||
3. Pass in the name of your feature to `@Feature`'s initializer, and optionally a description.
|
||||
|
||||
Once you've defined your feature, you can check whether or not it's enabled at runtime via `ExperimentalFeatures.shared.[feature].isEnabled`.
|
||||
|
||||
Here's a complete implementation for a new Experimental Feature called "Show Status Bar":
|
||||
|
||||
```swift
|
||||
// ExperimentalFeatures.swift
|
||||
struct ExperimentalFeatures
|
||||
{
|
||||
@Feature(name: "Show Status Bar", description: "Show the Status Bar during gameplay.")
|
||||
var showStatusBar
|
||||
}
|
||||
|
||||
// GameViewController+ShowStatusBar.swift
|
||||
extension GameViewController
|
||||
{
|
||||
override var prefersStatusBarHidden: Bool {
|
||||
return !ExperimentalFeatures.shared.showStatusBar.isEnabled
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Adding Options to a Feature
|
||||
Some features require additional configuration beyond being enabled or disabled. These are referred to as "options", and you can define as many options for a feature as necessary. Whenever an option's value changes, it is automatically persisted to `UserDefaults`.
|
||||
|
||||
The option’s underlying type must conform to `OptionValue`. Automatic conformance is provided for all standard property list types, but if you want to use your own type, it must either:
|
||||
* Conform to `RawRepresentable`, where its raw type is a valid property list type (e.g. enums with string backing), or
|
||||
* Conform to `Codable`
|
||||
|
||||
To declare a feature with options:
|
||||
|
||||
1. Create a new Swift file in `Delta/Experimental Features/Features` and name it after your feature (e.g. `VariableFastForward.swift`)
|
||||
2. Define a new struct named `[FeatureName]Options` (e.g. `VariableFastForwardOptions`)
|
||||
3. For each configurable value, define a new property on your Options struct with `@Options` property wrapper.
|
||||
> The property name (e.g. `speed`) will be combined with the feature's property name and used internally as the `UserDefaults` key for persisting data.
|
||||
4. **If the option represents a non-optional value, you must provide an initial value**. This will be used as the default value if the option has not been configured by user.
|
||||
5. Follow the above instructions for declaring a feature, but pass in an instance of your `Options` struct to the `options:` parameter in the `@Feature` initializer.
|
||||
|
||||
Heres's an example feature "Game Gestures" that shows an instruction alert the first time it is enabled. It uses an `@Option` to store whether the alert has already been shown or not.
|
||||
|
||||
```swift
|
||||
// GameGestures.swift
|
||||
struct GameGesturesOptions
|
||||
{
|
||||
@Option // No parameters = "Hidden" option
|
||||
var didShowGestureAlert: Bool = false
|
||||
}
|
||||
|
||||
// ExperimentalFeatures.swift
|
||||
struct ExperimentalFeatures
|
||||
{
|
||||
@Feature(name: "Game Gestures", options: GameGesturesOptions())
|
||||
var gameGestures
|
||||
}
|
||||
```
|
||||
|
||||
# User-Facing Options
|
||||
By default, Options are hidden, which means their values can only be changed programmatically.
|
||||
|
||||
However, options can also be user-facing, which we generally recommend. User-facing options will automatically appear in the `Experimental Features` section of Delta's settings, where they can be configured manually by users. To define a user-facing option, pass in a value for `name` in the `@Option` initializer, and optionally a `description`.
|
||||
|
||||
Because user-facing options are meant to be seen by users, the underlying type must conform to `LocalizedOptionValue`. This protocol refines `OptionValue` with two new methods:
|
||||
* `localizedDescription`, used to display the value in a human-readable manner.
|
||||
* `localizedNilDescription`, used to represent the `nil` value in a human-readable manner. The default implementation returns "None".
|
||||
|
||||
Heres's an example feature "Game Screenshots" that defines options so users can choose whether to save screenshots to the Photo Library, the Files app, or both. Unlike the above "Game Gestures" example, "Save to Files" and "Save to Photos" will be exposed in the `Experimental Features` section of Delta's settings, where they will appear as switches that the user can toggle.
|
||||
|
||||
```swift
|
||||
// GameScreenshots.swift
|
||||
struct GameScreenshotsOptions
|
||||
{
|
||||
@Option(name: "Save to Files", description: "Save the screenshot to the app's directory in Files.")
|
||||
var saveToFiles: Bool = true
|
||||
|
||||
@Option(name: "Save to Photos", description: "Save the screenshot to the Photo Library.")
|
||||
var saveToPhotos: Bool = false
|
||||
}
|
||||
|
||||
// ExperimentalFeatures.swift
|
||||
struct ExperimentalFeatures
|
||||
{
|
||||
@Feature(name: "Game Screenshots", options: GameScreenshotsOptions())
|
||||
var gameScreenshots
|
||||
}
|
||||
```
|
||||
|
||||
## Types of User-Facing Options
|
||||
|
||||
Delta supports 3 types of user-facing options:
|
||||
* Bool options
|
||||
* "Picker" options (e.g. array of values)
|
||||
* Custom options (any other type)
|
||||
|
||||
Here's an example feature "VariableFastForward" that uses all 3 types of user-facing options:
|
||||
|
||||
```swift
|
||||
// VariableFastForward.swift
|
||||
enum FastForwardSpeed: Double, CaseIterable, CustomStringConvertible
|
||||
{
|
||||
case x2 = 2
|
||||
case x3 = 3
|
||||
case x4 = 4
|
||||
case x8 = 8
|
||||
|
||||
var description: String {
|
||||
return "\(self.rawValue)x"
|
||||
}
|
||||
}
|
||||
|
||||
extension FastForwardSpeed: LocalizedOptionValue
|
||||
{
|
||||
var localizedDescription: Text {
|
||||
Text(self.description)
|
||||
}
|
||||
|
||||
static var localizedNilDescription: Text {
|
||||
Text("Maximum")
|
||||
}
|
||||
}
|
||||
|
||||
struct VariableFastForwardOptions
|
||||
{
|
||||
// Bool option (will appear as inline UISwitch)
|
||||
@Option(name: "Allow Unrestricted Speeds", description: "Allow speeds that exceed the maximum speed of a system.")
|
||||
var allowUnrestrictedSpeeds: Bool = false
|
||||
|
||||
// "Custom" option (will appear as full-screen view with text field)
|
||||
@Option(name: "Maximum Speed", description: "Change the maximum fast forward speed across all systems.", detailView: {
|
||||
TextField("", value: $0, formatter: NumberFormatter())
|
||||
.keyboardType(.numberPad)
|
||||
})
|
||||
var maxSpeed: Int?
|
||||
|
||||
// "Picker" options (will appear as standard UIMenu picker)
|
||||
@Option(name: "Nintendo Entertainment System", values: FastForwardSpeed.allCases)
|
||||
var nes: FastForwardSpeed?
|
||||
|
||||
@Option(name: "Super Nintendo", values: FastForwardSpeed.allCases)
|
||||
var snes: FastForwardSpeed?
|
||||
|
||||
@Option(name: "Nintendo 64", values: FastForwardSpeed.allCases)
|
||||
var n64: FastForwardSpeed?
|
||||
|
||||
// Etc.
|
||||
}
|
||||
```
|
||||
|
||||
Each type of user-facing option has slightly different requirements, which are detailed below:
|
||||
|
||||
### Bool Option
|
||||
If the property annotated with @Option is a `Bool`, there is nothing more you need to do. Delta will automatically show a toggle on the feature's detail page that can be used by user to update this value.
|
||||
|
||||
Example:
|
||||
```swift
|
||||
@Option(name: "showStatusBar")
|
||||
var showStatusBar: Bool = false
|
||||
```
|
||||
|
||||
### "Picker" Option
|
||||
If there is a known, finite number of supported values for your option, you can pass a `Collection` of them to the `values:` parameter in the `@Option` initializer. Delta will automatically show an inline picker on the feature's detail page that will allow users to select from the preset values.
|
||||
|
||||
>
|
||||
> If the option is an optional type, the picker will automatically include a `nil` option in the picker. You can customize the name used to represent the `nil` option by overriding `LocalizedOptionValue.localizedNilDescription`.
|
||||
>
|
||||
|
||||
Example:
|
||||
```swift
|
||||
enum Planet: String { mercury, venus, earth, ... }
|
||||
|
||||
extension Planet: LocalizedOptionValue
|
||||
{
|
||||
static var localizedNilDescription: Text {
|
||||
Text("No Favorite Planet")
|
||||
}
|
||||
}
|
||||
|
||||
@Option(name: "Current Planet", values: Planet.allCases)
|
||||
var currentPlanet: Planet = .earth
|
||||
|
||||
@Option(name: "Favorite Planet", values: Planet.allCases)
|
||||
var favoritePlanet: Planet? // Optional, so Delta will include `nil` option in picker, displayed as "No Favorite Planet".
|
||||
```
|
||||
|
||||
### "Custom" Option
|
||||
Every user-facing `@Option` requires some UI in order to be configured by users in Delta's settings. If your option is not one of the ones listed above, you'll need to provide your own `SwiftUI` view. This can be as simple as just an inline `TextField` (e.g. for `String` options), or a completely custom full screen SwiftUI view with access to the entire SwiftUI API (e.g. a full color picker for `Color` options).
|
||||
|
||||
To provide your own SwiftUI view, pass in a closure that returns your custom `View` to the `detailView:` parameter in `@Option`'s initializer. The closure passes in a `Binding` to the option's underlying value, which can then be passed into any SwiftUI control that takes a `Binding` (e.g. `Picker`, `Toggle`, `TextField`, etc.) to automatically update the option's value. However this is just a convenience, and you are welcome to update your `@Option` value from your custom view however works best.
|
||||
|
||||
By default, custom options will present their SwiftUI views full-screen when tapped. However, if you want your custom view to appear inline (like Bool and "picker" options), you can apply the `displayInline()` modifier to your view.
|
||||
|
||||
Example:
|
||||
```swift
|
||||
// Inline text field
|
||||
@Option(name: "Custom Nickname", detailView: {
|
||||
TextField("", text: $0)
|
||||
.displayInline()
|
||||
})
|
||||
var nickname: String
|
||||
```
|
||||
|
||||
## Using Features
|
||||
|
||||
All Experimental Features can be selectively enabled or disabled by the user in the "Experimental Features" section of Delta's settings. To check whether a feature is enabled at runtime, call `ExperimentalFeatures.shared.[feature].isEnabled`. **Your feature implementation must respect this flag and have no noticeable effect on the rest of the app when disabled.**
|
||||
|
||||
You can access individual feature options via `ExperimentalFeatures.shared.[feature].[option]`. To access `@Option`-specific properties, such as its `settingsKey`, use the `@Option`'s projected value by prepending the property with a `$` (e.g. `ExperimentalFeatures.shared.[feature].$[option].settingsName`).
|
||||
|
||||
Delta will automatically post a `Settings.didChangeNotification` notification whenever a feature is enabled, disabled, or one of its options changes. The `userInfo` dictionary will contain either `Feature.settingsName` or `Option.settingsName` under the `SettingsUserInfoKey.name` key, as well as the new value under the `SettingsUserInfoKey.value` key.
|
||||
|
||||
Example:
|
||||
|
||||
```swift
|
||||
// Handler for Settings.didChangeNotification
|
||||
func settingsDidChange(_ notification: Notification)
|
||||
{
|
||||
guard let name = notification.userInfo?[SettingsUserInfoKey.name] as? Settings.Name else { return }
|
||||
|
||||
switch name
|
||||
{
|
||||
case ExperimentalFeatures.shared.showStatusBar.settingsKey:
|
||||
// Update status bar
|
||||
self.setNeedsStatusBarAppearanceUpdate()
|
||||
|
||||
case ExperimentalFeatures.shared.customTintColor.settingsKey: fallthrough
|
||||
case ExperimentalFeatures.shared.customTintColor.$color.settingsKey:
|
||||
// Update tint color if feature itself is enabled/disabled OR tint color changes.
|
||||
self.updateTintColor()
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
```
|
||||
34
Docs/pull_request_template.md
Normal file
34
Docs/pull_request_template.md
Normal file
@ -0,0 +1,34 @@
|
||||
Mark the type contribution you are making:
|
||||
|
||||
- [ ] Experimental feature (new functionality that can be selectively enabled/disabled)
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
|
||||
# Description
|
||||
|
||||
Summary of your changes, including:
|
||||
|
||||
* Why is this change necessary?
|
||||
* Why did you decide on this solution?
|
||||
|
||||
# Testing
|
||||
|
||||
List all iOS versions and devices you've tested this change on.
|
||||
|
||||
**Example Configurations**:
|
||||
|
||||
- iPhone 14, iOS 16.3.1
|
||||
- iPhone X, iOS 15.7.4
|
||||
|
||||
# Checklist
|
||||
**General (All PRs)**
|
||||
- [ ] My code follows the style guidelines of this project
|
||||
- [ ] I have performed a self-review of my code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] I've tested my changes with different device + OS version configurations
|
||||
|
||||
**Experimental Feature-specific**
|
||||
- [ ] Added property to `ExperimentalFeatures` struct annotated with `@Feature`
|
||||
- [ ] Uses `@Option`'s to persist all feature-related data
|
||||
- [ ] Locked *all* behavior changes behind `ExperimentalFeatures.shared.[feature].isEnabled` runtime check
|
||||
- [ ] Isolates changes to separate files as much as possible (e.g. via Swift extensions)
|
||||
2
External/Harmony
vendored
2
External/Harmony
vendored
@ -1 +1 @@
|
||||
Subproject commit d348fc7440198fd91183d2236e3816dee8cc24ee
|
||||
Subproject commit e1901297408ad220aa3c05e4c4fd9660c3420679
|
||||
@ -130,7 +130,7 @@ SPEC CHECKSUMS:
|
||||
GoogleSignIn: 7137d297ddc022a7e0aa4619c86d72c909fa7213
|
||||
GTMAppAuth: 0ff230db599948a9ad7470ca667337803b3fc4dd
|
||||
GTMSessionFetcher: 5595ec75acf5be50814f81e9189490412bad82ba
|
||||
Harmony: 5fdc51d0a4f2ce7dcd4439becbbdda1fac4c9e3f
|
||||
Harmony: 1d8166b6168eec8cce046c4366765d15f7e3b9fc
|
||||
MelonDSDeltaCore: 46193f4fd88e4e18e4a5c841b1ae02dc46d1daa6
|
||||
N64DeltaCore: 4eeb468746722952bcd5467ecb9ebe7df070f53a
|
||||
NESDeltaCore: ffae3bba878fc505bac0914150a695ede7bc9550
|
||||
|
||||
2
Pods/Local Podspecs/Harmony.podspec.json
generated
2
Pods/Local Podspecs/Harmony.podspec.json
generated
@ -5,7 +5,7 @@
|
||||
"description": "iOS framework that automatically syncs Core Data databases across different backends.",
|
||||
"homepage": "https://github.com/rileytestut/Harmony",
|
||||
"platforms": {
|
||||
"ios": "12.0"
|
||||
"ios": "14.0"
|
||||
},
|
||||
"source": {
|
||||
"git": "https://github.com/rileytestut/Harmony.git"
|
||||
|
||||
2
Pods/Manifest.lock
generated
2
Pods/Manifest.lock
generated
@ -130,7 +130,7 @@ SPEC CHECKSUMS:
|
||||
GoogleSignIn: 7137d297ddc022a7e0aa4619c86d72c909fa7213
|
||||
GTMAppAuth: 0ff230db599948a9ad7470ca667337803b3fc4dd
|
||||
GTMSessionFetcher: 5595ec75acf5be50814f81e9189490412bad82ba
|
||||
Harmony: 5fdc51d0a4f2ce7dcd4439becbbdda1fac4c9e3f
|
||||
Harmony: 1d8166b6168eec8cce046c4366765d15f7e3b9fc
|
||||
MelonDSDeltaCore: 46193f4fd88e4e18e4a5c841b1ae02dc46d1daa6
|
||||
N64DeltaCore: 4eeb468746722952bcd5467ecb9ebe7df070f53a
|
||||
NESDeltaCore: ffae3bba878fc505bac0914150a695ede7bc9550
|
||||
|
||||
106
Pods/Pods.xcodeproj/project.pbxproj
generated
106
Pods/Pods.xcodeproj/project.pbxproj
generated
@ -12645,6 +12645,31 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
1A1444025E75F6B39111B81990FB1920 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = DE938F9D29DEE2089E52BCE4D765DF2B /* Harmony.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "";
|
||||
GCC_PREFIX_HEADER = "Target Support Files/Harmony/Harmony-prefix.pch";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MODULEMAP_FILE = Headers/Public/Harmony/Harmony.modulemap;
|
||||
OTHER_LDFLAGS = "";
|
||||
OTHER_LIBTOOLFLAGS = "";
|
||||
PRIVATE_HEADERS_FOLDER_PATH = "";
|
||||
PRODUCT_MODULE_NAME = Harmony;
|
||||
PRODUCT_NAME = Harmony;
|
||||
PUBLIC_HEADERS_FOLDER_PATH = "";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) ";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
1E7F8B6EB3D07FA3D8236A4BCB55962F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 698A11ECCAF009BA9765514FEF63E5D2 /* SNESDeltaCore.release.xcconfig */;
|
||||
@ -12795,6 +12820,32 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
2F70207C6F18AF342FEC2F08613C804F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 143773242E2A521744AB37B4F8D49425 /* Harmony.release.xcconfig */;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "";
|
||||
GCC_PREFIX_HEADER = "Target Support Files/Harmony/Harmony-prefix.pch";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MODULEMAP_FILE = Headers/Public/Harmony/Harmony.modulemap;
|
||||
OTHER_LDFLAGS = "";
|
||||
OTHER_LIBTOOLFLAGS = "";
|
||||
PRIVATE_HEADERS_FOLDER_PATH = "";
|
||||
PRODUCT_MODULE_NAME = Harmony;
|
||||
PRODUCT_NAME = Harmony;
|
||||
PUBLIC_HEADERS_FOLDER_PATH = "";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) ";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
3312B6D3B835689CC7619671A911CF1F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 14C688327822408CE66949114C33CC83 /* GoogleAPIClientForREST.release.xcconfig */;
|
||||
@ -13643,32 +13694,6 @@
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
979B8CE91B81CC23F92BFAF9017FD30E /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 143773242E2A521744AB37B4F8D49425 /* Harmony.release.xcconfig */;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "";
|
||||
GCC_PREFIX_HEADER = "Target Support Files/Harmony/Harmony-prefix.pch";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MODULEMAP_FILE = Headers/Public/Harmony/Harmony.modulemap;
|
||||
OTHER_LDFLAGS = "";
|
||||
OTHER_LIBTOOLFLAGS = "";
|
||||
PRIVATE_HEADERS_FOLDER_PATH = "";
|
||||
PRODUCT_MODULE_NAME = Harmony;
|
||||
PRODUCT_NAME = Harmony;
|
||||
PUBLIC_HEADERS_FOLDER_PATH = "";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) ";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
99FB94707B9559CB88ACBC1B6D5AA3F1 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9E90A4E912BD6E255A8140BE26A53D08 /* SDWebImage.debug.xcconfig */;
|
||||
@ -14092,31 +14117,6 @@
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
FB4707879DC0D9B9AB55309F19257D69 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = DE938F9D29DEE2089E52BCE4D765DF2B /* Harmony.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
|
||||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "";
|
||||
GCC_PREFIX_HEADER = "Target Support Files/Harmony/Harmony-prefix.pch";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MODULEMAP_FILE = Headers/Public/Harmony/Harmony.modulemap;
|
||||
OTHER_LDFLAGS = "";
|
||||
OTHER_LIBTOOLFLAGS = "";
|
||||
PRIVATE_HEADERS_FOLDER_PATH = "";
|
||||
PRODUCT_MODULE_NAME = Harmony;
|
||||
PRODUCT_NAME = Harmony;
|
||||
PUBLIC_HEADERS_FOLDER_PATH = "";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) ";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
@ -14294,8 +14294,8 @@
|
||||
7C3E1190ABE8B5371898BF25BD61572F /* Build configuration list for PBXNativeTarget "Harmony" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
FB4707879DC0D9B9AB55309F19257D69 /* Debug */,
|
||||
979B8CE91B81CC23F92BFAF9017FD30E /* Release */,
|
||||
1A1444025E75F6B39111B81990FB1920 /* Debug */,
|
||||
2F70207C6F18AF342FEC2F08613C804F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
|
||||
14
README.md
14
README.md
@ -10,7 +10,7 @@ Delta is an iOS application that allows you to emulate and play video games for
|
||||
|
||||
<p align="center">
|
||||
<img src="https://user-images.githubusercontent.com/705880/115471008-203aa480-a1ec-11eb-8aba-237a46799543.png" width=75%><br/>
|
||||
<em>Mario and Pokémon and properties of Nintendo Co., Ltd. and are not associated with Delta or AltStore LLC.</em>
|
||||
<em>Mario and Pokémon are properties of Nintendo Co., Ltd. and are not associated with Delta or AltStore LLC.</em>
|
||||
</p>
|
||||
|
||||
## Supported Systems
|
||||
@ -20,7 +20,7 @@ Delta is an iOS application that allows you to emulate and play video games for
|
||||
- Game Boy / Game Boy Color (GBC)
|
||||
- Game Boy Advance (GBA)
|
||||
- Nintendo DS (DS)
|
||||
- Sega Genesis / Mega Drive (GEN) **(in progress)**
|
||||
- Sega Genesis / Mega Drive (GEN) **(in beta)**
|
||||
|
||||
## Features
|
||||
- Accurate, full speed emulation thanks to mature underlying emulator cores.
|
||||
@ -126,12 +126,10 @@ Each system in Delta is implemented as its own "Delta Core", which serves as a s
|
||||
- [MelonDSDeltaCore](https://github.com/rileytestut/MelonDSDeltaCore)
|
||||
- [GPGXDeltaCore](https://github.com/rileytestut/GPGXDeltaCore)
|
||||
|
||||
## Project Requirements
|
||||
- Xcode 12
|
||||
- Swift 5+
|
||||
- iOS 12.2 or later
|
||||
|
||||
Why iOS 12.2 or later? Doing so allows me to distribute Delta without embedding Swift libraries inside. This helps me afford bandwidth costs by reducing download sizes by roughly 30%, but also noticeably improves how long it takes to install/refresh Delta with AltStore. If you're compiling Delta yourself, however, you should be able to lower the deployment target to iOS 12.0 without any issues.
|
||||
## Minimum Project Requirements
|
||||
- Xcode 15
|
||||
- Swift 5.9
|
||||
- iOS 14.0
|
||||
|
||||
## Compilation Instructions
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user