Compare commits

...

35 Commits

Author SHA1 Message Date
Riley Testut
c4926a6626 Updates app verssion to 1.5 (85) 2024-01-31 12:30:12 -06:00
Riley Testut
52d9e97f64 Updates app version to 1.5rc2 (82) 2024-01-19 18:59:08 -06:00
Riley Testut
9963bb84b4 [Delta Sync] Fixes accidentally signing-out after signing-in to previous Google Drive account 2024-01-19 18:59:08 -06:00
Riley Testut
ec336c39db [Delta Sync] Fixes incorrectly marking some GameSaves as conflicted during initial sync
Fixes an edge case where updating from Delta 1.4.x to Delta 1.5 could result in false-conflicts if user had previously updated to Delta 1.5 in the past (including betas). This is because GameSaves updated on 1.5 have a new `sha1` field, which changes the hash for the whole record.
2024-01-17 18:35:53 -06:00
Riley Testut
1d37a1dae8 [Delta Sync] Fixes issue causing Harmony to skip seeding database 2024-01-16 17:47:31 -06:00
Riley Testut
066993db67 Displays Touch/Touch Screen instead of iPhone/iPad when using touch controls 2024-01-12 14:26:12 -06:00
Riley Testut
ed5f0174d7 [Delta Sync] Replaces auth error print statements with OSLog 2024-01-12 13:42:19 -06:00
Riley Testut
adb4d21f99 Updates app version to 1.5 (80) 2023-12-11 15:57:43 -06:00
Riley Testut
9dd4d01b66 Fixes incorrectly sorted Info.plist keys 2023-12-01 14:01:41 -06:00
Riley Testut
5f2de79221 Updates app version to 1.5rc (78) 2023-12-01 13:59:49 -06:00
Riley Testut
7ea9369356 Merges Ian Clawson’s Local Multiplayer PRs into cores 2023-11-16 18:57:04 -06:00
Riley Testut
995dd6afdf Shows “Learn More” FAQ button if there are no games 2023-11-16 17:52:37 -06:00
Riley Testut
c5333ad118 Adds FAQ links underneath Controller Skins and BIOS settings 2023-11-16 17:52:37 -06:00
Riley Testut
47b96d1cf6 [iPad] Hides “Haptic Feedback” Settings section on iPad 2023-11-16 17:52:37 -06:00
Riley Testut
3e97e5c951 [ExpFeat] Adds “Repair Database” to explicitly repair database
Disables automatic database repairing at app launch.
2023-11-16 17:52:37 -06:00
Riley Testut
aea0527828 Updates incorrect Settings labels to use 17pt font 2023-11-16 17:52:37 -06:00
Riley Testut
a79924d340 [iPad] Fixes not rounding last visible row in MelonDS core settings 2023-11-16 17:52:37 -06:00
Riley Testut
5ffccbb884 [iPad] Fixes crash when changing DS emulator core 2023-11-16 17:52:37 -06:00
Riley Testut
41bb827d7b [iPad] Fixes incorrectly indented rows on MelonDS core settings screen 2023-11-16 17:52:37 -06:00
Riley Testut
a87fea63ad Shows AirPlay icon + message when AirPlaying 2023-11-16 17:52:37 -06:00
Riley Testut
4b159603a1 Adds “Export Error Log…” button to Settings
Also hides “Experimental Features” setting for public builds
2023-11-16 17:52:36 -06:00
Riley Testut
a8b9bd1092 Updates Harmony to use OSLog 2023-11-16 17:20:27 -06:00
Riley Testut
cc12806273 [README] Updates minimum project requirements 2023-10-19 12:54:00 -05:00
Riley Testut
594ff25c5c Removes develop branch instructions from CONTRIBUTING.md 2023-10-19 12:29:40 -05:00
Riley Testut
aa5d53097f Merge branch 'develop' 2023-10-19 12:27:07 -05:00
Riley Testut
7f79e1d3a6
Adds develop branch instructions to CONTRIBUTING.md 2023-05-16 11:56:07 -05:00
Riley Testut
17f8f95727 Adds CONTRIBUTING.md 2023-05-03 13:50:08 -05:00
Riley Testut
3b208465e0 [Docs] Adds Experimental Features guidelines 2023-05-03 13:50:08 -05:00
Riley Testut
7806c85039 [Docs] Adds pull request template 2023-05-03 13:50:08 -05:00
Riley Testut
cfa31678ea
Merge pull request #225 from rileytestut/1.4
Delta 1.4
2023-02-28 17:35:56 -06:00
Riley Testut
c293c50965 [README] Updates compilation instructions
* Adds instructions for updating Systems.xcworkspace
* Asks user to install Git LFS
2022-08-18 12:18:41 -05:00
Joelle Stickney
785136a663
Update README.md 2022-02-20 15:20:48 -05:00
Riley Testut
0d27892c8d
Fixes typo in README 2021-04-21 16:48:28 -07:00
Riley Testut
3e216515b7
Merge pull request #113 from rileytestut/develop
Delta 1.3
2021-04-21 13:58:52 -07:00
Riley Testut
34afd64ae0
Merge pull request #32 from rileytestut/develop
Updates Delta to 1.1
2019-10-16 10:43:45 -07:00
32 changed files with 1070 additions and 222 deletions

