diff --git a/menu/cbs/menu_cbs_ok.c b/menu/cbs/menu_cbs_ok.c index 2cbad23471..b998856268 100644 --- a/menu/cbs/menu_cbs_ok.c +++ b/menu/cbs/menu_cbs_ok.c @@ -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; diff --git a/menu/menu_setting.c b/menu/menu_setting.c index d4c76e47a7..82569bdf01 100644 --- a/menu/menu_setting.c +++ b/menu/menu_setting.c @@ -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; diff --git a/pkg/apple/RetroArchTopShelfExtension/ContentProvider.h b/pkg/apple/RetroArchTopShelfExtension/ContentProvider.h new file mode 100644 index 0000000000..004de6fdd6 --- /dev/null +++ b/pkg/apple/RetroArchTopShelfExtension/ContentProvider.h @@ -0,0 +1,17 @@ +// +// ContentProvider.h +// RetroArchTopShelfExtension +// +// Created by Eric Warmenhoven on 2/17/24. +// Copyright © 2024 RetroArch. All rights reserved. +// + +#import + +#define kRetroArchAppGroup @"group.com.libretro.dist.tvos.RetroArchAppGroup" + +@interface ContentProvider : TVTopShelfContentProvider + + +@end + diff --git a/pkg/apple/RetroArchTopShelfExtension/ContentProvider.m b/pkg/apple/RetroArchTopShelfExtension/ContentProvider.m new file mode 100644 index 0000000000..e15bdcfe62 --- /dev/null +++ b/pkg/apple/RetroArchTopShelfExtension/ContentProvider.m @@ -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 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 *collection = [[TVTopShelfItemCollection alloc] initWithItems:items]; + collection.title = key; + [collections addObject:collection]; + } + TVTopShelfSectionedContent *content = [[TVTopShelfSectionedContent alloc] initWithSections:collections]; + completionHandler(content); +} + +@end diff --git a/pkg/apple/RetroArchTopShelfExtension/Info.plist b/pkg/apple/RetroArchTopShelfExtension/Info.plist new file mode 100644 index 0000000000..818086286c --- /dev/null +++ b/pkg/apple/RetroArchTopShelfExtension/Info.plist @@ -0,0 +1,13 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.tv-top-shelf + NSExtensionPrincipalClass + ContentProvider + + + diff --git a/pkg/apple/RetroArchTopShelfExtension/RetroArchTopShelfExtension.entitlements b/pkg/apple/RetroArchTopShelfExtension/RetroArchTopShelfExtension.entitlements new file mode 100644 index 0000000000..0c67376eba --- /dev/null +++ b/pkg/apple/RetroArchTopShelfExtension/RetroArchTopShelfExtension.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/pkg/apple/RetroArch_iOS13.xcodeproj/project.pbxproj b/pkg/apple/RetroArch_iOS13.xcodeproj/project.pbxproj index 656f1c3f3e..ce88dd1ddc 100644 --- a/pkg/apple/RetroArch_iOS13.xcodeproj/project.pbxproj +++ b/pkg/apple/RetroArch_iOS13.xcodeproj/project.pbxproj @@ -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 = ""; }; + 0712A7752B807AE400C9765F /* ContentProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ContentProvider.m; sourceTree = ""; }; + 0712A7772B807AE400C9765F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 0712A77F2B807F8F00C9765F /* RetroArchTopShelfExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RetroArchTopShelfExtension.entitlements; sourceTree = ""; }; 0714E7132983A5AC00E6B45B /* libMoltenVK.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libMoltenVK.dylib; path = tvOS/modules/libMoltenVK.dylib; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; 07B7872C29E8FE8F0088B74F /* filters */ = {isa = PBXFileReference; lastKnownFileType = folder; path = filters; sourceTree = ""; }; @@ -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 = ""; + }; 0789FC2D2A07845300D042B7 /* Packages */ = { isa = PBXGroup; children = ( @@ -547,6 +594,7 @@ 92E5DCD3231A5786006491BF /* modules */, 926C77E221FD1E6700103EDE /* Assets.xcassets */, 926C77E421FD1E6700103EDE /* Info.plist */, + 073DB2892B8706490001BA32 /* RetroArchTV.entitlements */, ); path = tvOS; sourceTree = ""; @@ -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 = ""; @@ -1228,6 +1278,7 @@ 96AFAE3116C1D4EA009DE44C /* OpenGLES.framework */, 9292D6E228F549D000E47A75 /* WidgetKit.framework */, 9292D6E428F549D000E47A75 /* SwiftUI.framework */, + 0712A7712B807AE400C9765F /* TVServices.framework */, ); name = Frameworks; sourceTree = ""; @@ -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 = ( diff --git a/pkg/apple/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/retroarch_1440w.png b/pkg/apple/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/retroarch_1440w.png index 6b04cbc9bc..678509bc47 100644 Binary files a/pkg/apple/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/retroarch_1440w.png and b/pkg/apple/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/retroarch_1440w.png differ diff --git a/pkg/apple/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/retroarch_720w.png b/pkg/apple/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/retroarch_720w.png index bc352f3425..1340bfed97 100644 Binary files a/pkg/apple/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/retroarch_720w.png and b/pkg/apple/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image Wide.imageset/retroarch_720w.png differ diff --git a/pkg/apple/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/retroarch_1440.png b/pkg/apple/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/retroarch_1440.png index ca41b40715..0954a023c6 100644 Binary files a/pkg/apple/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/retroarch_1440.png and b/pkg/apple/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/retroarch_1440.png differ diff --git a/pkg/apple/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/retroarch_720.png b/pkg/apple/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/retroarch_720.png index 724a09ff41..1fb1c83690 100644 Binary files a/pkg/apple/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/retroarch_720.png and b/pkg/apple/tvOS/Assets.xcassets/App Icon & Top Shelf Image.brandassets/Top Shelf Image.imageset/retroarch_720.png differ diff --git a/pkg/apple/tvOS/Info.plist b/pkg/apple/tvOS/Info.plist index b02cb2485e..a0ebe6cb14 100644 --- a/pkg/apple/tvOS/Info.plist +++ b/pkg/apple/tvOS/Info.plist @@ -2,6 +2,17 @@ + CFBundleURLTypes + + + CFBundleURLName + RetroArch URL + CFBundleURLSchemes + + retroarch + + + ALTBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) ALTDeviceID diff --git a/pkg/apple/tvOS/RetroArchTV.entitlements b/pkg/apple/tvOS/RetroArchTV.entitlements new file mode 100644 index 0000000000..0c67376eba --- /dev/null +++ b/pkg/apple/tvOS/RetroArchTV.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/retroarch.c b/retroarch.c index 232f16bc48..4a4ca916a3 100644 --- a/retroarch.c +++ b/retroarch.c @@ -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 } } } diff --git a/tasks/task_content.c b/tasks/task_content.c index 99f92bda3e..de2fcf8c9f 100644 --- a/tasks/task_content.c +++ b/tasks/task_content.c @@ -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 } } } diff --git a/ui/drivers/cocoa/apple_platform.h b/ui/drivers/cocoa/apple_platform.h index e838780974..8fd28e6075 100644 --- a/ui/drivers/cocoa/apple_platform.h +++ b/ui/drivers/cocoa/apple_platform.h @@ -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__ diff --git a/ui/drivers/cocoa/cocoa_common.m b/ui/drivers/cocoa/cocoa_common.m index ee0b445a2b..d10cc46b1c 100644 --- a/ui/drivers/cocoa/cocoa_common.m +++ b/ui/drivers/cocoa/cocoa_common.m @@ -27,6 +27,10 @@ #ifdef HAVE_IOS_SWIFT #import "RetroArch-Swift.h" #endif +#if TARGET_OS_TV +#import +#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 diff --git a/ui/drivers/ui_cocoatouch.m b/ui/drivers/ui_cocoatouch.m index ada55176a3..1462e62477 100644 --- a/ui/drivers/ui_cocoatouch.m +++ b/ui/drivers/ui_cocoatouch.m @@ -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 *)options { + if ([[url scheme] isEqualToString:@"retroarch"]) + return [self openRetroArchURL:url]; + NSFileManager *manager = [NSFileManager defaultManager]; NSString *filename = (NSString*)url.path.lastPathComponent; NSError *error = nil;