tvOS Top Shelf (#16302)

This puts the History and Favorites playlists (up to five items each)
in the Top Shelf menu. In order for this to be enabled you must build
it yourself and change the app identifiers for the TV app and Top
Shelf extension, and add both of them to an app group.
This commit is contained in:
Eric Warmenhoven 2024-02-28 02:20:32 -05:00 committed by GitHub
parent 0df031a580
commit 7379d33801
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 457 additions and 1 deletions

View file

@ -5941,6 +5941,9 @@ static int action_ok_delete_entry(const char *path,
{
playlist_delete_index(playlist, menu->rpl_entry_selection_ptr);
playlist_write_file(playlist);
#if TARGET_OS_TV
update_topshelf();
#endif
}
new_selection_ptr = menu_st->selection_ptr;

View file

@ -8700,6 +8700,9 @@ static void general_write_handler(rarch_setting_t *setting)
* playlist file (to update maximum capacity) */
retroarch_favorites_deinit();
retroarch_favorites_init();
#if TARGET_OS_TV
update_topshelf();
#endif
}
}
break;

View file

@ -0,0 +1,17 @@
//
// ContentProvider.h
// RetroArchTopShelfExtension
//
// Created by Eric Warmenhoven on 2/17/24.
// Copyright © 2024 RetroArch. All rights reserved.
//
#import <TVServices/TVServices.h>
#define kRetroArchAppGroup @"group.com.libretro.dist.tvos.RetroArchAppGroup"
@interface ContentProvider : TVTopShelfContentProvider
@end

View file

@ -0,0 +1,43 @@
//
// ContentProvider.m
// RetroArchTopShelfExtension
//
// Created by Eric Warmenhoven on 2/17/24.
// Copyright © 2024 RetroArch. All rights reserved.
//
#import "ContentProvider.h"
@implementation ContentProvider
- (void)loadTopShelfContentWithCompletionHandler:(void (^) (id<TVTopShelfContent> content))completionHandler
{
NSUserDefaults *ud = [[NSUserDefaults alloc] initWithSuiteName:kRetroArchAppGroup];
NSDictionary *contentDict = [ud objectForKey:@"topshelf"];
NSMutableArray *collections = [NSMutableArray arrayWithCapacity:[contentDict count]];
for (NSString *key in [contentDict allKeys])
{
NSArray *contentArray = [contentDict objectForKey:key];
NSMutableArray *items = [NSMutableArray arrayWithCapacity:[contentArray count]];
for (NSDictionary *item in contentArray)
{
TVTopShelfSectionedItem *tsitem = [[TVTopShelfSectionedItem alloc] initWithIdentifier:item[@"id"]];
tsitem.title = item[@"title"];
[tsitem setImageURL:[NSURL URLWithString:item[@"img"]] forTraits:(TVTopShelfItemImageTraitScreenScale1x | TVTopShelfItemImageTraitScreenScale2x)];
[tsitem setImageShape:TVTopShelfSectionedItemImageShapeSquare];
[tsitem setDisplayAction:[[TVTopShelfAction alloc] initWithURL:[NSURL URLWithString:item[@"play"]]]];
[items addObject:tsitem];
}
TVTopShelfItemCollection<TVTopShelfSectionedItem *> *collection = [[TVTopShelfItemCollection alloc] initWithItems:items];
collection.title = key;
[collections addObject:collection];
}
TVTopShelfSectionedContent *content = [[TVTopShelfSectionedContent alloc] initWithSections:collections];
completionHandler(content);
}
@end

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.tv-top-shelf</string>
<key>NSExtensionPrincipalClass</key>
<string>ContentProvider</string>
</dict>
</dict>
</plist>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View file