62
CONTRIBUTING.md Normal file
View 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

View File

@ -188,9 +188,12 @@
D54A4BB329E4D27E004C7D57 /* FeatureDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54A4BB229E4D27E004C7D57 /* FeatureDetailView.swift */; }; D54A4BB329E4D27E004C7D57 /* FeatureDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54A4BB229E4D27E004C7D57 /* FeatureDetailView.swift */; };
D54F710229E89DCB009C069A /* SettingsUserInfoKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54F710129E89DCB009C069A /* SettingsUserInfoKey.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 */; }; 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 */; }; D55C468F29E761C000EA6DE9 /* AnyFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55C468E29E761C000EA6DE9 /* AnyFeature.swift */; };
D55C469129E7631000EA6DE9 /* AnyOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55C469029E7631000EA6DE9 /* AnyOption.swift */; }; D55C469129E7631000EA6DE9 /* AnyOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55C469029E7631000EA6DE9 /* AnyOption.swift */; };
D560BD8629EDC45600289847 /* ExternalDisplaySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D560BD8529EDC45600289847 /* ExternalDisplaySceneDelegate.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 */; }; D57D795629F300E100BB2CF8 /* CustomTintColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A817AF29DF4E6E00904AFE /* CustomTintColor.swift */; };
D57D795F29F315F700BB2CF8 /* FeatureDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54A4BB229E4D27E004C7D57 /* FeatureDetailView.swift */; }; D57D795F29F315F700BB2CF8 /* FeatureDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54A4BB229E4D27E004C7D57 /* FeatureDetailView.swift */; };
D57D796029F315F700BB2CF8 /* ExperimentalFeaturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A9C00229DDED6D00A8D610 /* ExperimentalFeaturesView.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 */; }; D5CDCCF12A859E7500E22131 /* ReviewSaveStatesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A1375A2A7D8F2600AB1372 /* ReviewSaveStatesViewController.swift */; };
D5CDCCF22A859E7500E22131 /* RepairDatabaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A137442A7D814000AB1372 /* RepairDatabaseViewController.swift */; }; D5CDCCF22A859E7500E22131 /* RepairDatabaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A137442A7D814000AB1372 /* RepairDatabaseViewController.swift */; };
D5CDCCF32A859E7500E22131 /* GamePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A137662A7DB37200AB1372 /* GamePickerViewController.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 */; }; D5D78AE529F9BC3700E064F0 /* DSAirPlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D78AE429F9BC3700E064F0 /* DSAirPlay.swift */; };
D5D78AE729F9D40A00E064F0 /* EnvironmentValues+FeatureOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D78AE629F9D40A00E064F0 /* EnvironmentValues+FeatureOption.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 */; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; D5D797E5298D946200738869 /* Contributor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contributor.swift; sourceTree = "<group>"; };
@ -566,6 +574,8 @@
D5CDCCC32A85765900E22131 /* OSLog+Delta.swift */, D5CDCCC32A85765900E22131 /* OSLog+Delta.swift */,
D5CDCCEA2A8593FC00E22131 /* UserDefaults+Delta.swift */, D5CDCCEA2A8593FC00E22131 /* UserDefaults+Delta.swift */,
AC1AE30B2A6A068F00956EB9 /* Bundle+AppIconImage.swift */, AC1AE30B2A6A068F00956EB9 /* Bundle+AppIconImage.swift */,
D5D39E782AF2D624004BE3F7 /* GameView+AirPlay.swift */,
D55917D72B51CF7D007B7DC0 /* ProcessInfo+visionOS.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
@ -576,6 +586,7 @@
BF11734F1DA32CF600047DF8 /* ControllersSettingsViewController.swift */, BF11734F1DA32CF600047DF8 /* ControllersSettingsViewController.swift */,
BF616A121F08184A0077F8B2 /* ControllerInputsViewController.swift */, BF616A121F08184A0077F8B2 /* ControllerInputsViewController.swift */,
BF8DDD231F4F6C880088A21B /* InputCalloutView.swift */, BF8DDD231F4F6C880088A21B /* InputCalloutView.swift */,
D55917D92B51D097007B7DC0 /* LocalDeviceController.swift */,
); );
path = Controllers; path = Controllers;
sourceTree = "<group>"; sourceTree = "<group>";
@ -784,6 +795,7 @@
BFFA4C081E8A24D600D87934 /* GameTableViewCell.swift */, BFFA4C081E8A24D600D87934 /* GameTableViewCell.swift */,
BF71CF891FE904B1001F1613 /* GameTableViewCell.xib */, BF71CF891FE904B1001F1613 /* GameTableViewCell.xib */,
BF8A333321A484A000A42FD4 /* BadgedTableViewCell.swift */, BF8A333321A484A000A42FD4 /* BadgedTableViewCell.swift */,
D56F7ABB2B05988700490ACB /* AttributedHeaderFooterView.swift */,
); );
path = "Table View"; path = "Table View";
sourceTree = "<group>"; sourceTree = "<group>";
@ -1547,6 +1559,7 @@
BF59427C1E09BC830051894B /* Cheat.swift in Sources */, BF59427C1E09BC830051894B /* Cheat.swift in Sources */,
BFBAB2E31EB685A2004E0B0E /* DeltaCoreProtocol+Delta.swift in Sources */, BFBAB2E31EB685A2004E0B0E /* DeltaCoreProtocol+Delta.swift in Sources */,
D5CDCCF22A859E7500E22131 /* RepairDatabaseViewController.swift in Sources */, D5CDCCF22A859E7500E22131 /* RepairDatabaseViewController.swift in Sources */,
D55917D82B51CF7D007B7DC0 /* ProcessInfo+visionOS.swift in Sources */,
BF3540081C5DAFAD00C1184C /* PauseTransitionCoordinator.swift in Sources */, BF3540081C5DAFAD00C1184C /* PauseTransitionCoordinator.swift in Sources */,
BF525EEA1FF6CD12004AA849 /* DeepLink.swift in Sources */, BF525EEA1FF6CD12004AA849 /* DeepLink.swift in Sources */,
AC1C991029F8B8C30020E6E4 /* ToastNotificationOptions.swift in Sources */, AC1C991029F8B8C30020E6E4 /* ToastNotificationOptions.swift in Sources */,
@ -1660,8 +1673,10 @@
BF6866171DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift in Sources */, BF6866171DCAC8B900BF2D06 /* ControllerSkin+Configuring.swift in Sources */,
BF713C0822499ED4004A1A2B /* PreviousHarmony.xcdatamodeld in Sources */, BF713C0822499ED4004A1A2B /* PreviousHarmony.xcdatamodeld in Sources */,
BF59427D1E09BC830051894B /* ControllerSkin.swift in Sources */, BF59427D1E09BC830051894B /* ControllerSkin.swift in Sources */,
D56F7ABC2B05988700490ACB /* AttributedHeaderFooterView.swift in Sources */,
BFAB9F7D219A43380080EC7D /* SyncManager.swift in Sources */, BFAB9F7D219A43380080EC7D /* SyncManager.swift in Sources */,
BFCEA67E1D56FF640061A534 /* UIViewControllerContextTransitioning+Conveniences.swift in Sources */, BFCEA67E1D56FF640061A534 /* UIViewControllerContextTransitioning+Conveniences.swift in Sources */,
D55917DA2B51D097007B7DC0 /* LocalDeviceController.swift in Sources */,
BF1173501DA32CF600047DF8 /* ControllersSettingsViewController.swift in Sources */, BF1173501DA32CF600047DF8 /* ControllersSettingsViewController.swift in Sources */,
BFFC461E1D59823500AF2CC6 /* GamesPresentationController.swift in Sources */, BFFC461E1D59823500AF2CC6 /* GamesPresentationController.swift in Sources */,
BF99A5971DC2F9C400468E9E /* ControllerSkinTableViewCell.swift in Sources */, BF99A5971DC2F9C400468E9E /* ControllerSkinTableViewCell.swift in Sources */,
@ -1669,6 +1684,7 @@
BF5942861E09BC8B0051894B /* _Cheat.swift in Sources */, BF5942861E09BC8B0051894B /* _Cheat.swift in Sources */,
BF5E7F441B9A650B00AE44F8 /* SettingsViewController.swift in Sources */, BF5E7F441B9A650B00AE44F8 /* SettingsViewController.swift in Sources */,
BF8CA9361F5F651900499FDD /* PopoverMenuController.swift in Sources */, BF8CA9361F5F651900499FDD /* PopoverMenuController.swift in Sources */,
D5D39E792AF2D624004BE3F7 /* GameView+AirPlay.swift in Sources */,
BFEF24F31F7DD4FD00454C62 /* SaveStateMigrationPolicy.swift in Sources */, BFEF24F31F7DD4FD00454C62 /* SaveStateMigrationPolicy.swift in Sources */,
BF5942931E09BD1A0051894B /* NSFetchedResultsController+Conveniences.m in Sources */, BF5942931E09BD1A0051894B /* NSFetchedResultsController+Conveniences.m in Sources */,
D5CDCCEE2A859DC200E22131 /* SyncValidationError.swift in Sources */, D5CDCCEE2A859DC200E22131 /* SyncValidationError.swift in Sources */,
@ -1933,18 +1949,18 @@
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 74; CURRENT_PROJECT_VERSION = 85;
DEVELOPMENT_TEAM = 6XVY5G3U44; DEVELOPMENT_TEAM = 6XVY5G3U44;
INFOPLIST_FILE = "Delta/Supporting Files/Info.plist"; INFOPLIST_FILE = "Delta/Supporting Files/Info.plist";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.5b4; MARKETING_VERSION = 1.5;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-ld64", "-ld64",
"$(inherited)", "$(inherited)",
); );
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DDEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DDEBUG";
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.Delta; PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.Delta;
PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE = "";
@ -1971,18 +1987,18 @@
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 74; CURRENT_PROJECT_VERSION = 85;
DEVELOPMENT_TEAM = 6XVY5G3U44; DEVELOPMENT_TEAM = 6XVY5G3U44;
INFOPLIST_FILE = "Delta/Supporting Files/Info.plist"; INFOPLIST_FILE = "Delta/Supporting Files/Info.plist";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.5b4; MARKETING_VERSION = 1.5;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"-ld64", "-ld64",
"$(inherited)", "$(inherited)",
); );
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DIMPACTOR"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" -DIMPACTOR";
PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.Delta; PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.Delta;
PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE = "";

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"/> <device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <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="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
@ -19,7 +19,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="separatorColor" systemColor="separatorColor"/> <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"> <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"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/> <color key="textColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
@ -36,15 +36,15 @@
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Player 1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="tls-Hv-Rx2"> <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"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Riley's iPhone" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="vJP-Ie-a9H"> <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"/> <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"/> <color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
@ -59,15 +59,15 @@
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Player 2" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="e3u-x9-IEC"> <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"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="SteelSeries Stratus" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="2OP-A1-VYo"> <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"/> <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"/> <color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
@ -82,15 +82,15 @@
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Player 3" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Cdn-11-xZe"> <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"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="MOGA Gamepad" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="wWc-NY-Bsd"> <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"/> <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"/> <color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
@ -105,15 +105,15 @@
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Player 4" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Hls-3b-EaS"> <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"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Jayce's iPhone" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="hNf-uc-PLR"> <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"/> <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"/> <color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
@ -386,15 +386,15 @@
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Service" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="4U1-fe-PIb"> <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"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Google Drive" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="kLY-5g-v8n"> <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"/> <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"/> <color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
@ -505,11 +505,38 @@
</tableViewCell> </tableViewCell>
</cells> </cells>
</tableViewSection> </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> <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"/> <rect key="frame" x="16" y="1570.5" width="343" height="44"/>
<autoresizingMask key="autoresizingMask"/> <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"> <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"/> <rect key="frame" x="0.0" y="0.0" width="316.5" height="44"/>
<autoresizingMask key="autoresizingMask"/> <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"> <tableViewSection headerTitle="Patreon" footerTitle="Receive early access to new features and more by becoming a patron." id="QvT-Yt-oP1">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Cell" textLabel="faT-qa-JP0" style="IBUITableViewCellStyleDefault" id="4it-3L-j8P"> <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"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="4it-3L-j8P" id="7dE-36-hzp"> <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"/> <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"> <tableViewSection headerTitle="Credits" id="foh-L9-g6W">
<cells> <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"> <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"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="BU4-ee-DGz" id="fWf-gm-1sf"> <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"/> <rect key="frame" x="0.0" y="0.0" width="316.5" height="44"/>
@ -571,7 +598,7 @@
</tableViewCellContentView> </tableViewCellContentView>
</tableViewCell> </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"> <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"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="CV9-Df-mUX" id="gLC-z2-rMU"> <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"/> <rect key="frame" x="0.0" y="0.0" width="316.5" height="44"/>
@ -594,7 +621,7 @@
</tableViewCellContentView> </tableViewCellContentView>
</tableViewCell> </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"> <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"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="8qc-0t-Nte" id="jUL-fL-i0n"> <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"/> <rect key="frame" x="0.0" y="0.0" width="316.5" height="44"/>
@ -617,7 +644,7 @@
</tableViewCellContentView> </tableViewCellContentView>
</tableViewCell> </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"> <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"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Zh9-JJ-jEQ" id="VCc-oJ-ODB"> <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"/> <rect key="frame" x="0.0" y="0.0" width="316.5" height="44"/>
@ -640,7 +667,7 @@
</tableViewCellContentView> </tableViewCellContentView>
</tableViewCell> </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"> <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"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="rrX-Bh-zdW" id="b9U-W6-LnS"> <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"/> <rect key="frame" x="0.0" y="0.0" width="316.5" height="44"/>
@ -663,7 +690,7 @@
</tableViewCellContentView> </tableViewCellContentView>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="DetailCell" textLabel="g59-8E-zW7" style="IBUITableViewCellStyleDefault" id="hkv-lx-68h"> <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"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="hkv-lx-68h" id="bNT-kB-3cI"> <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"/> <rect key="frame" x="0.0" y="0.0" width="316.5" height="44"/>
@ -679,7 +706,7 @@
</tableViewCellContentView> </tableViewCellContentView>
</tableViewCell> </tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="DetailCell" textLabel="2K3-IL-94S" style="IBUITableViewCellStyleDefault" id="j7p-ZK-mHq"> <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"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="j7p-ZK-mHq" id="BqT-yP-OpS"> <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"/> <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="buttonHapticFeedbackEnabledSwitch" destination="g7m-wj-ueP" id="nOj-sc-foi"/>
<outlet property="controllerOpacityLabel" destination="zaz-yD-CYG" id="eUW-u9-xxx"/> <outlet property="controllerOpacityLabel" destination="zaz-yD-CYG" id="eUW-u9-xxx"/>
<outlet property="controllerOpacitySlider" destination="whi-If-wFf" id="6Cx-HY-xLG"/> <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="previewsEnabledSwitch" destination="OJE-9e-9i3" id="Ndg-eN-PPs"/>
<outlet property="respectSilentModeSwitch" destination="mlO-iy-zU2" id="TDT-cx-kCf"/> <outlet property="respectSilentModeSwitch" destination="mlO-iy-zU2" id="TDT-cx-kCf"/>
<outlet property="syncingServiceLabel" destination="kLY-5g-v8n" id="zzx-qM-q1g"/> <outlet property="syncingServiceLabel" destination="kLY-5g-v8n" id="zzx-qM-q1g"/>
@ -828,15 +856,15 @@
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Controller Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="VBO-V1-Wfu"> <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"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="16"/> <fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Player 1" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="tqn-1q-p53"> <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"/> <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"/> <color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>

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

