Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions ui/drivers/cocoa/RetroArchPlaylistManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, strong) NSString *fullPath;
@property (nonatomic, strong, nullable) NSString *corePath;
@property (nonatomic, strong, nullable) NSString *coreName;
// Display name of the playlist/system this game belongs to
@property (nonatomic, strong, nullable) NSString *system;
@end

// Unified playlist management class
Expand All @@ -27,6 +29,13 @@ NS_ASSUME_NONNULL_BEGIN
+ (NSArray<RetroArchPlaylistGame *> *)getAllGames;
+ (nullable RetroArchPlaylistGame *)findGameByFilename:(NSString *)filename;

// Library sharing: serialize the whole library so another app can fetch it.
// Returns a JSON array of game dictionaries.
+ (nullable NSData *)exportAllGamesAsJSONData;
// Same, encoded as a URL-safe base64 string (base64url, no padding) suitable
// for embedding in a callback URL query parameter.
+ (nullable NSString *)exportAllGamesAsBase64URLString;

// History and favorites support for App Intents suggestions
+ (NSArray<RetroArchPlaylistGame *> *)getHistoryGames;
+ (NSArray<RetroArchPlaylistGame *> *)getFavoriteGames;
Expand Down
59 changes: 59 additions & 0 deletions ui/drivers/cocoa/RetroArchPlaylistManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ + (void)enumerateAllPlaylistEntries:(PlaylistEntryBlock _Nonnull)block
game.title = [NSString stringWithUTF8String:entry->label];
game.fullPath = [NSString stringWithUTF8String:entry->path];

/* System/playlist display name (strip trailing ".lpl") */
if ([playlistName.pathExtension.lowercaseString isEqualToString:@"lpl"])
game.system = playlistName.stringByDeletingPathExtension;
else
game.system = playlistName;

/* Extract filename from path */
const char *filename = path_basename(entry->path);
game.filename = [NSString stringWithUTF8String:filename];
Expand Down Expand Up @@ -211,6 +217,59 @@ + (nullable RetroArchPlaylistGame *)findGameByFilename:(NSString * _Nonnull)file
return nil;
}

+ (nullable NSData *)exportAllGamesAsJSONData
{
NSArray<RetroArchPlaylistGame *> *allGames = [self getAllGames];
NSMutableArray<NSDictionary *> *serialized =
[[NSMutableArray alloc] initWithCapacity:allGames.count];

for (RetroArchPlaylistGame *game in allGames) {
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];

// "titleId" is the same <filename> used in the retroarch://game/<filename> launch scheme
dict[@"titleId"] = game.filename ?: @"";
dict[@"titleName"] = game.title ?: @"";
dict[@"filename"] = game.filename ?: @"";
dict[@"gameId"] = game.gameId ?: @"";
dict[@"developer"] = @"";
dict[@"version"] = @"";
if (game.system)
dict[@"system"] = game.system;
if (game.coreName)
dict[@"coreName"] = game.coreName;

[serialized addObject:dict];
}

NSError *error = nil;
NSData *json = [NSJSONSerialization dataWithJSONObject:serialized
options:0
error:&error];
if (!json) {
RARCH_WARN("Failed to serialize game library: %s\n",
[[error localizedDescription] UTF8String]);
return nil;
}

return json;
}

+ (nullable NSString *)exportAllGamesAsBase64URLString
{
NSData *json = [self exportAllGamesAsJSONData];
if (!json)
return nil;

/* URL-safe base64 (base64url) without padding, matching the encoding other
* front-ends use for their library callbacks. */
NSString *encoded = [json base64EncodedStringWithOptions:0];
encoded = [encoded stringByReplacingOccurrencesOfString:@"+" withString:@"-"];
encoded = [encoded stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
encoded = [encoded stringByReplacingOccurrencesOfString:@"=" withString:@""];

return encoded;
}

// Private helper method to extract games from a playlist
+ (NSArray<RetroArchPlaylistGame *> *)getGamesFromPlaylist:(playlist_t *)playlist
withIdPrefix:(NSString *)idPrefix
Expand Down
50 changes: 49 additions & 1 deletion ui/drivers/ui_cocoatouch.m
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@

#include "cocoa/cocoa_common.h"
#include "cocoa/apple_platform.h"
#ifdef HAVE_RETROARCH_PLAYLIST_MANAGER
#import "cocoa/RetroArchPlaylistManager.h"
#endif

#if defined(HAVE_COCOA_METAL)
#include "../../gfx/common/metal_view.h"
Expand Down Expand Up @@ -1147,7 +1150,10 @@ -(BOOL)openRetroArchURL:(NSURL *)url
return YES; // Just bring app to foreground
}

// Handle game launch URL: retroarch://game/filename
// Handle game launch URL: retroarch://game/<filename>
// The <filename> matches the "titleId" exported by the library query below,
// so a frontend app can round-trip a fetched game straight back into a
// launch URL.
if ([url.host isEqualToString:@"game"])
{
NSString *filename = [url.path hasPrefix:@"/"] ? [url.path substringFromIndex:1] : url.path;
Expand All @@ -1158,6 +1164,48 @@ -(BOOL)openRetroArchURL:(NSURL *)url
}
}

#ifdef HAVE_RETROARCH_PLAYLIST_MANAGER
// Handle library query URL: retroarch://library?scheme=<callerScheme>
// Serializes the whole game library and hands it back to the requesting app
// by opening <callerScheme>://retroarch?games=<base64url-encoded-JSON>.
if ([url.host isEqualToString:@"library"])
{
NSURLComponents *comp = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
RARCH_AUTORELEASE(comp);
NSString *caller_scheme = nil;
for (NSURLQueryItem *q in comp.queryItems)
{
if ([q.name isEqualToString:@"scheme"])
caller_scheme = q.value;
}

if (!caller_scheme || caller_scheme.length == 0)
{
RARCH_WARN("Library query missing 'scheme' parameter: %s\n", [[url absoluteString] UTF8String]);
return NO;
}

NSString *encoded = [RetroArchPlaylistManager exportAllGamesAsBase64URLString];
if (!encoded)
{
RARCH_WARN("Failed to export game library for '%s'\n", [caller_scheme UTF8String]);
return NO;
}

NSString *reply = [NSString stringWithFormat:@"%@://retroarch?games=%@", caller_scheme, encoded];
NSURL *replyURL = [NSURL URLWithString:reply];
if (!replyURL)
{
RARCH_WARN("Could not build reply URL for scheme '%s'\n", [caller_scheme UTF8String]);
return NO;
}

RARCH_LOG("Returning game library to '%s'\n", [caller_scheme UTF8String]);
[[UIApplication sharedApplication] openURL:replyURL options:@{} completionHandler:nil];
return YES;
}
#endif

RARCH_LOG("Unknown RetroArch URL format: %s\n", [[url absoluteString] UTF8String]);
return NO;
}
Expand Down
Loading