@ -22,6 +22,9 @@
/* Begin PBXBuildFile section */
070A88432A4E7AA9003161C0 /* OpenAL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 070A88422A4E7AA9003161C0 /* OpenAL.framework */; };
0712A7722B807AE400C9765F /* TVServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0712A7712B807AE400C9765F /* TVServices.framework */; };
0712A7762B807AE400C9765F /* ContentProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 0712A7752B807AE400C9765F /* ContentProvider.m */; };
0712A77A2B807AE400C9765F /* RetroArchTopShelfExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 0712A7702B807AE400C9765F /* RetroArchTopShelfExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
0714E7152983A5E500E6B45B /* libMoltenVK.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 0714E7132983A5AC00E6B45B /* libMoltenVK.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
0718BC632ABBAFB6001F2CBE /* Network.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0718BC5F2ABBA807001F2CBE /* Network.framework */; };
0734BB242ADB7FEE00EBDCAD /* libMoltenVK.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 92EDD1622982E40C00AD33B4 /* libMoltenVK.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
@ -129,6 +132,13 @@
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
0712A7782B807AE400C9765F /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 96AFAE1C16C1D4EA009DE44C /* Project object */;
proxyType = 1;
remoteGlobalIDString = 0712A76F2B807AE400C9765F;
remoteInfo = RetroArchTopShelfExtension;
};
9292D6EF28F549D200E47A75 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 96AFAE1C16C1D4EA009DE44C /* Project object */;
@ -139,6 +149,17 @@
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
0712A77B2B807AE400C9765F /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
0712A77A2B807AE400C9765F /* RetroArchTopShelfExtension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
0714E7162983A5E500E6B45B /* Embed Libraries */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@ -176,8 +197,15 @@
/* Begin PBXFileReference section */
070A88422A4E7AA9003161C0 /* OpenAL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenAL.framework; path = System/Library/Frameworks/OpenAL.framework; sourceTree = SDKROOT; };
0712A7702B807AE400C9765F /* RetroArchTopShelfExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = RetroArchTopShelfExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
0712A7712B807AE400C9765F /* TVServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TVServices.framework; path = Library/Frameworks/TVServices.framework; sourceTree = DEVELOPER_DIR; };
0712A7742B807AE400C9765F /* ContentProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ContentProvider.h; sourceTree = "<group>"; };
0712A7752B807AE400C9765F /* ContentProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ContentProvider.m; sourceTree = "<group>"; };
0712A7772B807AE400C9765F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
0712A77F2B807F8F00C9765F /* RetroArchTopShelfExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RetroArchTopShelfExtension.entitlements; sourceTree = "<group>"; };
0714E7132983A5AC00E6B45B /* libMoltenVK.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libMoltenVK.dylib; path = tvOS/modules/libMoltenVK.dylib; sourceTree = "<group>"; };
0718BC5F2ABBA807001F2CBE /* Network.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Network.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS17.0.sdk/System/Library/Frameworks/Network.framework; sourceTree = DEVELOPER_DIR; };
073DB2892B8706490001BA32 /* RetroArchTV.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RetroArchTV.entitlements; sourceTree = "<group>"; };
076CA50C2B695C2C00840EA5 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS17.2.sdk/usr/lib/libz.tbd; sourceTree = DEVELOPER_DIR; };
0789FC2E2A07845300D042B7 /* AltKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AltKit; path = Frameworks/AltKit; sourceTree = "<group>"; };
07B7872C29E8FE8F0088B74F /* filters */ = {isa = PBXFileReference; lastKnownFileType = folder; path = filters; sourceTree = "<group>"; };
@ -468,6 +496,14 @@
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
0712A76D2B807AE400C9765F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
0712A7722B807AE400C9765F /* TVServices.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
9204BE111D319EF300BD49DB /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -520,6 +556,17 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0712A7732B807AE400C9765F /* RetroArchTopShelfExtension */ = {
isa = PBXGroup;
children = (
0712A77F2B807F8F00C9765F /* RetroArchTopShelfExtension.entitlements */,
0712A7742B807AE400C9765F /* ContentProvider.h */,
0712A7752B807AE400C9765F /* ContentProvider.m */,
0712A7772B807AE400C9765F /* Info.plist */,
);
path = RetroArchTopShelfExtension;
sourceTree = "<group>";
};
0789FC2D2A07845300D042B7 /* Packages */ = {
isa = PBXGroup;
children = (
@ -547,6 +594,7 @@
92E5DCD3231A5786006491BF /* modules */,
926C77E221FD1E6700103EDE /* Assets.xcassets */,
926C77E421FD1E6700103EDE /* Info.plist */,
073DB2892B8706490001BA32 /* RetroArchTV.entitlements */,
);
path = tvOS;
sourceTree = "<group>";
@ -1178,6 +1226,7 @@
926C77D821FD1E6500103EDE /* tvOS */,
92CC05CC21FF782C00FF79F0 /* WebServer */,
9292D6E628F549D100E47A75 /* RetroArchWidgetExtension */,
0712A7732B807AE400C9765F /* RetroArchTopShelfExtension */,
96AFAE2816C1D4EA009DE44C /* Frameworks */,
96AFAE2616C1D4EA009DE44C /* Products */,
96AFAE3416C1D4EA009DE44C /* Supporting Files */,
@ -1192,6 +1241,7 @@
9204BE2B1D319EF300BD49DB /* RetroArch.app */,
926C77D721FD1E6500103EDE /* RetroArchTV.app */,
9292D6E128F549D000E47A75 /* RetroArchWidgetExtensionExtension.appex */,
0712A7702B807AE400C9765F /* RetroArchTopShelfExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@ -1228,6 +1278,7 @@
96AFAE3116C1D4EA009DE44C /* OpenGLES.framework */,
9292D6E228F549D000E47A75 /* WidgetKit.framework */,
9292D6E428F549D000E47A75 /* SwiftUI.framework */,
0712A7712B807AE400C9765F /* TVServices.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -1263,6 +1314,23 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
0712A76F2B807AE400C9765F /* RetroArchTopShelfExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 0712A77E2B807AE400C9765F /* Build configuration list for PBXNativeTarget "RetroArchTopShelfExtension" */;
buildPhases = (
0712A76C2B807AE400C9765F /* Sources */,
0712A76D2B807AE400C9765F /* Frameworks */,
0712A76E2B807AE400C9765F /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = RetroArchTopShelfExtension;
productName = RetroArchTopShelfExtension;
productReference = 0712A7702B807AE400C9765F /* RetroArchTopShelfExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
9204BE091D319EF300BD49DB /* RetroArchiOS */ = {
isa = PBXNativeTarget;
buildConfigurationList = 9204BE281D319EF300BD49DB /* Build configuration list for PBXNativeTarget "RetroArchiOS" */;
@ -1298,10 +1366,12 @@
92CC057521FE2D4900FF79F0 /* ShellScript */,
926C77D521FD1E6500103EDE /* Resources */,
0714E7162983A5E500E6B45B /* Embed Libraries */,
0712A77B2B807AE400C9765F /* Embed Foundation Extensions */,
);
buildRules = (
);
dependencies = (
0712A7792B807AE400C9765F /* PBXTargetDependency */,
);
name = RetroArchTV;
packageProductDependencies = (
@ -1372,14 +1442,22 @@
projectRoot = "";
targets = (
9204BE091D319EF300BD49DB /* RetroArchiOS */,
926C77D621FD1E6500103EDE /* RetroArchTV */,
9292D6E028F549D000E47A75 /* RetroArchWidgetExtensionExtension */,
926C77D621FD1E6500103EDE /* RetroArchTV */,
0712A76F2B807AE400C9765F /* RetroArchTopShelfExtension */,
0795205D2B839A99000698BB /* Rebuild assets.zip */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
0712A76E2B807AE400C9765F /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
9204BE211D319EF300BD49DB /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@ -1513,6 +1591,14 @@
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
0712A76C2B807AE400C9765F /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0712A7762B807AE400C9765F /* ContentProvider.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
9204BE0A1D319EF300BD49DB /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@ -1597,6 +1683,11 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
0712A7792B807AE400C9765F /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 0712A76F2B807AE400C9765F /* RetroArchTopShelfExtension */;
targetProxy = 0712A7782B807AE400C9765F /* PBXContainerItemProxy */;
};
9292D6F028F549D200E47A75 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 9292D6E028F549D000E47A75 /* RetroArchWidgetExtensionExtension */;
@ -1616,6 +1707,139 @@
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
0712A77C2B807AE400C9765F /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CODE_SIGN_ENTITLEMENTS = RetroArchTopShelfExtension/RetroArchTopShelfExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1.17.0;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = UK699V5ZS8;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = RetroArchTopShelfExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = RetroArchTopShelfExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 RetroArch. All rights reserved.";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.17.0;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.libretro.dist.tvos.RetroArch.RetroArchTopShelfExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = appletvos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
TARGETED_DEVICE_FAMILY = 3;
TVOS_DEPLOYMENT_TARGET = 13.0;
};
name = Debug;
};
0712A77D2B807AE400C9765F /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CODE_SIGN_ENTITLEMENTS = RetroArchTopShelfExtension/RetroArchTopShelfExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1.17.0;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = UK699V5ZS8;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = RetroArchTopShelfExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = RetroArchTopShelfExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 RetroArch. All rights reserved.";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.17.0;
PRODUCT_BUNDLE_IDENTIFIER = com.libretro.dist.tvos.RetroArch.RetroArchTopShelfExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = appletvos;
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
TARGETED_DEVICE_FAMILY = 3;
TVOS_DEPLOYMENT_TARGET = 13.0;
};
name = Release;
};
0732B0982B83D5CD00CA82CD /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@ -1811,6 +2035,7 @@
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CODE_SIGN_ENTITLEMENTS = tvOS/RetroArchTV.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1.17.0;
@ -1899,6 +2124,7 @@
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CODE_SIGN_ENTITLEMENTS = tvOS/RetroArchTV.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1.17.0;
@ -2358,6 +2584,15 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
0712A77E2B807AE400C9765F /* Build configuration list for PBXNativeTarget "RetroArchTopShelfExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
0712A77C2B807AE400C9765F /* Debug */,
0712A77D2B807AE400C9765F /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
079520602B839A99000698BB /* Build configuration list for PBXAggregateTarget "Rebuild assets.zip" */ = {
isa = XCConfigurationList;
buildConfigurations = (

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -2,6 +2,17 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>RetroArch URL</string>
<key>CFBundleURLSchemes</key>
<array>
<string>retroarch</string>
</array>
</dict>
</array>
<key>ALTBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>ALTDeviceID</key>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

View file

@ -4438,6 +4438,9 @@ bool command_event(enum event_command cmd, void *data)
runloop_msg_queue_push(
msg_hash_to_str(MSG_ADDED_TO_FAVORITES), 1, 180, true, NULL,
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
#if TARGET_OS_TV
update_topshelf();
#endif
}
}
}

