[Experimental Feature] Supports AirPlay controller skins
Allows users to customize controller skin when AirPlaying games to an external display.
This commit is contained in:
parent
1137189b57
commit
39522fda58
@ -1 +1 @@
|
|||||||
Subproject commit 3ce43f3103c637dfdb27f85fc0d0041c8a37292b
|
Subproject commit 44a999ab2c974bcbc7bbc71753a61b77fa306c07
|
||||||
@ -1 +1 @@
|
|||||||
Subproject commit b0eeb87c41cf5d78182879a10a51f7c147a60ef7
|
Subproject commit 581fd3557c4ffd2cfb7dd049dfba14ed2f14a96c
|
||||||
@ -198,6 +198,7 @@
|
|||||||
D5AAF27729884F8600F21ACF /* CheatDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AAF27629884F8600F21ACF /* CheatDevice.swift */; };
|
D5AAF27729884F8600F21ACF /* CheatDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AAF27629884F8600F21ACF /* CheatDevice.swift */; };
|
||||||
D5ADD12229F33FBF00CE0560 /* Features.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5ADD12129F33FBF00CE0560 /* Features.swift */; };
|
D5ADD12229F33FBF00CE0560 /* Features.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5ADD12129F33FBF00CE0560 /* Features.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 */; };
|
||||||
D5D797E6298D946200738869 /* Contributor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D797E5298D946200738869 /* Contributor.swift */; };
|
D5D797E6298D946200738869 /* Contributor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D797E5298D946200738869 /* Contributor.swift */; };
|
||||||
D5D797E9298DCC7300738869 /* cheatbase.zip in Resources */ = {isa = PBXBuildFile; fileRef = D5D797E7298DC9E200738869 /* cheatbase.zip */; };
|
D5D797E9298DCC7300738869 /* cheatbase.zip in Resources */ = {isa = PBXBuildFile; fileRef = D5D797E7298DC9E200738869 /* cheatbase.zip */; };
|
||||||
D5D7C1F929E60EA500663793 /* UserDefaults+OptionValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58F39C829E0A702008B4100 /* UserDefaults+OptionValues.swift */; };
|
D5D7C1F929E60EA500663793 /* UserDefaults+OptionValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58F39C829E0A702008B4100 /* UserDefaults+OptionValues.swift */; };
|
||||||
@ -211,6 +212,7 @@
|
|||||||
D5D7C20829E616CF00663793 /* FeatureContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D7C20729E616CF00663793 /* FeatureContainer.swift */; };
|
D5D7C20829E616CF00663793 /* FeatureContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D7C20729E616CF00663793 /* FeatureContainer.swift */; };
|
||||||
D5D7C20A29E61FA600663793 /* OptionToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D7C20929E61FA600663793 /* OptionToggleView.swift */; };
|
D5D7C20A29E61FA600663793 /* OptionToggleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D7C20929E61FA600663793 /* OptionToggleView.swift */; };
|
||||||
D5D7C20C29E624CB00663793 /* DisplayInlineKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D7C20B29E624CB00663793 /* DisplayInlineKey.swift */; };
|
D5D7C20C29E624CB00663793 /* DisplayInlineKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D7C20B29E624CB00663793 /* DisplayInlineKey.swift */; };
|
||||||
|
D5DF06E029F326E6009E577C /* AirPlaySkins.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DF06DE29F326E6009E577C /* AirPlaySkins.swift */; };
|
||||||
D5F82FB82981D3AC00B229AF /* LegacySearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F82FB72981D3AC00B229AF /* LegacySearchBar.swift */; };
|
D5F82FB82981D3AC00B229AF /* LegacySearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F82FB72981D3AC00B229AF /* LegacySearchBar.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
@ -457,6 +459,7 @@
|
|||||||
D5AAF27629884F8600F21ACF /* CheatDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatDevice.swift; sourceTree = "<group>"; };
|
D5AAF27629884F8600F21ACF /* CheatDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatDevice.swift; sourceTree = "<group>"; };
|
||||||
D5ADD12129F33FBF00CE0560 /* Features.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features.swift; sourceTree = "<group>"; };
|
D5ADD12129F33FBF00CE0560 /* Features.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Features.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>"; };
|
||||||
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>"; };
|
||||||
D5D797E7298DC9E200738869 /* cheatbase.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = cheatbase.zip; sourceTree = "<group>"; };
|
D5D797E7298DC9E200738869 /* cheatbase.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = cheatbase.zip; sourceTree = "<group>"; };
|
||||||
D5D7C1E629E5F90200663793 /* OptionValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionValue.swift; sourceTree = "<group>"; };
|
D5D7C1E629E5F90200663793 /* OptionValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionValue.swift; sourceTree = "<group>"; };
|
||||||
@ -465,6 +468,7 @@
|
|||||||
D5D7C20729E616CF00663793 /* FeatureContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureContainer.swift; sourceTree = "<group>"; };
|
D5D7C20729E616CF00663793 /* FeatureContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureContainer.swift; sourceTree = "<group>"; };
|
||||||
D5D7C20929E61FA600663793 /* OptionToggleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionToggleView.swift; sourceTree = "<group>"; };
|
D5D7C20929E61FA600663793 /* OptionToggleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionToggleView.swift; sourceTree = "<group>"; };
|
||||||
D5D7C20B29E624CB00663793 /* DisplayInlineKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayInlineKey.swift; sourceTree = "<group>"; };
|
D5D7C20B29E624CB00663793 /* DisplayInlineKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayInlineKey.swift; sourceTree = "<group>"; };
|
||||||
|
D5DF06DE29F326E6009E577C /* AirPlaySkins.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AirPlaySkins.swift; sourceTree = "<group>"; };
|
||||||
D5F82FB72981D3AC00B229AF /* LegacySearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacySearchBar.swift; sourceTree = "<group>"; };
|
D5F82FB72981D3AC00B229AF /* LegacySearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacySearchBar.swift; sourceTree = "<group>"; };
|
||||||
DC866E433B3BA9AE18ABA1EC /* libPods-Delta.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Delta.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
DC866E433B3BA9AE18ABA1EC /* libPods-Delta.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Delta.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
@ -1084,6 +1088,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D5A9C01C29DE058C00A8D610 /* VariableFastForward.swift */,
|
D5A9C01C29DE058C00A8D610 /* VariableFastForward.swift */,
|
||||||
|
D5DF06DE29F326E6009E577C /* AirPlaySkins.swift */,
|
||||||
);
|
);
|
||||||
path = Features;
|
path = Features;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -1134,6 +1139,7 @@
|
|||||||
D58F39C829E0A702008B4100 /* UserDefaults+OptionValues.swift */,
|
D58F39C829E0A702008B4100 /* UserDefaults+OptionValues.swift */,
|
||||||
D517F6BD29E7535F000D14D0 /* Collection+Optionals.swift */,
|
D517F6BD29E7535F000D14D0 /* Collection+Optionals.swift */,
|
||||||
D54F710329E89DFC009C069A /* NotificationName+Settings.swift */,
|
D54F710329E89DFC009C069A /* NotificationName+Settings.swift */,
|
||||||
|
D5D78AE629F9D40A00E064F0 /* EnvironmentValues+FeatureOption.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -1538,6 +1544,7 @@
|
|||||||
BF5942951E09BD1A0051894B /* NSManagedObjectContext+Conveniences.swift in Sources */,
|
BF5942951E09BD1A0051894B /* NSManagedObjectContext+Conveniences.swift in Sources */,
|
||||||
BFC3628021ADE2BA00EF2BE6 /* UIAlertController+Error.swift in Sources */,
|
BFC3628021ADE2BA00EF2BE6 /* UIAlertController+Error.swift in Sources */,
|
||||||
BF353FF91C5D870B00C1184C /* MenuItem.swift in Sources */,
|
BF353FF91C5D870B00C1184C /* MenuItem.swift in Sources */,
|
||||||
|
D5DF06E029F326E6009E577C /* AirPlaySkins.swift in Sources */,
|
||||||
D5D797E6298D946200738869 /* Contributor.swift in Sources */,
|
D5D797E6298D946200738869 /* Contributor.swift in Sources */,
|
||||||
D5ADD12229F33FBF00CE0560 /* Features.swift in Sources */,
|
D5ADD12229F33FBF00CE0560 /* Features.swift in Sources */,
|
||||||
BFDB3418219E4B1700595A62 /* SyncStatusViewController.swift in Sources */,
|
BFDB3418219E4B1700595A62 /* SyncStatusViewController.swift in Sources */,
|
||||||
@ -1662,6 +1669,7 @@
|
|||||||
D54F710229E89DCB009C069A /* SettingsUserInfoKey.swift in Sources */,
|
D54F710229E89DCB009C069A /* SettingsUserInfoKey.swift in Sources */,
|
||||||
D5D7C20629E60F6100663793 /* OptionPickerView.swift in Sources */,
|
D5D7C20629E60F6100663793 /* OptionPickerView.swift in Sources */,
|
||||||
D5D7C20329E60F2000663793 /* Option.swift in Sources */,
|
D5D7C20329E60F2000663793 /* Option.swift in Sources */,
|
||||||
|
D5D78AE729F9D40A00E064F0 /* EnvironmentValues+FeatureOption.swift in Sources */,
|
||||||
D5D7C20229E60F2000663793 /* OptionValue.swift in Sources */,
|
D5D7C20229E60F2000663793 /* OptionValue.swift in Sources */,
|
||||||
D5D7C1F929E60EA500663793 /* UserDefaults+OptionValues.swift in Sources */,
|
D5D7C1F929E60EA500663793 /* UserDefaults+OptionValues.swift in Sources */,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -13,16 +13,27 @@ import Harmony
|
|||||||
|
|
||||||
extension ControllerSkinConfigurations
|
extension ControllerSkinConfigurations
|
||||||
{
|
{
|
||||||
init(traits: DeltaCore.ControllerSkin.Traits)
|
init?(traits: DeltaCore.ControllerSkin.Traits)
|
||||||
{
|
{
|
||||||
switch (traits.displayType, traits.orientation)
|
switch (traits.device, traits.displayType, traits.orientation)
|
||||||
{
|
{
|
||||||
case (.standard, .portrait): self = .standardPortrait
|
case (.iphone, .standard, .portrait): self = .iphoneStandardPortrait
|
||||||
case (.standard, .landscape): self = .standardLandscape
|
case (.iphone, .standard, .landscape): self = .iphoneStandardLandscape
|
||||||
case (.edgeToEdge, .portrait): self = .edgeToEdgePortrait
|
case (.iphone, .edgeToEdge, .portrait): self = .iphoneEdgeToEdgePortrait
|
||||||
case (.edgeToEdge, .landscape): self = .edgeToEdgeLandscape
|
case (.iphone, .edgeToEdge, .landscape): self = .iphoneEdgeToEdgeLandscape
|
||||||
case (.splitView, .portrait): self = .splitViewPortrait
|
case (.iphone, .splitView, _): return nil
|
||||||
case (.splitView, .landscape): self = .splitViewLandscape
|
|
||||||
|
case (.ipad, .standard, .portrait): self = .ipadStandardPortrait
|
||||||
|
case (.ipad, .standard, .landscape): self = .ipadStandardLandscape
|
||||||
|
case (.ipad, .edgeToEdge, .portrait): self = .ipadEdgeToEdgePortrait
|
||||||
|
case (.ipad, .edgeToEdge, .landscape): self = .ipadEdgeToEdgeLandscape
|
||||||
|
case (.ipad, .splitView, .portrait): self = .ipadSplitViewPortrait
|
||||||
|
case (.ipad, .splitView, .landscape): self = .ipadSplitViewLandscape
|
||||||
|
|
||||||
|
case (.tv, .standard, .portrait): self = .tvStandardPortrait
|
||||||
|
case (.tv, .standard, .landscape): self = .tvStandardLandscape
|
||||||
|
case (.tv, .edgeToEdge, _): return nil
|
||||||
|
case (.tv, .splitView, _): return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,16 +9,35 @@
|
|||||||
#ifndef ControllerSkinConfigurations_h
|
#ifndef ControllerSkinConfigurations_h
|
||||||
#define ControllerSkinConfigurations_h
|
#define ControllerSkinConfigurations_h
|
||||||
|
|
||||||
|
// Every possible (supported) combination of traits.
|
||||||
typedef NS_OPTIONS(int16_t, ControllerSkinConfigurations)
|
typedef NS_OPTIONS(int16_t, ControllerSkinConfigurations)
|
||||||
{
|
{
|
||||||
ControllerSkinConfigurationStandardPortrait = 1 << 0,
|
/* iPhone */
|
||||||
ControllerSkinConfigurationStandardLandscape = 1 << 1,
|
ControllerSkinConfigurationiPhoneStandardPortrait NS_SWIFT_NAME(iphoneStandardPortrait) = 1 << 0,
|
||||||
|
ControllerSkinConfigurationiPhoneStandardLandscape NS_SWIFT_NAME(iphoneStandardLandscape) = 1 << 1,
|
||||||
|
|
||||||
ControllerSkinConfigurationSplitViewPortrait = 1 << 2,
|
// iPhone doesn't support Split View
|
||||||
ControllerSkinConfigurationSplitViewLandscape = 1 << 3,
|
// ControllerSkinConfigurationiPhoneSplitViewPortrait = 1 << 2,
|
||||||
|
// ControllerSkinConfigurationiPhoneSplitViewLandscape = 1 << 3,
|
||||||
|
|
||||||
ControllerSkinConfigurationEdgeToEdgePortrait = 1 << 4,
|
ControllerSkinConfigurationiPhoneEdgeToEdgePortrait NS_SWIFT_NAME(iphoneEdgeToEdgePortrait) = 1 << 4,
|
||||||
ControllerSkinConfigurationEdgeToEdgeLandscape = 1 << 5,
|
ControllerSkinConfigurationiPhoneEdgeToEdgeLandscape NS_SWIFT_NAME(iphoneEdgeToEdgeLandscape) = 1 << 5,
|
||||||
|
|
||||||
|
|
||||||
|
/* iPad */
|
||||||
|
ControllerSkinConfigurationiPadStandardPortrait NS_SWIFT_NAME(ipadStandardPortrait) = 1 << 6,
|
||||||
|
ControllerSkinConfigurationiPadStandardLandscape NS_SWIFT_NAME(ipadStandardLandscape) = 1 << 7,
|
||||||
|
|
||||||
|
ControllerSkinConfigurationiPadSplitViewPortrait NS_SWIFT_NAME(ipadSplitViewPortrait) = 1 << 2, // Backwards compatible with legacy ControllerSkinConfigurationSplitViewPortrait
|
||||||
|
ControllerSkinConfigurationiPadSplitViewLandscape NS_SWIFT_NAME(ipadSplitViewLandscape) = 1 << 3, // Backwards compatible with legacy ControllerSkinConfigurationSplitViewLandscape
|
||||||
|
|
||||||
|
ControllerSkinConfigurationiPadEdgeToEdgePortrait NS_SWIFT_NAME(ipadEdgeToEdgePortrait) = 1 << 8,
|
||||||
|
ControllerSkinConfigurationiPadEdgeToEdgeLandscape NS_SWIFT_NAME(ipadEdgeToEdgeLandscape) = 1 << 9,
|
||||||
|
|
||||||
|
|
||||||
|
/* TV */
|
||||||
|
ControllerSkinConfigurationTVStandardPortrait = 1 << 10,
|
||||||
|
ControllerSkinConfigurationTVStandardLandscape = 1 << 11,
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif /* ControllerSkinConfigurations_h */
|
#endif /* ControllerSkinConfigurations_h */
|
||||||
|
|||||||
@ -1144,7 +1144,13 @@ private extension GameViewController
|
|||||||
|
|
||||||
if let game = self.game, let traits = scene.gameViewController.controllerView.controllerSkinTraits
|
if let game = self.game, let traits = scene.gameViewController.controllerView.controllerSkinTraits
|
||||||
{
|
{
|
||||||
if let standardSkin = DeltaCore.ControllerSkin.standardControllerSkin(for: game.type), standardSkin.supports(traits)
|
if ExperimentalFeatures.shared.airPlaySkins.isEnabled,
|
||||||
|
let preferredControllerSkin = ExperimentalFeatures.shared.airPlaySkins.preferredAirPlayControllerSkin(for: game.type), preferredControllerSkin.supports(traits)
|
||||||
|
{
|
||||||
|
// Use preferredControllerSkin directly.
|
||||||
|
controllerSkin = preferredControllerSkin
|
||||||
|
}
|
||||||
|
else if let standardSkin = DeltaCore.ControllerSkin.standardControllerSkin(for: game.type), standardSkin.supports(traits)
|
||||||
{
|
{
|
||||||
if standardSkin.hasTouchScreen(for: traits)
|
if standardSkin.hasTouchScreen(for: traits)
|
||||||
{
|
{
|
||||||
@ -1359,6 +1365,11 @@ private extension GameViewController
|
|||||||
case Settings.features.dsAirPlay.$layoutAxis.settingsKey:
|
case Settings.features.dsAirPlay.$layoutAxis.settingsKey:
|
||||||
self.updateExternalDisplay()
|
self.updateExternalDisplay()
|
||||||
|
|
||||||
|
case ExperimentalFeatures.shared.airPlaySkins.settingsKey: fallthrough
|
||||||
|
case _ where settingsName.rawValue.hasPrefix(ExperimentalFeatures.shared.airPlaySkins.settingsKey.rawValue):
|
||||||
|
// Update whenever any of the AirPlay skins have changed.
|
||||||
|
self.updateExternalDisplay()
|
||||||
|
|
||||||
default: break
|
default: break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,11 @@ struct ExperimentalFeatures: FeatureContainer
|
|||||||
{
|
{
|
||||||
static let shared = ExperimentalFeatures()
|
static let shared = ExperimentalFeatures()
|
||||||
|
|
||||||
|
@Feature(name: "AirPlay Skins",
|
||||||
|
description: "Customize the appearance of games when AirPlaying to your TV.",
|
||||||
|
options: AirPlaySkinsOptions())
|
||||||
|
var airPlaySkins
|
||||||
|
|
||||||
@Feature(name: "Variable Fast Forward",
|
@Feature(name: "Variable Fast Forward",
|
||||||
description: "Change the preferred Fast Foward speed per-system. You can also change it by long-pressing the Fast Forward button from the Pause Menu.",
|
description: "Change the preferred Fast Foward speed per-system. You can also change it by long-pressing the Fast Forward button from the Pause Menu.",
|
||||||
options: VariableFastForwardOptions())
|
options: VariableFastForwardOptions())
|
||||||
|
|||||||
167
Delta/Experimental Features/Features/AirPlaySkins.swift
Normal file
167
Delta/Experimental Features/Features/AirPlaySkins.swift
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
//
|
||||||
|
// AirPlaySkins.swift
|
||||||
|
// Delta
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 4/20/23.
|
||||||
|
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
import DeltaFeatures
|
||||||
|
import DeltaCore
|
||||||
|
|
||||||
|
extension Feature where Options == AirPlaySkinsOptions
|
||||||
|
{
|
||||||
|
func preferredAirPlayControllerSkin(for gameType: GameType) -> ControllerSkin?
|
||||||
|
{
|
||||||
|
guard let identifier = self[gameType] else { return nil }
|
||||||
|
|
||||||
|
let predicate = NSPredicate(format: "%K == %@", #keyPath(ControllerSkin.identifier), identifier)
|
||||||
|
let controllerSkin = ControllerSkin.instancesWithPredicate(predicate, inManagedObjectContext: DatabaseManager.shared.viewContext, type: ControllerSkin.self).first
|
||||||
|
return controllerSkin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AirPlaySkinsOptions
|
||||||
|
{
|
||||||
|
@Option(name: "Manage Skins", detailView: { _ in SkinManager() })
|
||||||
|
private var skinManager: String = "" // Hack until I figure out how to support Void properties...
|
||||||
|
|
||||||
|
@Option(name: LocalizedStringKey(System.nes.localizedName), description: "The controller skin used when AirPlaying NES games.", detailView: { SkinPicker(gameType: .nes, controllerSkinID: $0) })
|
||||||
|
var nes: String?
|
||||||
|
|
||||||
|
@Option(name: LocalizedStringKey(System.snes.localizedName), description: "The controller skin used when AirPlaying SNES games.", detailView: { SkinPicker(gameType: .snes, controllerSkinID: $0) })
|
||||||
|
var snes: String?
|
||||||
|
|
||||||
|
@Option(name: LocalizedStringKey(System.genesis.localizedName), description: "The controller skin used when AirPlaying Genesis games.", detailView: { SkinPicker(gameType: .genesis, controllerSkinID: $0) })
|
||||||
|
var genesis: String?
|
||||||
|
|
||||||
|
@Option(name: LocalizedStringKey(System.n64.localizedName), description: "The controller skin used when AirPlaying N64 games.", detailView: { SkinPicker(gameType: .n64, controllerSkinID: $0) })
|
||||||
|
var n64: String?
|
||||||
|
|
||||||
|
@Option(name: LocalizedStringKey(System.gbc.localizedName), description: "The controller skin used when AirPlaying GBC games.", detailView: { SkinPicker(gameType: .gbc, controllerSkinID: $0) })
|
||||||
|
var gbc: String?
|
||||||
|
|
||||||
|
@Option(name: LocalizedStringKey(System.gba.localizedName), description: "The controller skin used when AirPlaying GBA games.", detailView: { SkinPicker(gameType: .gba, controllerSkinID: $0) })
|
||||||
|
var gba: String?
|
||||||
|
|
||||||
|
@Option(name: LocalizedStringKey(System.ds.localizedName), description: "The controller skin used when AirPlaying DS games.", detailView: { SkinPicker(gameType: .ds, controllerSkinID: $0) })
|
||||||
|
var ds: String?
|
||||||
|
|
||||||
|
subscript(gameType: GameType) -> String? {
|
||||||
|
guard let system = System(gameType: gameType) else { return nil }
|
||||||
|
switch system
|
||||||
|
{
|
||||||
|
case .nes: return self.nes
|
||||||
|
case .snes: return self.snes
|
||||||
|
case .genesis: return self.genesis
|
||||||
|
case .n64: return self.n64
|
||||||
|
case .gbc: return self.gbc
|
||||||
|
case .gba: return self.gba
|
||||||
|
case .ds: return self.ds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension AirPlaySkinsOptions
|
||||||
|
{
|
||||||
|
struct SkinPicker: View
|
||||||
|
{
|
||||||
|
let gameType: GameType
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var controllerSkinID: String?
|
||||||
|
|
||||||
|
@FetchRequest
|
||||||
|
private var controllerSkins: FetchedResults<ControllerSkin>
|
||||||
|
|
||||||
|
@Environment(\.featureOption)
|
||||||
|
private var option
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Picker(option.name ?? "", selection: $controllerSkinID) {
|
||||||
|
ForEach(controllerSkins, id: \.identifier) { controllerSkin in
|
||||||
|
Text(controllerSkin.name)
|
||||||
|
.tag(Optional<String>(controllerSkin.identifier)) // Must be Optional<String> in order for selection to work.
|
||||||
|
// .tag(controllerSkin.identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("None")
|
||||||
|
.tag(String?.none)
|
||||||
|
}
|
||||||
|
.pickerStyle(.menu)
|
||||||
|
.displayInline()
|
||||||
|
}
|
||||||
|
|
||||||
|
init(gameType: GameType, controllerSkinID: Binding<String?>)
|
||||||
|
{
|
||||||
|
self.gameType = gameType
|
||||||
|
self._controllerSkinID = controllerSkinID
|
||||||
|
|
||||||
|
let configuration = ControllerSkinConfigurations.tvStandardLandscape
|
||||||
|
|
||||||
|
let predicate = NSPredicate(format: "%K == %@ AND (%K & %d) != 0 AND %K == NO",
|
||||||
|
#keyPath(ControllerSkin.gameType), self.gameType.rawValue,
|
||||||
|
#keyPath(ControllerSkin.supportedConfigurations), configuration.rawValue,
|
||||||
|
#keyPath(ControllerSkin.isStandard))
|
||||||
|
|
||||||
|
self._controllerSkins = FetchRequest(entity: ControllerSkin.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \ControllerSkin.name, ascending: true)], predicate: predicate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SkinManager: View
|
||||||
|
{
|
||||||
|
@FetchRequest(entity: ControllerSkin.entity(),
|
||||||
|
sortDescriptors: [NSSortDescriptor(keyPath: \ControllerSkin.name, ascending: true)],
|
||||||
|
predicate: {
|
||||||
|
let configuration = ControllerSkinConfigurations.tvStandardLandscape
|
||||||
|
return NSPredicate(format: "(%K & %d) != 0 AND %K == NO",
|
||||||
|
#keyPath(ControllerSkin.supportedConfigurations), configuration.rawValue,
|
||||||
|
#keyPath(ControllerSkin.isStandard))
|
||||||
|
}())
|
||||||
|
private var controllerSkins: FetchedResults<ControllerSkin>
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if controllerSkins.isEmpty
|
||||||
|
{
|
||||||
|
Text("No AirPlay Skins")
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
List {
|
||||||
|
ForEach(controllerSkins, id: \.identifier) { controllerSkin in
|
||||||
|
HStack {
|
||||||
|
Text(controllerSkin.name)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if let system = System(gameType: controllerSkin.gameType)
|
||||||
|
{
|
||||||
|
Text(system.localizedShortName)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDelete(perform: deleteAirPlaySkins)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deleteAirPlaySkins(at indexes: IndexSet)
|
||||||
|
{
|
||||||
|
let objectIDs = indexes.map { controllerSkins[$0].objectID }
|
||||||
|
|
||||||
|
DatabaseManager.shared.performBackgroundTask { context in
|
||||||
|
let controllerSkins = objectIDs.compactMap { context.object(with: $0) as? ControllerSkin }
|
||||||
|
for controllerSkin in controllerSkins
|
||||||
|
{
|
||||||
|
context.delete(controllerSkin)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.saveWithErrorLogging()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -34,21 +34,20 @@ extension ControllerSkin
|
|||||||
|
|
||||||
var configurations = ControllerSkinConfigurations()
|
var configurations = ControllerSkinConfigurations()
|
||||||
|
|
||||||
let device: DeltaCore.ControllerSkin.Device = (UIDevice.current.userInterfaceIdiom == .pad) ? .ipad : .iphone
|
let allTraitCombinations = DeltaCore.ControllerSkin.Device.allCases.flatMap { device in
|
||||||
|
DeltaCore.ControllerSkin.DisplayType.allCases.flatMap { displayType in
|
||||||
let traitCollections: [(displayType: DeltaCore.ControllerSkin.DisplayType, orientation: DeltaCore.ControllerSkin.Orientation)] =
|
DeltaCore.ControllerSkin.Orientation.allCases.map { orientation in
|
||||||
[(.standard, .portrait), (.standard, .landscape), (.edgeToEdge, .portrait), (.edgeToEdge, .landscape), (.splitView, .portrait), (.splitView, .landscape)]
|
DeltaCore.ControllerSkin.Traits(device: device, displayType: displayType, orientation: orientation)
|
||||||
|
}
|
||||||
for collection in traitCollections
|
|
||||||
{
|
|
||||||
let traits = DeltaCore.ControllerSkin.Traits(device: device, displayType: collection.displayType, orientation: collection.orientation)
|
|
||||||
if skin.supports(traits)
|
|
||||||
{
|
|
||||||
let configuration = ControllerSkinConfigurations(traits: traits)
|
|
||||||
configurations.formUnion(configuration)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for traits in allTraitCombinations
|
||||||
|
{
|
||||||
|
guard let configuration = ControllerSkinConfigurations(traits: traits), skin.supports(traits) else { continue }
|
||||||
|
configurations.formUnion(configuration)
|
||||||
|
}
|
||||||
|
|
||||||
self.supportedConfigurations = configurations
|
self.supportedConfigurations = configurations
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -108,13 +108,13 @@ private extension ControllerSkinsViewController
|
|||||||
{
|
{
|
||||||
guard let system = self.system, let traits = self.traits else { return }
|
guard let system = self.system, let traits = self.traits else { return }
|
||||||
|
|
||||||
let configuration = ControllerSkinConfigurations(traits: traits)
|
guard let configuration = ControllerSkinConfigurations(traits: traits) else { return }
|
||||||
|
|
||||||
let fetchRequest: NSFetchRequest<ControllerSkin> = ControllerSkin.fetchRequest()
|
let fetchRequest: NSFetchRequest<ControllerSkin> = ControllerSkin.fetchRequest()
|
||||||
|
|
||||||
if traits.device == .iphone && traits.displayType == .edgeToEdge
|
if traits.device == .iphone && traits.displayType == .edgeToEdge
|
||||||
{
|
{
|
||||||
let fallbackConfiguration: ControllerSkinConfigurations = (traits.orientation == .landscape) ? .standardLandscape : .standardPortrait
|
let fallbackConfiguration: ControllerSkinConfigurations = (traits.orientation == .landscape) ? .iphoneStandardLandscape : .iphoneStandardPortrait
|
||||||
|
|
||||||
// Allow selecting skins that only support standard display types as well.
|
// Allow selecting skins that only support standard display types as well.
|
||||||
fetchRequest.predicate = NSPredicate(format: "%K == %@ AND ((%K & %d) != 0 OR (%K & %d) != 0)",
|
fetchRequest.predicate = NSPredicate(format: "%K == %@ AND ((%K & %d) != 0 OR (%K & %d) != 0)",
|
||||||
|
|||||||
@ -59,9 +59,11 @@ struct FeatureDetailView<Feature: AnyFeature>: View
|
|||||||
|
|
||||||
private struct OptionRow<Option: AnyOption, DetailView: View>: View where DetailView == Option.DetailView
|
private struct OptionRow<Option: AnyOption, DetailView: View>: View where DetailView == Option.DetailView
|
||||||
{
|
{
|
||||||
var name: LocalizedStringKey
|
let name: LocalizedStringKey
|
||||||
var value: any LocalizedOptionValue
|
let value: any LocalizedOptionValue
|
||||||
var detailView: DetailView
|
let detailView: DetailView
|
||||||
|
|
||||||
|
let option: Option
|
||||||
|
|
||||||
@State
|
@State
|
||||||
private var displayInline: Bool = false
|
private var displayInline: Bool = false
|
||||||
@ -78,10 +80,16 @@ private struct OptionRow<Option: AnyOption, DetailView: View>: View where Detail
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.value = value
|
self.value = value
|
||||||
self.detailView = detailView
|
self.detailView = detailView
|
||||||
|
|
||||||
|
self.option = option
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
|
let detailView = detailView
|
||||||
|
.environment(\.managedObjectContext, DatabaseManager.shared.viewContext)
|
||||||
|
.environment(\.featureOption, option)
|
||||||
|
|
||||||
if displayInline
|
if displayInline
|
||||||
{
|
{
|
||||||
// Display entire view inline.
|
// Display entire view inline.
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// EnvironmentValues+FeatureOption.swift
|
||||||
|
// DeltaFeatures
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 4/26/23.
|
||||||
|
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
private struct FeatureOptionKey: EnvironmentKey
|
||||||
|
{
|
||||||
|
static let defaultValue: any AnyOption = Option(wrappedValue: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension EnvironmentValues
|
||||||
|
{
|
||||||
|
var featureOption: any AnyOption {
|
||||||
|
get { self[FeatureOptionKey.self] }
|
||||||
|
set { self[FeatureOptionKey.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user