View File

@ -11,6 +11,7 @@ import Foundation
import GBCDeltaCore import GBCDeltaCore
import Harmony import Harmony
import Roxas
@objc(GameSave) @objc(GameSave)
public class GameSave: _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
}
} }

View File

@ -711,12 +711,24 @@ private extension GameViewController
{ {
// AirPlaying, hide all (non-touch) screens. // 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) for (screen, gameView) in zip(screens, self.gameViews)
{ {
gameView.isEnabled = screen.isTouchScreen 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 else
@ -725,7 +737,8 @@ private extension GameViewController
// Most likely this system only has 1 screen, so just hide self.gameView. // Most likely this system only has 1 screen, so just hide self.gameView.
self.gameView.isEnabled = false self.gameView.isEnabled = false
self.gameView.isHidden = true self.gameView.isHidden = false
self.gameView.isAirPlaying = true
} }
} }
else else
@ -736,6 +749,7 @@ private extension GameViewController
{ {
gameView.isEnabled = true gameView.isEnabled = true
gameView.isHidden = false gameView.isHidden = false
gameView.isAirPlaying = false
} }
} }
} }
@ -1252,6 +1266,8 @@ private extension GameViewController
// Implicitly called from updateControllerSkin() // Implicitly called from updateControllerSkin()
// self.updateExternalDisplay() // self.updateExternalDisplay()
self.gameView?.isAirPlaying = true
} }
func updateExternalDisplay() func updateExternalDisplay()
@ -1322,6 +1338,8 @@ private extension GameViewController
} }
self.updateControllerSkin() // Reset TouchControllerSkin + GameViews self.updateControllerSkin() // Reset TouchControllerSkin + GameViews
self.gameView?.isAirPlaying = false
} }
} }

