Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added tvOS Top Shelf to launch apps from home screen #578

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
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
1 change: 1 addition & 0 deletions Limelight/AppDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) NSString *pcUuidToLoad;
@property (strong, nonatomic) NSString *appToRun;
@property (strong, nonatomic) void (^shortcutCompletionHandler)(BOOL);

@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
Expand Down
24 changes: 24 additions & 0 deletions Limelight/AppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,30 @@ - (void)application:(UIApplication *)application performActionForShortcutItem:(U
}
#endif

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
if ([url.host isEqualToString:@"appClicked"]) {
NSString *query = [url query];
NSArray *params = [query componentsSeparatedByString:@"&"];
NSMutableDictionary *queryParameters = [NSMutableDictionary dictionary];

for (NSString *param in params) {
NSArray *keyValue = [param componentsSeparatedByString:@"="];
if ([keyValue count] == 2) {
queryParameters[keyValue[0]] = keyValue[1];
}
}

NSString *appId = queryParameters[@"app"];
NSString *UUID = queryParameters[@"UUID"];

_pcUuidToLoad = UUID;
_appToRun = appId;

return YES;
}
return NO;
}

- (void)applicationWillResignActive:(UIApplication *)application
{
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
Expand Down
4 changes: 3 additions & 1 deletion Limelight/Database/DataManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
- (void) updateAppsForExistingHost:(TemporaryHost *)host;
- (void) removeHost:(TemporaryHost*)host;
- (void) removeApp:(TemporaryApp*)app;

#if TARGET_OS_TV
- (void)moveAppUpInList:(NSString *)appId;
#endif
- (TemporarySettings*) getSettings;

- (void) updateUniqueId:(NSString*)uniqueId;
Expand Down
172 changes: 171 additions & 1 deletion Limelight/Database/DataManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,58 @@ - (void) updateHost:(TemporaryHost *)host {
}];
}

#if TARGET_OS_TV

- (NSDictionary *)dictionaryFromApp:(App *)app {
return @{@"hostUUID": app.host.uuid, @"hostName": app.host.name, @"name": app.name, @"id": app.id };
}

- (void)moveAppUpInList:(NSString *)appId {
NSUserDefaults *sharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.MoonlightTV"];
NSString *json = [sharedDefaults objectForKey:@"appList"];
NSData *jsonData = [json dataUsingEncoding:NSUTF8StringEncoding];
NSMutableArray *apps = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:nil];

// Identify the selected app and its index
NSDictionary *selectedApp = nil;
NSInteger selectedIndex = NSNotFound;
for (NSDictionary *app in apps) {
if ([app[@"id"] isEqualToString:appId]) {
selectedApp = app;
selectedIndex = [apps indexOfObject:app];
break;
}
}

if (selectedApp && selectedIndex != NSNotFound) {
// Move the app to the top of the list
[apps removeObjectAtIndex:selectedIndex];
[apps insertObject:selectedApp atIndex:0];

// Serialize to JSON and save back to user defaults
NSData *newJsonData = [NSJSONSerialization dataWithJSONObject:apps options:0 error:nil];
NSString *newJsonStr = [[NSString alloc] initWithData:newJsonData encoding:NSUTF8StringEncoding];
[sharedDefaults setObject:newJsonStr forKey:@"appList"];

// Update hostUUIDOrder accordingly
NSString *hostUUID = selectedApp[@"hostUUID"];
NSMutableArray *hostUUIDOrder = [[sharedDefaults objectForKey:@"hostUUIDOrder"] mutableCopy];
if (!hostUUIDOrder) {
hostUUIDOrder = [NSMutableArray array];
}
[hostUUIDOrder removeObject:hostUUID];
[hostUUIDOrder insertObject:hostUUID atIndex:0];
[sharedDefaults setObject:hostUUIDOrder forKey:@"hostUUIDOrder"];

// Synchronize changes
[sharedDefaults synchronize];

} else {
NSLog(@"App with ID %@ not found.", appId);
}
}
#endif