View file

@ -1691,6 +1691,9 @@ static void task_push_to_history_list(
entry.entry_slot = runloop_st->entry_state_slot;
command_playlist_push_write(playlist_hist, &entry);
#if TARGET_OS_TV
update_topshelf();
#endif
}
}
}

View file

@ -5,6 +5,7 @@
#include "config_file.h"
extern config_file_t *open_userdefaults_config_file(void);
extern void write_userdefaults_config_file(void);
extern void update_topshelf(void);
#endif
#ifdef __OBJC__

View file

@ -27,6 +27,10 @@
#ifdef HAVE_IOS_SWIFT
#import "RetroArch-Swift.h"
#endif
#if TARGET_OS_TV
#import <TVServices/TVServices.h>
#import "../../pkg/apple/RetroArchTopShelfExtension/ContentProvider.h"
#endif
#endif
#include "../../../configuration.h"
@ -895,3 +899,74 @@ void write_userdefaults_config_file(void)
if (conf)
[NSUserDefaults.standardUserDefaults setObject:conf forKey:@FILE_PATH_MAIN_CONFIG];
}
#if TARGET_OS_TV
static NSDictionary *topshelfDictForEntry(const struct playlist_entry *entry, gfx_thumbnail_path_data_t *path_data)
{
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:@{
@"id": [NSString stringWithUTF8String:entry->path],
@"title": [NSString stringWithUTF8String:entry->label],
}];
if (!string_is_empty(path_data->content_db_name))
{
const char *img_name = NULL;
if (gfx_thumbnail_get_img_name(path_data, &img_name, PLAYLIST_THUMBNAIL_FLAG_STD_NAME))
dict[@"img"] = [NSString stringWithFormat:@"https://thumbnails.libretro.com/%s/Named_Boxarts/%s",
path_data->content_db_name, img_name];
}
NSURLComponents *play = [[NSURLComponents alloc] initWithString:@"retroarch://topshelf"];
[play setQueryItems:@[
[[NSURLQueryItem alloc] initWithName:@"path" value:[NSString stringWithUTF8String:entry->path]],
[[NSURLQueryItem alloc] initWithName:@"core_path" value:[NSString stringWithUTF8String:entry->core_path]],
]];
dict[@"play"] = [play string];
return dict;
}
void update_topshelf(void)
{
if (@available(tvOS 13.0, *))
{
NSUserDefaults *ud = [[NSUserDefaults alloc] initWithSuiteName:kRetroArchAppGroup];
if (!ud)
return;
NSMutableDictionary *contentDict = [NSMutableDictionary dictionaryWithCapacity:2];
const struct playlist_entry *entry;
gfx_thumbnail_path_data_t *thumbnail_path_data = gfx_thumbnail_path_init();
settings_t *settings = config_get_ptr();
bool history_list_enable = settings->bools.history_list_enable;
if (history_list_enable && playlist_size(g_defaults.content_history) > 0)
{
NSMutableArray *array = [NSMutableArray arrayWithCapacity:playlist_size(g_defaults.content_history)];
NSString *key = [NSString stringWithUTF8String:msg_hash_to_str(MENU_ENUM_LABEL_VALUE_HISTORY_TAB)];
for (size_t i = 0; i < 5 && i < playlist_size(g_defaults.content_history); i++)
{
gfx_thumbnail_path_reset(thumbnail_path_data);
gfx_thumbnail_set_content_playlist(thumbnail_path_data, g_defaults.content_history, i);
playlist_get_index(g_defaults.content_history, i, &entry);
[array addObject:topshelfDictForEntry(entry, thumbnail_path_data)];
}
contentDict[key] = array;
}
if (playlist_size(g_defaults.content_favorites) > 0)
{
NSMutableArray *array = [NSMutableArray arrayWithCapacity:playlist_size(g_defaults.content_favorites)];
NSString *key = [NSString stringWithUTF8String:msg_hash_to_str(MENU_ENUM_LABEL_VALUE_FAVORITES_TAB)];
for (size_t i = 0; i < 5 && i < playlist_size(g_defaults.content_favorites); i++)
{
gfx_thumbnail_path_reset(thumbnail_path_data);
gfx_thumbnail_set_content_playlist(thumbnail_path_data, g_defaults.content_favorites, i);
playlist_get_index(g_defaults.content_favorites, i, &entry);
[array addObject:topshelfDictForEntry(entry, thumbnail_path_data)];
}
contentDict[key] = array;
}
[ud setObject:contentDict forKey:@"topshelf"];
[TVTopShelfContentProvider topShelfContentDidChange];
}
}
#endif