View File

@ -41,6 +41,10 @@ struct ExperimentalFeatures: FeatureContainer
options: ReviewSaveStatesOptions()) options: ReviewSaveStatesOptions())
var reviewSaveStates 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", @Feature(name: "Alternate App Icon",
description: "Change the app icon.", description: "Change the app icon.",
options: AlternateAppIconOptions()) options: AlternateAppIconOptions())

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

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

View File

@ -91,10 +91,17 @@ extension GamesViewController
{ {
super.viewDidLoad() 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 = RSTPlaceholderView(frame: self.view.bounds)
self.placeholderView.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.placeholderView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.placeholderView.textLabel.text = NSLocalizedString("No Games", comment: "") 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.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.view.insertSubview(self.placeholderView, at: 0)
self.pageControl = UIPageControl() self.pageControl = UIPageControl()
@ -350,6 +357,7 @@ private extension GamesViewController
if let viewController = self.viewControllerForIndex(index) if let viewController = self.viewControllerForIndex(index)
{ {
self.pageViewController.view.setHidden(false, animated: animated) self.pageViewController.view.setHidden(false, animated: animated)
self.pageViewController.view.superview?.setHidden(false, animated: animated)
self.placeholderView.setHidden(true, animated: animated) self.placeholderView.setHidden(true, animated: animated)
self.pageViewController.setViewControllers([viewController], direction: .forward, animated: false, completion: nil) self.pageViewController.setViewControllers([viewController], direction: .forward, animated: false, completion: nil)
@ -368,9 +376,16 @@ private extension GamesViewController
self.title = NSLocalizedString("Games", comment: "") self.title = NSLocalizedString("Games", comment: "")
self.pageViewController.view.setHidden(true, animated: animated) self.pageViewController.view.setHidden(true, animated: animated)
self.pageViewController.view.superview?.setHidden(true, animated: animated)
self.placeholderView.setHidden(false, 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 - //MARK: - Importing -

View File

@ -78,33 +78,12 @@ extension LaunchViewController
// Repair database _after_ starting SyncManager so we can access RecordController. // Repair database _after_ starting SyncManager so we can access RecordController.
let isDatabaseRepaired = RSTLaunchCondition(condition: { !UserDefaults.standard.shouldRepairDatabase }) { completionHandler in 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() let repairViewController = RepairDatabaseViewController()
repairViewController.completionHandler = { [weak repairViewController] in repairViewController.completionHandler = { [weak repairViewController] in
repairViewController?.dismiss(animated: true) repairViewController?.dismiss(animated: true)
finish()
UserDefaults.standard.shouldRepairDatabase = false
completionHandler(nil)
} }
let navigationController = UINavigationController(rootViewController: repairViewController) let navigationController = UINavigationController(rootViewController: repairViewController)

View File

@ -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 class ControllersSettingsViewController: UITableViewController
{ {
var playerIndex: Int! { var playerIndex: Int! {

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

View File

@ -120,6 +120,11 @@ class MelonDSCoreSettingsViewController: UITableViewController
self.navigationItem.rightBarButtonItem = nil 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) NotificationCenter.default.addObserver(self, selector: #selector(MelonDSCoreSettingsViewController.willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
} }
@ -235,7 +240,8 @@ private extension MelonDSCoreSettingsViewController
func changeCore() 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 desmumeActionTitle = DS.core.metadata?.name.value ?? DS.core.name
var melonDSActionTitle = MelonDS.core.metadata?.name.value ?? MelonDS.core.name var melonDSActionTitle = MelonDS.core.metadata?.name.value ?? MelonDS.core.name
@ -313,9 +319,17 @@ extension MelonDSCoreSettingsViewController
switch section switch section
{ {
case _ where isSectionHidden(section): return 0 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. 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 override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
@ -400,24 +414,6 @@ extension MelonDSCoreSettingsViewController
return cell 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) override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{ {
switch Section(rawValue: indexPath.section)! 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: "") 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) case .dsBIOS, .dsiBIOS:
} guard #available(iOS 15, *) else { break }
} return nil
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 }
default: break 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 override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
@ -504,14 +521,24 @@ extension MelonDSCoreSettingsViewController
override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat
{ {
let section = Section(rawValue: section)! 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
} }
} }
} }

View File

@ -53,7 +53,7 @@ struct Settings
static func registerDefaults() 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.gameShortcutsMode): GameShortcutsMode.recent.rawValue,
#keyPath(UserDefaults.isButtonHapticFeedbackEnabled): true, #keyPath(UserDefaults.isButtonHapticFeedbackEnabled): true,
#keyPath(UserDefaults.isThumbstickHapticFeedbackEnabled): true, #keyPath(UserDefaults.isThumbstickHapticFeedbackEnabled): true,
@ -63,12 +63,7 @@ struct Settings
#keyPath(UserDefaults.respectSilentMode): true, #keyPath(UserDefaults.respectSilentMode): true,
Settings.preferredCoreSettingsKey(for: .ds): MelonDS.core.identifier] as [String : Any] Settings.preferredCoreSettingsKey(for: .ds): MelonDS.core.identifier] as [String : Any]
#if BETA #if !BETA
// Assume we need to repair database relationships until explicitly set to false.
defaults[#keyPath(UserDefaults.shouldRepairDatabase)] = true
#else
// Manually set MelonDS as preferred DS core in case DeSmuME is cached from a previous version. // 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)) UserDefaults.standard.set(MelonDS.core.identifier, forKey: Settings.preferredCoreSettingsKey(for: .ds))
@ -77,6 +72,12 @@ struct Settings
#endif #endif
UserDefaults.standard.register(defaults: defaults) 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.
}
} }
} }