- (void) updateAppsForExistingHost:(TemporaryHost *)host {
[_managedObjectContext performBlockAndWait:^{
Host* parent = [self getHostForTemporaryHost:host withHostRecords:[self fetchRecords:@"Host"]];
Expand Down Expand Up @@ -177,6 +229,8 @@ - (void) removeHost:(TemporaryHost*)host {
if (managedHost != nil) {
[self->_managedObjectContext deleteObject:managedHost];
[self saveData];


}
}];
}
Expand All @@ -186,8 +240,124 @@ - (void) saveData {
if ([_managedObjectContext hasChanges] && ![_managedObjectContext save:&error]) {
Log(LOG_E, @"Unable to save hosts to database: %@", error);
}

[_appDelegate saveContext];


#if TARGET_OS_TV
// Save hosts/apps for Top Shelf
NSArray *hosts = [self fetchRecords:@"Host"];
NSUserDefaults *sharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.MoonlightTV"];
NSString *existingJson = [sharedDefaults objectForKey:@"appList"];

// Retrieve the existing order of host UUIDs
NSMutableArray *storedUUIDOrder = [[sharedDefaults objectForKey:@"hostUUIDOrder"] mutableCopy];
if (!storedUUIDOrder) {
storedUUIDOrder = [NSMutableArray array];
}
// Update storedUUIDOrder if new hosts are added
for (Host* host in hosts) {
if (![storedUUIDOrder containsObject:host.uuid]) {
[storedUUIDOrder addObject:host.uuid];
}
}
// Save the updated order back to User Defaults
[sharedDefaults setObject:storedUUIDOrder forKey:@"hostUUIDOrder"];
[sharedDefaults synchronize];

// Sort hosts by order
hosts = [hosts sortedArrayUsingComparator:^NSComparisonResult(Host* a, Host* b) {
NSUInteger first = [storedUUIDOrder indexOfObject:a.uuid];
NSUInteger second = [storedUUIDOrder indexOfObject:b.uuid];
if (first < second) {
return NSOrderedAscending;
} else if (first > second) {
return NSOrderedDescending;
} else {
return NSOrderedSame;
}
}];

NSArray *existingApps;
if (existingJson != nil) {
existingApps = [NSJSONSerialization JSONObjectWithData:[existingJson dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil];
} else {
existingApps = [NSArray array];
}

NSMutableArray *mutableExistingApps = [existingApps mutableCopy];
NSMutableSet *currentHostUUIDs = [NSMutableSet set];

for (Host* host in hosts) {

[currentHostUUIDs addObject:host.uuid];

if ([host.appList count]>0) {
NSMutableDictionary *hostAppMap = [NSMutableDictionary dictionary];
for (NSDictionary *app in existingApps) {
hostAppMap[app[@"hostUUID"]] = app;
}

NSMutableSet *currentAppIds = [NSMutableSet set];

for (App *app in host.appList) {
[currentAppIds addObject:app.id];
NSDictionary *newAppDict = [self dictionaryFromApp:app];
NSUInteger existingIndex = [mutableExistingApps indexOfObjectPassingTest:^BOOL(NSDictionary *dict, NSUInteger idx, BOOL *stop) {
return [dict[@"id"] isEqualToString:app.id];
}];

if (existingIndex != NSNotFound) {
mutableExistingApps[existingIndex] = newAppDict;
} else {
[mutableExistingApps addObject:newAppDict];
}
}

// Removing apps not in source list for this host
NSIndexSet *indexesToDelete = [mutableExistingApps indexesOfObjectsPassingTest:^BOOL(NSDictionary *dict, NSUInteger idx, BOOL *stop) {
return ![currentAppIds containsObject:dict[@"id"]] && [dict[@"hostUUID"] isEqualToString:host.uuid];
}];
[mutableExistingApps removeObjectsAtIndexes:indexesToDelete];
}
}

// Remove apps belonging to hosts that are no longer there
NSIndexSet *indexesToDelete = [mutableExistingApps indexesOfObjectsPassingTest:^BOOL(NSDictionary *dict, NSUInteger idx, BOOL *stop) {
return ![currentHostUUIDs containsObject:dict[@"hostUUID"]];
}];
[mutableExistingApps removeObjectsAtIndexes:indexesToDelete];

// Partition into separate arrays
NSMutableDictionary<NSString *, NSMutableArray *> *hostToAppsMap = [NSMutableDictionary new];
for (NSDictionary *app in mutableExistingApps) {
NSString *hostUUID = app[@"hostUUID"];
if (!hostToAppsMap[hostUUID]) {
hostToAppsMap[hostUUID] = [NSMutableArray new];
}
[hostToAppsMap[hostUUID] addObject:app];
}

// Sort these arrays
NSArray *sortedKeys = [hostToAppsMap.allKeys sortedArrayUsingComparator:^NSComparisonResult(NSString *a, NSString *b) {
NSUInteger first = [storedUUIDOrder indexOfObject:a];
NSUInteger second = [storedUUIDOrder indexOfObject:b];
return first < second ? NSOrderedAscending : NSOrderedDescending;
}];

// Merge them back
[mutableExistingApps removeAllObjects];
for (NSString *key in sortedKeys) {
[mutableExistingApps addObjectsFromArray:hostToAppsMap[key]];
}

NSData *jsonData = [NSJSONSerialization dataWithJSONObject:mutableExistingApps options:0 error:&error];
if (jsonData) {
NSString *jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
[sharedDefaults setObject:jsonStr forKey:@"appList"];
[sharedDefaults synchronize];
}

#endif
}

- (NSArray*) getHosts {
Expand Down
17 changes: 17 additions & 0 deletions Limelight/UIAppView.m
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,23 @@ - (void) updateAppImage {
if (!(appImage.size.width == 130.f && appImage.size.height == 180.f) && // GFE 2.0
!(appImage.size.width == 628.f && appImage.size.height == 888.f)) { // GFE 3.0
[_appImage setImage:appImage];

// Save to caches for Top Shelf
NSURL *url = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.MoonlightTV"];
if (url && appImage) {
NSURL *cachesURL = [url URLByAppendingPathComponent:@"Library" isDirectory:YES];
cachesURL = [cachesURL URLByAppendingPathComponent:@"Caches" isDirectory:YES];
// Construct the file path
NSString *filePath = [cachesURL path];
filePath = [filePath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-%@", _app.host.uuid, _app.id]];
filePath = [filePath stringByAppendingPathExtension:@"png"];
// Convert NSString to NSURL
NSURL *imageURL = [NSURL fileURLWithPath:filePath];
NSData *imageData = UIImagePNGRepresentation(appImage);
NSError *error = nil;
[imageData writeToURL:imageURL atomically:YES];
}

} else {
noAppImage = true;
}
Expand Down
20 changes: 19 additions & 1 deletion Limelight/ViewControllers/MainFrameViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,19 @@ - (void)alreadyPaired {
[self->_appManager stopRetrieving];
[self->_appManager retrieveAssetsFromHost:host];
[self hideLoadingFrame: nil];

AppDelegate* delegate = (AppDelegate*)[UIApplication sharedApplication].delegate;
if (delegate.appToRun != nil)
{
for (TemporaryApp *app in host.appList) {
if ([app.id isEqualToString:delegate.appToRun]) {
[self appClicked:app view:nil];
break;
}
}
delegate.appToRun = nil;
}

});
}
});
Expand Down Expand Up @@ -850,8 +863,13 @@ - (void) appClicked:(TemporaryApp *)app view:(UIView *)view {
// before we call prepareToStreamApp.
[[self revealViewController] revealToggleAnimated:NO];
}
#endif

#else

DataManager* database = [[DataManager alloc] init];
[database moveAppUpInList:app.id];
#endif

if ([self findRunningApp:app.host]) {
// If there's a running app, display a menu
[self appLongClicked:app view:view];
Expand Down
11 changes: 11 additions & 0 deletions Moonlight TV/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,16 @@
</array>
<key>UIUserInterfaceStyle</key>
<string>Dark</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>moonlight</string>
</array>
<key>CFBundleURLName</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</dict>
</array>
</dict>
</plist>
10 changes: 10 additions & 0 deletions Moonlight TV/Moonlight TV.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?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>com.apple.security.application-groups</key>
<array>
<string>group.MoonlightTV</string>
</array>
</dict>
</plist>
Loading