From 48d080430cd78f0f48db12ea91c67e38825e0e9b Mon Sep 17 00:00:00 2001 From: Anastasiy Safari Date: Sun, 3 Sep 2023 11:17:58 -0700 Subject: [PATCH 1/7] Added tvOS Top Shelf with recent apps --- Limelight/AppDelegate.h | 1 + Limelight/AppDelegate.m | 24 ++ Limelight/Database/DataManager.h | 4 +- Limelight/Database/DataManager.m | 109 ++++++++- Limelight/UIAppView.m | 17 ++ .../ViewControllers/MainFrameViewController.m | 20 +- Moonlight TV/Info.plist | 11 + Moonlight TV/Moonlight TV.entitlements | 10 + Moonlight.xcodeproj/project.pbxproj | 214 +++++++++++++++++- NoAppImage.png | Bin 0 -> 1102 bytes Shelf/ContentProvider.swift | 123 ++++++++++ Shelf/Info.plist | 13 ++ Shelf/Shelf.entitlements | 10 + 13 files changed, 545 insertions(+), 11 deletions(-) create mode 100644 Moonlight TV/Moonlight TV.entitlements create mode 100644 NoAppImage.png create mode 100644 Shelf/ContentProvider.swift create mode 100644 Shelf/Info.plist create mode 100644 Shelf/Shelf.entitlements diff --git a/Limelight/AppDelegate.h b/Limelight/AppDelegate.h index 90deb407..06b3bd6b 100644 --- a/Limelight/AppDelegate.h +++ b/Limelight/AppDelegate.h @@ -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; diff --git a/Limelight/AppDelegate.m b/Limelight/AppDelegate.m index 5db73f57..7ab539c0 100644 --- a/Limelight/AppDelegate.m +++ b/Limelight/AppDelegate.m @@ -39,6 +39,30 @@ - (void)application:(UIApplication *)application performActionForShortcutItem:(U } #endif +- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)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. diff --git a/Limelight/Database/DataManager.h b/Limelight/Database/DataManager.h index d7d4d5ec..5a4a9c1e 100644 --- a/Limelight/Database/DataManager.h +++ b/Limelight/Database/DataManager.h @@ -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; diff --git a/Limelight/Database/DataManager.m b/Limelight/Database/DataManager.m index 1c3144a7..1917e148 100644 --- a/Limelight/Database/DataManager.m +++ b/Limelight/Database/DataManager.m @@ -108,6 +108,46 @@ - (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"]; + NSArray *apps = [NSJSONSerialization JSONObjectWithData:[json dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil]; + + NSMutableArray *newList = [NSMutableArray arrayWithArray:apps]; + + __block NSUInteger targetIndex = NSNotFound; + __block NSUInteger firstIndexForHostApps = NSNotFound; + __block NSString *hostUUID = nil; + + [newList enumerateObjectsUsingBlock:^(NSDictionary *app, NSUInteger idx, BOOL * _Nonnull stop) { + if ([app[@"id"] isEqualToString:appId]) { + targetIndex = idx; + hostUUID = app[@"hostUUID"]; + } + }]; + + [newList enumerateObjectsUsingBlock:^(NSDictionary *app, NSUInteger idx, BOOL * _Nonnull stop) { + if ([app[@"hostUUID"] isEqualToString:hostUUID] && firstIndexForHostApps == NSNotFound) { + firstIndexForHostApps = idx; + } + }]; + + if (targetIndex != NSNotFound && firstIndexForHostApps != NSNotFound) { + // Move target app to the first position within the host's apps + NSDictionary *targetApp = newList[targetIndex]; + [newList removeObjectAtIndex:targetIndex]; + [newList insertObject:targetApp atIndex:firstIndexForHostApps]; + } + +} +#endif + - (void) updateAppsForExistingHost:(TemporaryHost *)host { [_managedObjectContext performBlockAndWait:^{ Host* parent = [self getHostForTemporaryHost:host withHostRecords:[self fetchRecords:@"Host"]]; @@ -177,6 +217,8 @@ - (void) removeHost:(TemporaryHost*)host { if (managedHost != nil) { [self->_managedObjectContext deleteObject:managedHost]; [self saveData]; + + } }]; } @@ -186,8 +228,73 @@ - (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"]; + + 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]; + + 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 { diff --git a/Limelight/UIAppView.m b/Limelight/UIAppView.m index 6ad9cf48..293fec92 100644 --- a/Limelight/UIAppView.m +++ b/Limelight/UIAppView.m @@ -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; } diff --git a/Limelight/ViewControllers/MainFrameViewController.m b/Limelight/ViewControllers/MainFrameViewController.m index fe105e6f..051a6ad1 100644 --- a/Limelight/ViewControllers/MainFrameViewController.m +++ b/Limelight/ViewControllers/MainFrameViewController.m @@ -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; + } + }); } }); @@ -823,8 +836,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]; diff --git a/Moonlight TV/Info.plist b/Moonlight TV/Info.plist index b91ee818..13983d61 100644 --- a/Moonlight TV/Info.plist +++ b/Moonlight TV/Info.plist @@ -56,5 +56,16 @@ UIUserInterfaceStyle Dark + CFBundleURLTypes + + + CFBundleURLSchemes + + moonlight + + CFBundleURLName + $(PRODUCT_BUNDLE_IDENTIFIER) + + diff --git a/Moonlight TV/Moonlight TV.entitlements b/Moonlight TV/Moonlight TV.entitlements new file mode 100644 index 00000000..b12d118a --- /dev/null +++ b/Moonlight TV/Moonlight TV.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.MoonlightTV + + + diff --git a/Moonlight.xcodeproj/project.pbxproj b/Moonlight.xcodeproj/project.pbxproj index 2968af53..312eb46c 100644 --- a/Moonlight.xcodeproj/project.pbxproj +++ b/Moonlight.xcodeproj/project.pbxproj @@ -38,6 +38,10 @@ 98D5856D1C0EA79600F6CC00 /* TemporaryHost.m in Sources */ = {isa = PBXBuildFile; fileRef = 98D5856C1C0EA79600F6CC00 /* TemporaryHost.m */; }; 98D585701C0ED0E800F6CC00 /* TemporarySettings.m in Sources */ = {isa = PBXBuildFile; fileRef = 98D5856F1C0ED0E800F6CC00 /* TemporarySettings.m */; }; DC1F5A07206436B20037755F /* ConnectionHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = DC1F5A06206436B20037755F /* ConnectionHelper.m */; }; + DD65E6482AA13C5F002304D6 /* NoAppImage.png in Resources */ = {isa = PBXBuildFile; fileRef = DD65E6472AA13C5F002304D6 /* NoAppImage.png */; }; + DDB750742A9D3DE000526F05 /* TVServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DDB750732A9D3DE000526F05 /* TVServices.framework */; }; + DDB750772A9D3DE000526F05 /* ContentProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB750762A9D3DE000526F05 /* ContentProvider.swift */; }; + DDB7507B2A9D3DE000526F05 /* Shelf.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = DDB750722A9D3DE000526F05 /* Shelf.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; FB1A674D2131E65900507771 /* KeyboardSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = FB1A674C2131E65900507771 /* KeyboardSupport.m */; }; FB1A67602132419700507771 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FB1A675E2132419700507771 /* Main.storyboard */; }; FB1A67622132419A00507771 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FB1A67612132419A00507771 /* Assets.xcassets */; }; @@ -147,6 +151,13 @@ remoteGlobalIDString = FB290E2D19B37A4E004C83CF; remoteInfo = "moonlight-common"; }; + DDB750792A9D3DE000526F05 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = FB290CE619B2C406004C83CF /* Project object */; + proxyType = 1; + remoteGlobalIDString = DDB750712A9D3DE000526F05; + remoteInfo = Shelf; + }; FB1A68142132509400507771 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 98AB2E7F1CAD46830089BB98 /* moonlight-common.xcodeproj */; @@ -163,6 +174,20 @@ }; /* End PBXContainerItemProxy section */ +/* Begin PBXCopyFilesBuildPhase section */ + DDB7507F2A9D3DE000526F05 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + DDB7507B2A9D3DE000526F05 /* Shelf.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 566E9D2B2770B23A00EF7BFE /* Moonlight v1.7.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.7.xcdatamodel"; sourceTree = ""; }; 693B3A9A218638CD00982F7B /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; @@ -203,6 +228,13 @@ D4746EEA1CBC740C006FB401 /* Moonlight-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "Moonlight-Bridging-Header.h"; path = "Input/Moonlight-Bridging-Header.h"; sourceTree = ""; }; DC1F5A05206436B10037755F /* ConnectionHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ConnectionHelper.h; sourceTree = ""; }; DC1F5A06206436B20037755F /* ConnectionHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ConnectionHelper.m; sourceTree = ""; }; + DD4F2B122A9EB91E00C98CC2 /* Moonlight TV.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Moonlight TV.entitlements"; sourceTree = ""; }; + DD4F2B132A9EB95600C98CC2 /* Shelf.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Shelf.entitlements; sourceTree = ""; }; + DD65E6472AA13C5F002304D6 /* NoAppImage.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = NoAppImage.png; sourceTree = SOURCE_ROOT; }; + DDB750722A9D3DE000526F05 /* Shelf.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Shelf.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + DDB750732A9D3DE000526F05 /* TVServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TVServices.framework; path = Library/Frameworks/TVServices.framework; sourceTree = DEVELOPER_DIR; }; + DDB750762A9D3DE000526F05 /* ContentProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentProvider.swift; sourceTree = ""; }; + DDB750782A9D3DE000526F05 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; FB1A674B2131E65900507771 /* KeyboardSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyboardSupport.h; sourceTree = ""; }; FB1A674C2131E65900507771 /* KeyboardSupport.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KeyboardSupport.m; sourceTree = ""; }; FB1A67532132419700507771 /* Moonlight TV.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Moonlight TV.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -388,6 +420,14 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + DDB7506F2A9D3DE000526F05 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DDB750742A9D3DE000526F05 /* TVServices.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; FB1A67502132419700507771 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -441,9 +481,21 @@ name = Products; sourceTree = ""; }; + DDB750752A9D3DE000526F05 /* Shelf */ = { + isa = PBXGroup; + children = ( + DD65E6472AA13C5F002304D6 /* NoAppImage.png */, + DD4F2B132A9EB95600C98CC2 /* Shelf.entitlements */, + DDB750762A9D3DE000526F05 /* ContentProvider.swift */, + DDB750782A9D3DE000526F05 /* Info.plist */, + ); + path = Shelf; + sourceTree = ""; + }; FB1A67542132419700507771 /* Moonlight TV */ = { isa = PBXGroup; children = ( + DD4F2B122A9EB91E00C98CC2 /* Moonlight TV.entitlements */, FB1A675E2132419700507771 /* Main.storyboard */, FB1A67612132419A00507771 /* Assets.xcassets */, FB1A67632132419A00507771 /* Info.plist */, @@ -460,6 +512,7 @@ 98AB2E7F1CAD46830089BB98 /* moonlight-common.xcodeproj */, FB290CF919B2C406004C83CF /* Moonlight */, FB1A67542132419700507771 /* Moonlight TV */, + DDB750752A9D3DE000526F05 /* Shelf */, FB290CF019B2C406004C83CF /* Frameworks */, FB290CEF19B2C406004C83CF /* Products */, ); @@ -470,6 +523,7 @@ children = ( FB290CEE19B2C406004C83CF /* Moonlight.app */, FB1A67532132419700507771 /* Moonlight TV.app */, + DDB750722A9D3DE000526F05 /* Shelf.appex */, ); name = Products; sourceTree = ""; @@ -492,6 +546,7 @@ FB290CF319B2C406004C83CF /* CoreGraphics.framework */, FB290CF519B2C406004C83CF /* UIKit.framework */, FB290CF719B2C406004C83CF /* CoreData.framework */, + DDB750732A9D3DE000526F05 /* TVServices.framework */, ); name = Frameworks; sourceTree = ""; @@ -851,6 +906,23 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + DDB750712A9D3DE000526F05 /* Shelf */ = { + isa = PBXNativeTarget; + buildConfigurationList = DDB7507C2A9D3DE000526F05 /* Build configuration list for PBXNativeTarget "Shelf" */; + buildPhases = ( + DDB7506E2A9D3DE000526F05 /* Sources */, + DDB7506F2A9D3DE000526F05 /* Frameworks */, + DDB750702A9D3DE000526F05 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Shelf; + productName = Shelf; + productReference = DDB750722A9D3DE000526F05 /* Shelf.appex */; + productType = "com.apple.product-type.app-extension"; + }; FB1A67522132419700507771 /* Moonlight TV */ = { isa = PBXNativeTarget; buildConfigurationList = FB1A67682132419A00507771 /* Build configuration list for PBXNativeTarget "Moonlight TV" */; @@ -858,11 +930,13 @@ FB1A674F2132419700507771 /* Sources */, FB1A67502132419700507771 /* Frameworks */, FB1A67512132419700507771 /* Resources */, + DDB7507F2A9D3DE000526F05 /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( FB1A68172132509800507771 /* PBXTargetDependency */, + DDB7507A2A9D3DE000526F05 /* PBXTargetDependency */, ); name = "Moonlight TV"; productName = "Moonlight TV"; @@ -893,10 +967,14 @@ FB290CE619B2C406004C83CF /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 0730; + LastSwiftUpdateCheck = 1430; LastUpgradeCheck = 1400; ORGANIZATIONNAME = "Moonlight Game Streaming Project"; TargetAttributes = { + DDB750712A9D3DE000526F05 = { + CreatedOnToolsVersion = 14.3.1; + LastSwiftMigration = 1430; + }; FB1A67522132419700507771 = { CreatedOnToolsVersion = 9.4.1; LastSwiftMigration = 1140; @@ -908,12 +986,11 @@ }; }; FB290CED19B2C406004C83CF = { - DevelopmentTeam = DM46QST4M7; LastSwiftMigration = 1140; }; }; }; - buildConfigurationList = FB290CE919B2C406004C83CF /* Build configuration list for PBXProject "Moonlight" */; + buildConfigurationList = FB290CE919B2C406004C83CF /* Build configuration list for PBXProject "Moonlight macOS" */; compatibilityVersion = "Xcode 8.0"; developmentRegion = en; hasScannedForEncodings = 0; @@ -934,6 +1011,7 @@ targets = ( FB290CED19B2C406004C83CF /* Moonlight */, FB1A67522132419700507771 /* Moonlight TV */, + DDB750712A9D3DE000526F05 /* Shelf */, ); }; /* End PBXProject section */ @@ -956,6 +1034,14 @@ /* End PBXReferenceProxy section */ /* Begin PBXResourcesBuildPhase section */ + DDB750702A9D3DE000526F05 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DD65E6482AA13C5F002304D6 /* NoAppImage.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; FB1A67512132419700507771 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -982,6 +1068,14 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + DDB7506E2A9D3DE000526F05 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DDB750772A9D3DE000526F05 /* ContentProvider.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; FB1A674F2132419700507771 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1099,6 +1193,11 @@ name = "moonlight-common"; targetProxy = 98AB2E851CAD468B0089BB98 /* PBXContainerItemProxy */; }; + DDB7507A2A9D3DE000526F05 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DDB750712A9D3DE000526F05 /* Shelf */; + targetProxy = DDB750792A9D3DE000526F05 /* PBXContainerItemProxy */; + }; FB1A68172132509800507771 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = "moonlight-common-tv"; @@ -1126,9 +1225,95 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + DDB7507D2A9D3DE000526F05 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + 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_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = Shelf/Shelf.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = JA65HS529K; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Shelf/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Shelf; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Moonlight Game Streaming Project. All rights reserved."; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-stream.Moonlight-dev122445.Shelf"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "Limelight/Input/Moonlight-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 16.4; + }; + name = Debug; + }; + DDB7507E2A9D3DE000526F05 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + 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_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = Shelf/Shelf.entitlements; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = JA65HS529K; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Shelf/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Shelf; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2023 Moonlight Game Streaming Project. All rights reserved."; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-stream.Moonlight-dev122445.Shelf"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OBJC_BRIDGING_HEADER = "Limelight/Input/Moonlight-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 16.4; + }; + name = Release; + }; FB1A67662132419A00507771 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = "Launch Image"; CLANG_ANALYZER_NONNULL = YES; @@ -1136,10 +1321,11 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = "Moonlight TV/Moonlight TV.entitlements"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = DM46QST4M7; + DEVELOPMENT_TEAM = JA65HS529K; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_INCREASE_PRECOMPILED_HEADER_SHARING = NO; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -1161,7 +1347,7 @@ ); MARKETING_VERSION = 8.5.0; MTL_ENABLE_DEBUG_INFO = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-stream.Moonlight"; + PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-stream.Moonlight-dev122445"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_OBJC_BRIDGING_HEADER = "Limelight/Input/Moonlight-Bridging-Header.h"; @@ -1176,6 +1362,7 @@ FB1A67672132419A00507771 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = "Launch Image"; CLANG_ANALYZER_NONNULL = YES; @@ -1183,11 +1370,12 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = "Moonlight TV/Moonlight TV.entitlements"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = DM46QST4M7; + DEVELOPMENT_TEAM = JA65HS529K; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_INCREASE_PRECOMPILED_HEADER_SHARING = NO; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -1337,6 +1525,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = JA65HS529K; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Limelight/Limelight-Prefix.pch"; HEADER_SEARCH_PATHS = ( @@ -1357,7 +1546,7 @@ "$(PROJECT_DIR)/libs/SDL2/lib/iOS", ); MARKETING_VERSION = 8.5.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-stream.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-stream.MoonlightiOS-dev"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; SKIP_INSTALL = NO; @@ -1415,6 +1604,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + DDB7507C2A9D3DE000526F05 /* Build configuration list for PBXNativeTarget "Shelf" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DDB7507D2A9D3DE000526F05 /* Debug */, + DDB7507E2A9D3DE000526F05 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; FB1A67682132419A00507771 /* Build configuration list for PBXNativeTarget "Moonlight TV" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1424,7 +1622,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - FB290CE919B2C406004C83CF /* Build configuration list for PBXProject "Moonlight" */ = { + FB290CE919B2C406004C83CF /* Build configuration list for PBXProject "Moonlight macOS" */ = { isa = XCConfigurationList; buildConfigurations = ( FB290D1E19B2C406004C83CF /* Debug */, diff --git a/NoAppImage.png b/NoAppImage.png new file mode 100644 index 0000000000000000000000000000000000000000..59eb1e97c49a894a1486f9611a63b4cf8200ff3b GIT binary patch literal 1102 zcmeAS@N?(olHy`uVBq!ia0vp^c|bgggAGV7;r>4pNUl+PM}Ws5BY94-ch z{rhkIsgN_GLC?eE%wZkFZNH7~8t2V!DvtZDTRk;zVX~c_U6^9&-jB)*eGSSFfQZ8a zh?x2snBjaTJ}7^&`}cGG3I8RNU0hOP!y~7;dTAbbTV+{OJvnOilR2tORX*Ezb1?4M z>vw(Ex=F>;Bo-f$017UhGW#WSo_oa7DY>hXdV;d2md55Exbe7bQ~uSc?I*buJ-Y~dy*rq&^;)iGkKw6JI{&M*Sl+cR zKOEF`MB=EWy5X_*H?ar)I&Mjg%=@;!`a(L(o%|-t=SQmbHJ2_=X8bSjFJk{`Ys%UvGSeO$V@@$Ba{TyM4%ea??#cFB30RILZMimw{*81*R!FJn9>p{%(y zH*<>cozo$Qx1QfIw>W9-UAflG;{~~yTdtil;PIKswqu*gx7zv>)BhYkbXUK3!CQ%S zy1mm+13jK}%H+V-^wp=ID&4C-U~)h*S*1mx_Wu2vYrfX4*$$5_y0(USX)bsxyY6i| zZ|`3p#yejF?T>NHV6#Z>IrDkX-SZB?Ove>n6z^E{8SyZ;3-A`aojBvWTWY~lk(a;Y z<`g&leRxmKG;?d%1ry(x>V2Pe$}z;oGFUrFXwA&&YgU!29;~ z>u%4>n?j_LRgP)QpFclljVVxHIg9VfBa+F$xQ@NvxGeS2y7(WC!A6B|Puwq+Wf#;H zzRst#K(lFgogkwxXH0eVlX=TH*8OEawC?q`{c7%94{p7`T=6uVW&66-Uuw@!KYHuI znyr6?LZt5i1CWO~W7Y$Q%QvfktIXjmU{nWXB3QnGWh$(ELYWRM|AO-`YuQ6) R(}6jk!PC{xWt~$(695A&1MC0* literal 0 HcmV?d00001 diff --git a/Shelf/ContentProvider.swift b/Shelf/ContentProvider.swift new file mode 100644 index 00000000..35241922 --- /dev/null +++ b/Shelf/ContentProvider.swift @@ -0,0 +1,123 @@ +// +// ContentProvider.swift +// Shelf +// +// Created by Anastasy on 8/28/23. +// Copyright © 2023 Moonlight Game Streaming Project. All rights reserved. +// + +import TVServices +import UIKit + +class MyTopShelfContent: NSObject, TVTopShelfContent, NSSecureCoding { + static var supportsSecureCoding: Bool { + return true + } + + var items: [TVTopShelfItem] = [] + + override init() { + super.init() + } + + required init?(coder: NSCoder) { + super.init() + self.items = coder.decodeObject(forKey: "items") as? [TVTopShelfItem] ?? [] + } + + func encode(with coder: NSCoder) { + coder.encode(self.items, forKey: "items") + } +} + +func urlForImage(named name: String) -> URL? { + + if let fileURL = Bundle.main.url(forResource: name, withExtension: "png") { + return fileURL + } + + return nil + +} + +func urlForCachedImage(uuid: String, appId: String) -> URL? { + let appGroupIdentifier = "group.MoonlightTV" + let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) + NSLog("######## LOAD IMAGE URL= \(url)") + + var cachesURL = url!.appendingPathComponent("Library", isDirectory: true).appendingPathComponent("Caches", isDirectory: true) + let imageName = "\(uuid)-\(appId)" + cachesURL = cachesURL.appendingPathComponent(imageName).appendingPathExtension("png") + + NSLog("######## cachesURL= \(cachesURL)") + + if FileManager.default.fileExists(atPath: cachesURL.path) { + return cachesURL + } + + return nil +} + +class ContentProvider: TVTopShelfContentProvider { + + override func loadTopShelfContent(completionHandler: @escaping (TVTopShelfContent?) -> Void) { + + var hostSections: [String: [TVTopShelfSectionedItem]] = [:] + + if let sharedDefaults = UserDefaults(suiteName: "group.MoonlightTV"), + let jsonString = sharedDefaults.string(forKey: "appList"), + let jsonData = jsonString.data(using: .utf8) + { + do { + if let appList = try JSONSerialization.jsonObject(with: jsonData, options: []) + as? [[String: Any]] + { + for appDict in appList { + if let appId = appDict["id"] as? String, + let hostName = appDict["hostName"] as? String, + let hostUUID = appDict["hostUUID"] as? String + { + let item = TVTopShelfSectionedItem(identifier: appId) + item.title = appDict["name"] as? String + + item.setImageURL(urlForImage(named: "NoAppImage"), for: [.screenScale1x, .screenScale2x]) + NSLog("######## SET EMPTY IMAGE") + if let cachedImageURL = urlForCachedImage(uuid: hostUUID, appId: appId) { + NSLog("######## LOAD IMAGE \(cachedImageURL)") + item.setImageURL(cachedImageURL, for: [.screenScale1x, .screenScale2x]) + } else { + item.setImageURL(urlForImage(named: "NoAppImage"), for: [.screenScale1x, .screenScale2x]) + } + + if hostSections[hostName] == nil { + hostSections[hostName] = [] + } + + hostSections[hostName]?.append(item) + + let action = TVTopShelfAction( + url: URL( + string: "moonlight://appClicked?app=\(appId)&UUID=\(hostUUID)")!) + item.playAction = action + item.displayAction = action + } + } + } + } catch { + print("appList deserialization failed: \(error)") + } + } + + var sectionedItemCollections: [TVTopShelfItemCollection] = [] + + for (hostName, items) in hostSections { + let itemCollection = TVTopShelfItemCollection(items: items) + itemCollection.title = "Moonlight: \(hostName)" + sectionedItemCollections.append(itemCollection) + } + + let sectionedContent = TVTopShelfSectionedContent(sections: sectionedItemCollections) + + completionHandler(sectionedContent) + } +} diff --git a/Shelf/Info.plist b/Shelf/Info.plist new file mode 100644 index 00000000..6f49c9c3 --- /dev/null +++ b/Shelf/Info.plist @@ -0,0 +1,13 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.tv-top-shelf + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).ContentProvider + + + diff --git a/Shelf/Shelf.entitlements b/Shelf/Shelf.entitlements new file mode 100644 index 00000000..b12d118a --- /dev/null +++ b/Shelf/Shelf.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.MoonlightTV + + + From e2b8e7679e69b46aed7458df989b943619add806 Mon Sep 17 00:00:00 2001 From: Anastasiy Safari Date: Sun, 3 Sep 2023 12:33:44 -0700 Subject: [PATCH 2/7] Removed debug info --- Shelf/ContentProvider.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Shelf/ContentProvider.swift b/Shelf/ContentProvider.swift index 35241922..47740e40 100644 --- a/Shelf/ContentProvider.swift +++ b/Shelf/ContentProvider.swift @@ -43,13 +43,11 @@ func urlForImage(named name: String) -> URL? { func urlForCachedImage(uuid: String, appId: String) -> URL? { let appGroupIdentifier = "group.MoonlightTV" let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) - NSLog("######## LOAD IMAGE URL= \(url)") var cachesURL = url!.appendingPathComponent("Library", isDirectory: true).appendingPathComponent("Caches", isDirectory: true) let imageName = "\(uuid)-\(appId)" cachesURL = cachesURL.appendingPathComponent(imageName).appendingPathExtension("png") - NSLog("######## cachesURL= \(cachesURL)") if FileManager.default.fileExists(atPath: cachesURL.path) { return cachesURL @@ -81,9 +79,7 @@ class ContentProvider: TVTopShelfContentProvider { item.title = appDict["name"] as? String item.setImageURL(urlForImage(named: "NoAppImage"), for: [.screenScale1x, .screenScale2x]) - NSLog("######## SET EMPTY IMAGE") if let cachedImageURL = urlForCachedImage(uuid: hostUUID, appId: appId) { - NSLog("######## LOAD IMAGE \(cachedImageURL)") item.setImageURL(cachedImageURL, for: [.screenScale1x, .screenScale2x]) } else { item.setImageURL(urlForImage(named: "NoAppImage"), for: [.screenScale1x, .screenScale2x]) From fc8d24b0bad9ddf6439fe111f7030cf5eb4822f9 Mon Sep 17 00:00:00 2001 From: Anastasiy Safari Date: Mon, 4 Sep 2023 15:04:23 -0700 Subject: [PATCH 3/7] Formatting --- Shelf/ContentProvider.swift | 39 ++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/Shelf/ContentProvider.swift b/Shelf/ContentProvider.swift index 47740e40..79b70193 100644 --- a/Shelf/ContentProvider.swift +++ b/Shelf/ContentProvider.swift @@ -41,19 +41,20 @@ func urlForImage(named name: String) -> URL? { } func urlForCachedImage(uuid: String, appId: String) -> URL? { - let appGroupIdentifier = "group.MoonlightTV" - let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) + let appGroupIdentifier = "group.MoonlightTV" + let url = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: appGroupIdentifier) - var cachesURL = url!.appendingPathComponent("Library", isDirectory: true).appendingPathComponent("Caches", isDirectory: true) - let imageName = "\(uuid)-\(appId)" - cachesURL = cachesURL.appendingPathComponent(imageName).appendingPathExtension("png") - + var cachesURL = url!.appendingPathComponent("Library", isDirectory: true).appendingPathComponent( + "Caches", isDirectory: true) + let imageName = "\(uuid)-\(appId)" + cachesURL = cachesURL.appendingPathComponent(imageName).appendingPathExtension("png") - if FileManager.default.fileExists(atPath: cachesURL.path) { - return cachesURL - } - - return nil + if FileManager.default.fileExists(atPath: cachesURL.path) { + return cachesURL + } + + return nil } class ContentProvider: TVTopShelfContentProvider { @@ -78,13 +79,15 @@ class ContentProvider: TVTopShelfContentProvider { let item = TVTopShelfSectionedItem(identifier: appId) item.title = appDict["name"] as? String - item.setImageURL(urlForImage(named: "NoAppImage"), for: [.screenScale1x, .screenScale2x]) - if let cachedImageURL = urlForCachedImage(uuid: hostUUID, appId: appId) { - item.setImageURL(cachedImageURL, for: [.screenScale1x, .screenScale2x]) - } else { - item.setImageURL(urlForImage(named: "NoAppImage"), for: [.screenScale1x, .screenScale2x]) - } - + item.setImageURL( + urlForImage(named: "NoAppImage"), for: [.screenScale1x, .screenScale2x]) + if let cachedImageURL = urlForCachedImage(uuid: hostUUID, appId: appId) { + item.setImageURL(cachedImageURL, for: [.screenScale1x, .screenScale2x]) + } else { + item.setImageURL( + urlForImage(named: "NoAppImage"), for: [.screenScale1x, .screenScale2x]) + } + if hostSections[hostName] == nil { hostSections[hostName] = [] } From 37188c74212c0ecd5987225fcf22432192137355 Mon Sep 17 00:00:00 2001 From: Anastasiy Safari Date: Tue, 5 Sep 2023 18:20:11 -0700 Subject: [PATCH 4/7] Error handling, fixed moving apps to top --- Limelight/Database/DataManager.m | 7 ++- Shelf/ContentProvider.swift | 73 ++++++++++++++++---------------- 2 files changed, 43 insertions(+), 37 deletions(-) diff --git a/Limelight/Database/DataManager.m b/Limelight/Database/DataManager.m index 1917e148..c72d26f7 100644 --- a/Limelight/Database/DataManager.m +++ b/Limelight/Database/DataManager.m @@ -144,7 +144,12 @@ - (void)moveAppUpInList:(NSString *)appId { [newList removeObjectAtIndex:targetIndex]; [newList insertObject:targetApp atIndex:firstIndexForHostApps]; } - + + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:newList options:0 error:nil]; + NSString *jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + [sharedDefaults setObject:jsonStr forKey:@"appList"]; + [sharedDefaults synchronize]; + } } #endif diff --git a/Shelf/ContentProvider.swift b/Shelf/ContentProvider.swift index 79b70193..12a3796c 100644 --- a/Shelf/ContentProvider.swift +++ b/Shelf/ContentProvider.swift @@ -42,25 +42,28 @@ func urlForImage(named name: String) -> URL? { func urlForCachedImage(uuid: String, appId: String) -> URL? { let appGroupIdentifier = "group.MoonlightTV" - let url = FileManager.default.containerURL( - forSecurityApplicationGroupIdentifier: appGroupIdentifier) - - var cachesURL = url!.appendingPathComponent("Library", isDirectory: true).appendingPathComponent( - "Caches", isDirectory: true) let imageName = "\(uuid)-\(appId)" - cachesURL = cachesURL.appendingPathComponent(imageName).appendingPathExtension("png") - if FileManager.default.fileExists(atPath: cachesURL.path) { - return cachesURL + guard + let url = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: appGroupIdentifier) + else { + return nil } - return nil + let cachesURL = + url + .appendingPathComponent("Library", isDirectory: true) + .appendingPathComponent("Caches", isDirectory: true) + .appendingPathComponent(imageName) + .appendingPathExtension("png") + + return FileManager.default.fileExists(atPath: cachesURL.path) ? cachesURL : nil } class ContentProvider: TVTopShelfContentProvider { override func loadTopShelfContent(completionHandler: @escaping (TVTopShelfContent?) -> Void) { - var hostSections: [String: [TVTopShelfSectionedItem]] = [:] if let sharedDefaults = UserDefaults(suiteName: "group.MoonlightTV"), @@ -79,44 +82,42 @@ class ContentProvider: TVTopShelfContentProvider { let item = TVTopShelfSectionedItem(identifier: appId) item.title = appDict["name"] as? String - item.setImageURL( - urlForImage(named: "NoAppImage"), for: [.screenScale1x, .screenScale2x]) + let defaultImageURL = urlForImage(named: "NoAppImage") + item.setImageURL(defaultImageURL, for: [.screenScale1x, .screenScale2x]) + if let cachedImageURL = urlForCachedImage(uuid: hostUUID, appId: appId) { item.setImageURL(cachedImageURL, for: [.screenScale1x, .screenScale2x]) - } else { - item.setImageURL( - urlForImage(named: "NoAppImage"), for: [.screenScale1x, .screenScale2x]) - } - - if hostSections[hostName] == nil { - hostSections[hostName] = [] } - hostSections[hostName]?.append(item) + hostSections[hostName, default: []].append(item) - let action = TVTopShelfAction( - url: URL( - string: "moonlight://appClicked?app=\(appId)&UUID=\(hostUUID)")!) - item.playAction = action - item.displayAction = action + if let actionURL = URL(string: "moonlight://appClicked?app=\(appId)&UUID=\(hostUUID)") + { + let action = TVTopShelfAction(url: actionURL) + item.playAction = action + item.displayAction = action + } } } } + var sectionedItemCollections: [TVTopShelfItemCollection] = [] + + for (hostName, items) in hostSections { + let itemCollection = TVTopShelfItemCollection(items: items) + itemCollection.title = "Moonlight: \(hostName)" + sectionedItemCollections.append(itemCollection) + } + + let sectionedContent = TVTopShelfSectionedContent(sections: sectionedItemCollections) + completionHandler(sectionedContent) + } catch { print("appList deserialization failed: \(error)") + completionHandler(nil) } + } else { + completionHandler(nil) } - var sectionedItemCollections: [TVTopShelfItemCollection] = [] - - for (hostName, items) in hostSections { - let itemCollection = TVTopShelfItemCollection(items: items) - itemCollection.title = "Moonlight: \(hostName)" - sectionedItemCollections.append(itemCollection) - } - - let sectionedContent = TVTopShelfSectionedContent(sections: sectionedItemCollections) - - completionHandler(sectionedContent) } } From 4d8b0257a858c444bc74d1929fe0f98c1bc5ce9a Mon Sep 17 00:00:00 2001 From: Anastasiy Safari Date: Wed, 6 Sep 2023 00:26:08 -0700 Subject: [PATCH 5/7] Fix closing brace --- Limelight/Database/DataManager.m | 1 - 1 file changed, 1 deletion(-) diff --git a/Limelight/Database/DataManager.m b/Limelight/Database/DataManager.m index c72d26f7..2e68a59b 100644 --- a/Limelight/Database/DataManager.m +++ b/Limelight/Database/DataManager.m @@ -149,7 +149,6 @@ - (void)moveAppUpInList:(NSString *)appId { NSString *jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; [sharedDefaults setObject:jsonStr forKey:@"appList"]; [sharedDefaults synchronize]; - } } #endif From b9cf77cc586d6a7a24169783577120fe5d0f7635 Mon Sep 17 00:00:00 2001 From: Anastasiy Safari Date: Thu, 17 Oct 2024 16:31:16 -0700 Subject: [PATCH 6/7] Better handling of host order --- Limelight/Database/DataManager.m | 116 ++++++++++++++++++++++++------- 1 file changed, 89 insertions(+), 27 deletions(-) diff --git a/Limelight/Database/DataManager.m b/Limelight/Database/DataManager.m index 2e68a59b..bc5d14a7 100644 --- a/Limelight/Database/DataManager.m +++ b/Limelight/Database/DataManager.m @@ -117,38 +117,46 @@ - (NSDictionary *)dictionaryFromApp:(App *)app { - (void)moveAppUpInList:(NSString *)appId { NSUserDefaults *sharedDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.MoonlightTV"]; NSString *json = [sharedDefaults objectForKey:@"appList"]; - NSArray *apps = [NSJSONSerialization JSONObjectWithData:[json dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil]; + NSData *jsonData = [json dataUsingEncoding:NSUTF8StringEncoding]; + NSMutableArray *apps = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:nil]; - NSMutableArray *newList = [NSMutableArray arrayWithArray:apps]; - - __block NSUInteger targetIndex = NSNotFound; - __block NSUInteger firstIndexForHostApps = NSNotFound; - __block NSString *hostUUID = nil; - - [newList enumerateObjectsUsingBlock:^(NSDictionary *app, NSUInteger idx, BOOL * _Nonnull stop) { + // Identify the selected app and its index + NSDictionary *selectedApp = nil; + NSInteger selectedIndex = NSNotFound; + for (NSDictionary *app in apps) { if ([app[@"id"] isEqualToString:appId]) { - targetIndex = idx; - hostUUID = app[@"hostUUID"]; + 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]; - [newList enumerateObjectsUsingBlock:^(NSDictionary *app, NSUInteger idx, BOOL * _Nonnull stop) { - if ([app[@"hostUUID"] isEqualToString:hostUUID] && firstIndexForHostApps == NSNotFound) { - firstIndexForHostApps = idx; + // 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]; } - }]; - - if (targetIndex != NSNotFound && firstIndexForHostApps != NSNotFound) { - // Move target app to the first position within the host's apps - NSDictionary *targetApp = newList[targetIndex]; - [newList removeObjectAtIndex:targetIndex]; - [newList insertObject:targetApp atIndex:firstIndexForHostApps]; - } - - NSData *jsonData = [NSJSONSerialization dataWithJSONObject:newList options:0 error:nil]; - NSString *jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; - [sharedDefaults setObject:jsonStr forKey:@"appList"]; + [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 @@ -240,7 +248,35 @@ - (void) saveData { 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]; @@ -290,7 +326,33 @@ - (void) saveData { return ![currentHostUUIDs containsObject:dict[@"hostUUID"]]; }]; [mutableExistingApps removeObjectsAtIndexes:indexesToDelete]; + + // Step 1: Partition into separate arrays + NSMutableDictionary *hostToAppsMap = [NSMutableDictionary new]; + for (NSDictionary *app in mutableExistingApps) { + NSString *hostUUID = app[@"hostUUID"]; + if (!hostToAppsMap[hostUUID]) { + hostToAppsMap[hostUUID] = [NSMutableArray new]; + } + [hostToAppsMap[hostUUID] addObject:app]; + } + + // Step 2: 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; + }]; + + // Step 3: Merge them back + [mutableExistingApps removeAllObjects]; + for (NSString *key in sortedKeys) { + [mutableExistingApps addObjectsFromArray:hostToAppsMap[key]]; + } + // Order fix 4 END + + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:mutableExistingApps options:0 error:&error]; if (jsonData) { NSString *jsonStr = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; From 56bc101e97e0437870b982c0919b769cd5b5a226 Mon Sep 17 00:00:00 2001 From: Anastasiy Safari Date: Sat, 19 Oct 2024 17:56:00 -0700 Subject: [PATCH 7/7] Little refactor --- Limelight/Database/DataManager.m | 9 +++------ Moonlight.xcodeproj/project.pbxproj | 16 ++++++++-------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/Limelight/Database/DataManager.m b/Limelight/Database/DataManager.m index 21b7d196..ca61d177 100644 --- a/Limelight/Database/DataManager.m +++ b/Limelight/Database/DataManager.m @@ -327,8 +327,7 @@ - (void) saveData { }]; [mutableExistingApps removeObjectsAtIndexes:indexesToDelete]; - - // Step 1: Partition into separate arrays + // Partition into separate arrays NSMutableDictionary *hostToAppsMap = [NSMutableDictionary new]; for (NSDictionary *app in mutableExistingApps) { NSString *hostUUID = app[@"hostUUID"]; @@ -338,20 +337,18 @@ - (void) saveData { [hostToAppsMap[hostUUID] addObject:app]; } - // Step 2: Sort these arrays + // 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; }]; - // Step 3: Merge them back + // Merge them back [mutableExistingApps removeAllObjects]; for (NSString *key in sortedKeys) { [mutableExistingApps addObjectsFromArray:hostToAppsMap[key]]; } - // Order fix 4 END - NSData *jsonData = [NSJSONSerialization dataWithJSONObject:mutableExistingApps options:0 error:&error]; if (jsonData) { diff --git a/Moonlight.xcodeproj/project.pbxproj b/Moonlight.xcodeproj/project.pbxproj index 6ca1aa72..22a3a34d 100644 --- a/Moonlight.xcodeproj/project.pbxproj +++ b/Moonlight.xcodeproj/project.pbxproj @@ -1083,7 +1083,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = JA65HS529K; + DEVELOPMENT_TEAM = DM46QST4M7; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Shelf/Info.plist; @@ -1097,7 +1097,7 @@ MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-stream.Moonlight-dev122445.Shelf"; + PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-stream.Moonlight.Shelf"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SKIP_INSTALL = YES; @@ -1126,7 +1126,7 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = JA65HS529K; + DEVELOPMENT_TEAM = DM46QST4M7; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Shelf/Info.plist; @@ -1140,7 +1140,7 @@ MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-stream.Moonlight-dev122445.Shelf"; + PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-stream.Moonlight.Shelf"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SKIP_INSTALL = YES; @@ -1169,7 +1169,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = JA65HS529K; + DEVELOPMENT_TEAM = DM46QST4M7; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_INCREASE_PRECOMPILED_HEADER_SHARING = NO; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -1199,7 +1199,7 @@ ); MARKETING_VERSION = 9.0.2; MTL_ENABLE_DEBUG_INFO = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-stream.Moonlight-dev122445"; + PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-stream.Moonlight"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_OBJC_BRIDGING_HEADER = "Limelight/Input/Moonlight-Bridging-Header.h"; @@ -1227,7 +1227,7 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = JA65HS529K; + DEVELOPMENT_TEAM = DM46QST4M7; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_INCREASE_PRECOMPILED_HEADER_SHARING = NO; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -1387,7 +1387,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = JA65HS529K; + DEVELOPMENT_TEAM = DM46QST4M7; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Limelight/Limelight-Prefix.pch"; HEADER_SEARCH_PATHS = (