View File

@ -8,8 +8,10 @@
import UIKit import UIKit
import SafariServices import SafariServices
import QuickLook
import DeltaCore import DeltaCore
import Harmony
import Roxas import Roxas
@ -43,6 +45,12 @@ private extension SettingsViewController
case status case status
} }
enum AdvancedRow: Int, CaseIterable
{
case exportLog
case experimentalFeatures
}
enum CreditsRow: Int, CaseIterable enum CreditsRow: Int, CaseIterable
{ {
case riley case riley
@ -68,6 +76,7 @@ class SettingsViewController: UITableViewController
@IBOutlet private var versionLabel: UILabel! @IBOutlet private var versionLabel: UILabel!
@IBOutlet private var syncingServiceLabel: UILabel! @IBOutlet private var syncingServiceLabel: UILabel!
@IBOutlet private var exportLogActivityIndicatorView: UIActivityIndicatorView!
private var selectionFeedbackGenerator: UISelectionFeedbackGenerator? private var selectionFeedbackGenerator: UISelectionFeedbackGenerator?
@ -75,6 +84,8 @@ class SettingsViewController: UITableViewController
private var syncingConflictsCount = 0 private var syncingConflictsCount = 0
private var _exportedLogURL: URL?
required init?(coder aDecoder: NSCoder) required init?(coder aDecoder: NSCoder)
{ {
super.init(coder: aDecoder) super.init(coder: aDecoder)
@ -104,6 +115,11 @@ class SettingsViewController: UITableViewController
self.versionLabel.text = NSLocalizedString("Delta", comment: "") self.versionLabel.text = NSLocalizedString("Delta", comment: "")
#endif #endif
} }
if #available(iOS 15, *)
{
self.tableView.register(AttributedHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: AttributedHeaderFooterView.reuseIdentifier)
}
} }
override func viewWillAppear(_ animated: Bool) override func viewWillAppear(_ animated: Bool)
@ -196,6 +212,17 @@ private extension SettingsViewController
{ {
switch section 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: case .hapticTouch:
if #available(iOS 13, *) if #available(iOS 13, *)
{ {
@ -294,6 +321,55 @@ private extension SettingsViewController
let hostingController = ExperimentalFeaturesView.makeViewController() let hostingController = ExperimentalFeaturesView.makeViewController()
self.navigationController?.pushViewController(hostingController, animated: true) 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 private extension SettingsViewController
@ -341,6 +417,9 @@ extension SettingsViewController
case .controllers: return 4 case .controllers: return 4
case .controllerSkins: return System.registeredSystems.count case .controllerSkins: return System.registeredSystems.count
case .syncing: return SyncManager.shared.coordinator?.account == nil ? 1 : super.tableView(tableView, numberOfRowsInSection: sectionIndex) case .syncing: return SyncManager.shared.coordinator?.account == nil ? 1 : super.tableView(tableView, numberOfRowsInSection: sectionIndex)
#if !BETA
case .advanced: return 1
#endif
default: default:
if isSectionHidden(section) if isSectionHidden(section)
{ {
@ -363,7 +442,7 @@ extension SettingsViewController
case .controllers: case .controllers:
if indexPath.row == Settings.localControllerPlayerIndex 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 }) 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 .controllerSkins: self.performSegue(withIdentifier: Segue.controllerSkins.rawValue, sender: cell)
case .cores: self.performSegue(withIdentifier: Segue.dsSettings.rawValue, sender: cell) case .cores: self.performSegue(withIdentifier: Segue.dsSettings.rawValue, sender: cell)
case .controllerOpacity, .gameAudio, .hapticFeedback, .hapticTouch, .syncing: break 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: case .patreon:
let patreonURL = URL(string: "altstore://patreon")! let patreonURL = URL(string: "altstore://patreon")!
@ -449,6 +538,17 @@ extension SettingsViewController
primary: primary:
switch Section(rawValue: indexPath.section)! 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: case .credits:
let row = CreditsRow(rawValue: indexPath.row)! let row = CreditsRow(rawValue: indexPath.row)!
switch 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? override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String?
{ {
let section = Section(rawValue: section)! let section = Section(rawValue: section)!
guard !isSectionHidden(section) else { return nil }
if isSectionHidden(section) switch section
{ {
return nil #if !BETA
} case .advanced: return nil
else #endif
{ case .controllerSkins: return nil
return super.tableView(tableView, titleForFooterInSection: section.rawValue) default: return super.tableView(tableView, titleForFooterInSection: section.rawValue)
} }
} }
@ -520,14 +648,47 @@ extension SettingsViewController
override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat
{ {
let section = Section(rawValue: section)! 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
}
}

View File

@ -90,8 +90,17 @@ private extension SyncingServicesViewController
let previousService = self.selectedSyncingService let previousService = self.selectedSyncingService
self.selectedSyncingService = service self.selectedSyncingService = service
// Set to non-nil if we later authenticate. // Same check as below when showing Sign In or Sign Out.
Settings.syncingService = nil 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) if (previousService == nil && service != nil) || (previousService != nil && service == nil)
{ {

View File

@ -197,6 +197,22 @@
<string>Delta uses your microphone to emulate the Nintendo DS microphone.</string> <string>Delta uses your microphone to emulate the Nintendo DS microphone.</string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>Press "OK" to allow Delta to use images from your Photo Library as game artwork.</string> <string>Press "OK" to allow Delta to use images from your Photo Library as game artwork.</string>
<key>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> <key>UIApplicationSceneManifest</key>
<dict> <dict>
<key>UIApplicationSupportsMultipleScenes</key> <key>UIApplicationSupportsMultipleScenes</key>

View File

@ -140,7 +140,7 @@ extension SyncManager
} }
catch 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) self.start(service: service, completionHandler: completionHandler)
@ -166,11 +166,12 @@ extension SyncManager
{ {
case .other(ServiceError.connectionFailed): case .other(ServiceError.connectionFailed):
// Authentication failed due to network connection, but otherwise started successfully so we ignore this error. // 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) completionHandler(.success)
default: default:
// Another authentication error occured, so we'll deauthenticate ourselves. // 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 self.deauthenticate() { (result) in
switch result switch result
@ -187,7 +188,7 @@ extension SyncManager
} }
catch catch
{ {
print("SyncManager.start error:", error) Logger.sync.error("Failed to start SyncManager. \(error.localizedDescription, privacy: .public)")
completionHandler(.failure(error)) completionHandler(.failure(error))
} }
} }

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