View file

@ -34,6 +34,7 @@
#include "../../input/drivers/cocoa_input.h"
#include "../../input/drivers_keyboard/keyboard_event_apple.h"
#include "../../retroarch.h"
#include "../../tasks/task_content.h"
#include "../../verbosity.h"
#ifdef HAVE_MENU
@ -559,6 +560,10 @@ enum
rarch_start_draw_observer();
#if TARGET_OS_TV
update_topshelf();
#endif
#if TARGET_OS_IOS
[MXMetricManager.sharedManager addSubscriber:self];
#endif
@ -571,6 +576,9 @@ enum
- (void)applicationDidEnterBackground:(UIApplication *)application
{
#if TARGET_OS_TV
update_topshelf();
#endif
rarch_stop_draw_observer();
}
@ -613,7 +621,38 @@ enum
#endif
}
-(BOOL)openRetroArchURL:(NSURL *)url
{
if ([url.host isEqualToString:@"topshelf"])
{
NSURLComponents *comp = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
NSString *ns_path, *ns_core_path;
char path[PATH_MAX_LENGTH];
char core_path[PATH_MAX_LENGTH];
content_ctx_info_t content_info = { 0 };
for (NSURLQueryItem *q in comp.queryItems)
{
if ([q.name isEqualToString:@"path"])
ns_path = q.value;
else if ([q.name isEqualToString:@"core_path"])
ns_core_path = q.value;
}
if (!ns_path || !ns_core_path)
return NO;
fill_pathname_expand_special(path, [ns_path UTF8String], sizeof(path));
fill_pathname_expand_special(core_path, [ns_core_path UTF8String], sizeof(core_path));
RARCH_LOG("TopShelf told us to open %s with %s\n", path, core_path);
return task_push_load_content_with_new_core_from_companion_ui(core_path, path,
NULL, NULL, NULL,
&content_info, NULL, NULL);
}
return NO;
}
-(BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
if ([[url scheme] isEqualToString:@"retroarch"])
return [self openRetroArchURL:url];
NSFileManager *manager = [NSFileManager defaultManager];
NSString *filename = (NSString*)url.path.lastPathComponent;
NSError *error = nil;