View 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

@ -1 +1 @@
Subproject commit d348fc7440198fd91183d2236e3816dee8cc24ee Subproject commit e1901297408ad220aa3c05e4c4fd9660c3420679

View File

@ -130,7 +130,7 @@ SPEC CHECKSUMS:
GoogleSignIn: 7137d297ddc022a7e0aa4619c86d72c909fa7213 GoogleSignIn: 7137d297ddc022a7e0aa4619c86d72c909fa7213
GTMAppAuth: 0ff230db599948a9ad7470ca667337803b3fc4dd GTMAppAuth: 0ff230db599948a9ad7470ca667337803b3fc4dd
GTMSessionFetcher: 5595ec75acf5be50814f81e9189490412bad82ba GTMSessionFetcher: 5595ec75acf5be50814f81e9189490412bad82ba
Harmony: 5fdc51d0a4f2ce7dcd4439becbbdda1fac4c9e3f Harmony: 1d8166b6168eec8cce046c4366765d15f7e3b9fc
MelonDSDeltaCore: 46193f4fd88e4e18e4a5c841b1ae02dc46d1daa6 MelonDSDeltaCore: 46193f4fd88e4e18e4a5c841b1ae02dc46d1daa6
N64DeltaCore: 4eeb468746722952bcd5467ecb9ebe7df070f53a N64DeltaCore: 4eeb468746722952bcd5467ecb9ebe7df070f53a
NESDeltaCore: ffae3bba878fc505bac0914150a695ede7bc9550 NESDeltaCore: ffae3bba878fc505bac0914150a695ede7bc9550

View File

@ -5,7 +5,7 @@
"description": "iOS framework that automatically syncs Core Data databases across different backends.", "description": "iOS framework that automatically syncs Core Data databases across different backends.",
"homepage": "https://github.com/rileytestut/Harmony", "homepage": "https://github.com/rileytestut/Harmony",
"platforms": { "platforms": {
"ios": "12.0" "ios": "14.0"
}, },
"source": { "source": {
"git": "https://github.com/rileytestut/Harmony.git" "git": "https://github.com/rileytestut/Harmony.git"

2
Pods/Manifest.lock generated
View File

@ -130,7 +130,7 @@ SPEC CHECKSUMS:
GoogleSignIn: 7137d297ddc022a7e0aa4619c86d72c909fa7213 GoogleSignIn: 7137d297ddc022a7e0aa4619c86d72c909fa7213
GTMAppAuth: 0ff230db599948a9ad7470ca667337803b3fc4dd GTMAppAuth: 0ff230db599948a9ad7470ca667337803b3fc4dd
GTMSessionFetcher: 5595ec75acf5be50814f81e9189490412bad82ba GTMSessionFetcher: 5595ec75acf5be50814f81e9189490412bad82ba
Harmony: 5fdc51d0a4f2ce7dcd4439becbbdda1fac4c9e3f Harmony: 1d8166b6168eec8cce046c4366765d15f7e3b9fc
MelonDSDeltaCore: 46193f4fd88e4e18e4a5c841b1ae02dc46d1daa6 MelonDSDeltaCore: 46193f4fd88e4e18e4a5c841b1ae02dc46d1daa6
N64DeltaCore: 4eeb468746722952bcd5467ecb9ebe7df070f53a N64DeltaCore: 4eeb468746722952bcd5467ecb9ebe7df070f53a
NESDeltaCore: ffae3bba878fc505bac0914150a695ede7bc9550 NESDeltaCore: ffae3bba878fc505bac0914150a695ede7bc9550

View File

@ -12645,6 +12645,31 @@
}; };
name = Release; 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 */ = { 1E7F8B6EB3D07FA3D8236A4BCB55962F /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 698A11ECCAF009BA9765514FEF63E5D2 /* SNESDeltaCore.release.xcconfig */; baseConfigurationReference = 698A11ECCAF009BA9765514FEF63E5D2 /* SNESDeltaCore.release.xcconfig */;
@ -12795,6 +12820,32 @@
}; };
name = Release; 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 */ = { 3312B6D3B835689CC7619671A911CF1F /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 14C688327822408CE66949114C33CC83 /* GoogleAPIClientForREST.release.xcconfig */; baseConfigurationReference = 14C688327822408CE66949114C33CC83 /* GoogleAPIClientForREST.release.xcconfig */;
@ -13643,32 +13694,6 @@
}; };
name = Debug; 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 */ = { 99FB94707B9559CB88ACBC1B6D5AA3F1 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 9E90A4E912BD6E255A8140BE26A53D08 /* SDWebImage.debug.xcconfig */; baseConfigurationReference = 9E90A4E912BD6E255A8140BE26A53D08 /* SDWebImage.debug.xcconfig */;
@ -14092,31 +14117,6 @@
}; };
name = Debug; 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 */ /* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */ /* Begin XCConfigurationList section */
@ -14294,8 +14294,8 @@
7C3E1190ABE8B5371898BF25BD61572F /* Build configuration list for PBXNativeTarget "Harmony" */ = { 7C3E1190ABE8B5371898BF25BD61572F /* Build configuration list for PBXNativeTarget "Harmony" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
FB4707879DC0D9B9AB55309F19257D69 /* Debug */, 1A1444025E75F6B39111B81990FB1920 /* Debug */,
979B8CE91B81CC23F92BFAF9017FD30E /* Release */, 2F70207C6F18AF342FEC2F08613C804F /* Release */,
); );
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;

View File

@ -10,7 +10,7 @@ Delta is an iOS application that allows you to emulate and play video games for
<p align="center"> <p align="center">
<img src="https://user-images.githubusercontent.com/705880/115471008-203aa480-a1ec-11eb-8aba-237a46799543.png" width=75%><br/> <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> </p>
## Supported Systems ## 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 / Game Boy Color (GBC)
- Game Boy Advance (GBA) - Game Boy Advance (GBA)
- Nintendo DS (DS) - Nintendo DS (DS)
- Sega Genesis / Mega Drive (GEN) **(in progress)** - Sega Genesis / Mega Drive (GEN) **(in beta)**
## Features ## Features
- Accurate, full speed emulation thanks to mature underlying emulator cores. - 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) - [MelonDSDeltaCore](https://github.com/rileytestut/MelonDSDeltaCore)
- [GPGXDeltaCore](https://github.com/rileytestut/GPGXDeltaCore) - [GPGXDeltaCore](https://github.com/rileytestut/GPGXDeltaCore)
## Project Requirements ## Minimum Project Requirements
- Xcode 12 - Xcode 15
- Swift 5+ - Swift 5.9
- iOS 12.2 or later - iOS 14.0
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.
## Compilation Instructions ## Compilation Instructions