diff --git a/Limelight/ViewControllers/MainFrameViewController.m b/Limelight/ViewControllers/MainFrameViewController.m index df4c44c5..beada0b0 100644 --- a/Limelight/ViewControllers/MainFrameViewController.m +++ b/Limelight/ViewControllers/MainFrameViewController.m @@ -15,7 +15,6 @@ #import "Utils.h" #import "UIComputerView.h" #import "UIAppView.h" -#import "SettingsViewController.h" #import "DataManager.h" #import "TemporarySettings.h" #import "WakeOnLanManager.h" @@ -28,6 +27,10 @@ #import "IdManager.h" #import "ConnectionHelper.h" +#if !TARGET_OS_TV +#import "SettingsViewController.h" +#endif + #import @implementation MainFrameViewController { @@ -39,38 +42,41 @@ AppAssetManager* _appManager; StreamConfiguration* _streamConfig; UIAlertController* _pairAlert; + LoadingFrameViewController* _loadingFrame; UIScrollView* hostScrollView; int currentPosition; NSArray* _sortedAppList; NSCache* _boxArtCache; - UIButton* _pullArrow; bool _background; +#if !TARGET_OS_TV + UIButton* _pullArrow; +#endif } static NSMutableSet* hostList; - (void)showPIN:(NSString *)PIN { dispatch_async(dispatch_get_main_queue(), ^{ self->_pairAlert = [UIAlertController alertControllerWithTitle:@"Pairing" - message:[NSString stringWithFormat:@"Enter the following PIN on the host machine: %@", PIN] - preferredStyle:UIAlertControllerStyleAlert]; + message:[NSString stringWithFormat:@"Enter the following PIN on the host machine: %@", PIN] + preferredStyle:UIAlertControllerStyleAlert]; [self->_pairAlert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action) { self->_pairAlert = nil; [self->_discMan startDiscovery]; [self hideLoadingFrame]; }]]; - [self presentViewController:self->_pairAlert animated:YES completion:nil]; + [[self activeViewController] presentViewController:self->_pairAlert animated:YES completion:nil]; }); } - (void)displayFailureDialog:(NSString *)message { UIAlertController* failedDialog = [UIAlertController alertControllerWithTitle:@"Pairing Failed" - message:message - preferredStyle:UIAlertControllerStyleAlert]; + message:message + preferredStyle:UIAlertControllerStyleAlert]; [failedDialog addAction:[UIAlertAction actionWithTitle:@"Help" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){ [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"]]; }]]; [failedDialog addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]]; - [self presentViewController:failedDialog animated:YES completion:nil]; + [[self activeViewController] presentViewController:failedDialog animated:YES completion:nil]; [_discMan startDiscovery]; [self hideLoadingFrame]; @@ -146,7 +152,7 @@ static NSMutableSet* hostList; [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"]]; }]]; [applistAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]]; - [self presentViewController:applistAlert animated:YES completion:nil]; + [[self activeViewController] presentViewController:applistAlert animated:YES completion:nil]; host.online = NO; [self showHostSelectionView]; }); @@ -251,7 +257,7 @@ static NSMutableSet* hostList; [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"]]; }]]; [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]]; - [self presentViewController:alert animated:YES completion:nil]; + [[self activeViewController] presentViewController:alert animated:YES completion:nil]; } - (void) hostClicked:(TemporaryHost *)host view:(UIView *)view { @@ -286,7 +292,7 @@ static NSMutableSet* hostList; // Exempt this host from discovery while handling the serverinfo request [self->_discMan removeHostFromDiscovery:host]; [hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest] - fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]]; + fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]]; [self->_discMan addHostToDiscovery:host]; if (serverInfoResp == nil || ![serverInfoResp isStatusOk]) { @@ -309,7 +315,7 @@ static NSMutableSet* hostList; if (view != nil) { // Only display an alert if this was the result of a real // user action, not just passively entering the foreground again - [self presentViewController:applistAlert animated:YES completion:nil]; + [[self activeViewController] presentViewController:applistAlert animated:YES completion:nil]; } host.online = NO; @@ -340,6 +346,16 @@ static NSMutableSet* hostList; }); } +- (UIViewController*) activeViewController { + UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController; + + while (topController.presentedViewController) { + topController = topController.presentedViewController; + } + + return topController; +} + - (void)hostLongClicked:(TemporaryHost *)host view:(UIView *)view { Log(LOG_D, @"Long clicked host: %@", host.name); UIAlertController* longClickAlert = [UIAlertController alertControllerWithTitle:host.name message:@"" preferredStyle:UIAlertControllerStyleActionSheet]; @@ -357,7 +373,7 @@ static NSMutableSet* hostList; }); wolAlert.message = @"Sent WOL Packet"; } - [self presentViewController:wolAlert animated:YES completion:nil]; + [[self activeViewController] presentViewController:wolAlert animated:YES completion:nil]; }]]; [longClickAlert addAction:[UIAlertAction actionWithTitle:@"Connection Help" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){ @@ -380,7 +396,7 @@ static NSMutableSet* hostList; longClickAlert.popoverPresentationController.sourceView = view; longClickAlert.popoverPresentationController.sourceRect = CGRectMake(view.bounds.size.width / 2.0, view.bounds.size.height / 2.0, 1.0, 1.0); // center of the view - [self presentViewController:longClickAlert animated:YES completion:^{ + [[self activeViewController] presentViewController:longClickAlert animated:YES completion:^{ [self updateHosts]; }]; } @@ -408,14 +424,14 @@ static NSMutableSet* hostList; }]]; [hostNotFoundAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]]; dispatch_async(dispatch_get_main_queue(), ^{ - [self presentViewController:hostNotFoundAlert animated:YES completion:nil]; + [[self activeViewController] presentViewController:hostNotFoundAlert animated:YES completion:nil]; }); } }];}); }]]; [alertController addTextFieldWithConfigurationHandler:nil]; [self hideLoadingFrame]; - [self presentViewController:alertController animated:YES completion:nil]; + [[self activeViewController] presentViewController:alertController animated:YES completion:nil]; } - (void) prepareToStreamApp:(TemporaryApp *)app { @@ -465,9 +481,11 @@ static NSMutableSet* hostList; [_appManager stopRetrieving]; +#if !TARGET_OS_TV if (currentPosition != FrontViewPositionLeft) { [[self revealViewController] revealToggle:self]; } +#endif TemporaryApp* currentApp = [self findRunningApp:app.host]; if (currentApp != nil) { @@ -495,7 +513,7 @@ static NSMutableSet* hostList; if (quitResponse.statusCode == 200) { ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init]; [hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest] - fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]]; + fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]]; if (![serverInfoResp isStatusOk] || [[serverInfoResp getStringTag:@"state"] hasSuffix:@"_SERVER_BUSY"]) { // On newer GFE versions, the quit request succeeds even though the app doesn't // really quit if another client tries to kill your app. We'll patch the response @@ -509,10 +527,10 @@ static NSMutableSet* hostList; // If it fails, display an error and stop the current operation if (quitResponse.statusCode != 200) { - alert = [UIAlertController alertControllerWithTitle:@"Quitting App Failed" - message:@"Failed to quit app. If this app was started by " - "another device, you'll need to quit from that device." - preferredStyle:UIAlertControllerStyleAlert]; + alert = [UIAlertController alertControllerWithTitle:@"Quitting App Failed" + message:@"Failed to quit app. If this app was started by " + "another device, you'll need to quit from that device." + preferredStyle:UIAlertControllerStyleAlert]; } // If it succeeds and we're to start streaming, segue to the stream and return else if (![app.id isEqualToString:currentApp.id]) { @@ -540,12 +558,12 @@ static NSMutableSet* hostList; dispatch_async(dispatch_get_main_queue(), ^{ [self updateAppsForHost:app.host]; [self hideLoadingFrame]; - [self presentViewController:alert animated:YES completion:nil]; + [[self activeViewController] presentViewController:alert animated:YES completion:nil]; }); }); }]]; [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]]; - [self presentViewController:alertController animated:YES completion:nil]; + [[self activeViewController] presentViewController:alertController animated:YES completion:nil]; } else { [self prepareToStreamApp:app]; [self performSegueWithIdentifier:@"createStreamFrame" sender:nil]; @@ -561,6 +579,7 @@ static NSMutableSet* hostList; return nil; } +#if !TARGET_OS_TV - (void)revealController:(SWRevealViewController *)revealController didMoveToPosition:(FrontViewPosition)position { // If we moved back to the center position, we should save the settings if (position == FrontViewPositionLeft) { @@ -593,6 +612,13 @@ static NSMutableSet* hostList; currentPosition = position; } +#endif + +#if TARGET_OS_TV +- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { + [self appClicked:_sortedAppList[indexPath.row]]; +} +#endif - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([segue.destinationViewController isKindOfClass:[StreamFrameViewController class]]) { @@ -602,17 +628,17 @@ static NSMutableSet* hostList; } - (void) showLoadingFrame { - LoadingFrameViewController* loadingFrame = [self.storyboard instantiateViewControllerWithIdentifier:@"loadingFrame"]; + _loadingFrame = [self.storyboard instantiateViewControllerWithIdentifier:@"loadingFrame"]; // Avoid animating this as it significantly prolongs the loading frame's // time on screen and can lead to warnings about dismissing while it's // still animating. - [self.navigationController presentViewController:loadingFrame animated:NO completion:nil]; + [[self activeViewController] presentViewController:_loadingFrame animated:NO completion:nil]; } - (void) hideLoadingFrame { // See comment above in showLoadingFrame about why we don't animate this - [self dismissViewControllerAnimated:NO completion:nil]; + [_loadingFrame dismissViewControllerAnimated:NO completion:nil]; [self enableNavigation]; } @@ -620,6 +646,7 @@ static NSMutableSet* hostList; { [super viewDidLoad]; +#if !TARGET_OS_TV // Set the side bar button action. When it's tapped, it'll show the sidebar. [_limelightLogoButton addTarget:self.revealViewController action:@selector(revealToggle:) forControlEvents:UIControlEventTouchDown]; @@ -632,6 +659,7 @@ static NSMutableSet* hostList; // Get callbacks associated with the viewController [self.revealViewController setDelegate:self]; +#endif // Set the current position to the center currentPosition = FrontViewPositionLeft; @@ -658,6 +686,7 @@ static NSMutableSet* hostList; [hostScrollView setShowsHorizontalScrollIndicator:NO]; hostScrollView.delaysContentTouches = NO; +#if !TARGET_OS_TV _pullArrow = [[UIButton alloc] init]; [_pullArrow addTarget:self.revealViewController action:@selector(revealToggle:) forControlEvents:UIControlEventTouchDown]; [_pullArrow setImage:[UIImage imageNamed:@"PullArrow"] forState:UIControlStateNormal]; @@ -666,16 +695,21 @@ static NSMutableSet* hostList; self.collectionView.frame.size.height / 6 - _pullArrow.frame.size.height / 2 - self.navigationController.navigationBar.frame.size.height, _pullArrow.frame.size.width, _pullArrow.frame.size.height); +#endif self.collectionView.delaysContentTouches = NO; self.collectionView.allowsMultipleSelection = NO; +#if !TARGET_OS_TV self.collectionView.multipleTouchEnabled = NO; +#endif [self retrieveSavedHosts]; _discMan = [[DiscoveryManager alloc] initWithHosts:[hostList allObjects] andCallback:self]; [self.view addSubview:hostScrollView]; +#if !TARGET_OS_TV [self.view addSubview:_pullArrow]; +#endif } -(void)beginForegroundRefresh @@ -711,7 +745,9 @@ static NSMutableSet* hostList; { [super viewDidAppear:animated]; +#if !TARGET_OS_TV [[self revealViewController] setPrimaryViewController:self]; +#endif [self.navigationController setNavigationBarHidden:NO animated:YES]; @@ -911,9 +947,12 @@ static NSMutableSet* hostList; cell.layer.shadowOpacity = 0.5f; cell.layer.shadowPath = shadowPath.CGPath; - cell.layer.borderColor = [[UIColor colorWithRed:0 green:0 blue:0 alpha:0.3f] CGColor]; cell.layer.borderWidth = 1; + +#if !TARGET_OS_TV + cell.layer.borderColor = [[UIColor colorWithRed:0 green:0 blue:0 alpha:0.3f] CGColor]; cell.exclusiveTouch = YES; +#endif return cell; } @@ -946,9 +985,11 @@ static NSMutableSet* hostList; return YES; } +#if !TARGET_OS_TV - (BOOL)shouldAutorotate { return YES; } +#endif - (void) disableNavigation { self.navigationController.navigationBar.topItem.rightBarButtonItem.enabled = NO; @@ -958,4 +999,12 @@ static NSMutableSet* hostList; self.navigationController.navigationBar.topItem.rightBarButtonItem.enabled = YES; } +- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator { + + if (context.nextFocusedView != nil) { + [context.nextFocusedView setAlpha:0.8]; + } + [context.previouslyFocusedView setAlpha:1.0]; +} + @end diff --git a/Limelight/ViewControllers/StreamFrameViewController.m b/Limelight/ViewControllers/StreamFrameViewController.m index 8afa7dff..81943d01 100644 --- a/Limelight/ViewControllers/StreamFrameViewController.m +++ b/Limelight/ViewControllers/StreamFrameViewController.m @@ -69,6 +69,9 @@ } - (void)viewDidDisappear:(BOOL)animated { + [_controllerSupport cleanup]; + [UIApplication sharedApplication].idleTimerDisabled = NO; + [_streamMan stopStream]; if (_inactivityTimer != nil) { [_inactivityTimer invalidate]; _inactivityTimer = nil; @@ -77,8 +80,6 @@ } - (void) returnToMainFrame { - [_controllerSupport cleanup]; - [UIApplication sharedApplication].idleTimerDisabled = NO; [self.navigationController popToRootViewControllerAnimated:YES]; } @@ -100,7 +101,6 @@ - (void)inactiveTimerExpired:(NSTimer*)timer { Log(LOG_I, @"Terminating stream after inactivity"); - [_streamMan stopStream]; [self returnToMainFrame]; _inactivityTimer = nil; @@ -124,14 +124,12 @@ _inactivityTimer = nil; } - [_streamMan stopStream]; [self returnToMainFrame]; } - (void)edgeSwiped { Log(LOG_I, @"User swiped to end stream"); - [_streamMan stopStream]; [self returnToMainFrame]; } diff --git a/Moonlight TV/Base.lproj/Main.storyboard b/Moonlight TV/Base.lproj/Main.storyboard index 9dcb8e6a..a25d8626 100644 --- a/Moonlight TV/Base.lproj/Main.storyboard +++ b/Moonlight TV/Base.lproj/Main.storyboard @@ -26,10 +26,10 @@ - + - + diff --git a/Moonlight TV/ViewController.h b/Moonlight TV/ViewController.h deleted file mode 100644 index a30cfd24..00000000 --- a/Moonlight TV/ViewController.h +++ /dev/null @@ -1,21 +0,0 @@ -// -// ViewController.h -// Moonlight TV -// -// Created by Diego Waxemberg on 8/25/18. -// Copyright © 2018 Moonlight Game Streaming Project. All rights reserved. -// - -#import -#import "DiscoveryManager.h" -#import "PairManager.h" -#import "StreamConfiguration.h" -#import "UIComputerView.h" -#import "UIAppView.h" -#import "AppAssetManager.h" - -@interface ViewController : UICollectionViewController - - -@end - diff --git a/Moonlight TV/ViewController.m b/Moonlight TV/ViewController.m deleted file mode 100644 index d1d4cf90..00000000 --- a/Moonlight TV/ViewController.m +++ /dev/null @@ -1,925 +0,0 @@ -// MainFrameViewController.m -// Moonlight -// -// Created by Diego Waxemberg on 1/17/14. -// Copyright (c) 2014 Moonlight Stream. All rights reserved. -// - -@import ImageIO; - -#import "ViewController.h" -#import "CryptoManager.h" -#import "HttpManager.h" -#import "Connection.h" -#import "StreamManager.h" -#import "Utils.h" -#import "UIComputerView.h" -#import "UIAppView.h" -#import "DataManager.h" -#import "TemporarySettings.h" -#import "WakeOnLanManager.h" -#import "AppListResponse.h" -#import "ServerInfoResponse.h" -#import "StreamFrameViewController.h" -#import "LoadingFrameViewController.h" -#import "ComputerScrollView.h" -#import "TemporaryApp.h" -#import "IdManager.h" -#import "ConnectionHelper.h" - -#import - -@implementation ViewController { - NSOperationQueue* _opQueue; - TemporaryHost* _selectedHost; - NSString* _uniqueId; - NSData* _cert; - DiscoveryManager* _discMan; - AppAssetManager* _appManager; - StreamConfiguration* _streamConfig; - UIAlertController* _pairAlert; - LoadingFrameViewController* _loadingFrame; - UIScrollView* hostScrollView; - int currentPosition; - NSArray* _sortedAppList; - NSCache* _boxArtCache; - UIButton* _pullArrow; - bool _background; -} -static NSMutableSet* hostList; - -- (void)showPIN:(NSString *)PIN { - dispatch_async(dispatch_get_main_queue(), ^{ - self->_pairAlert = [UIAlertController alertControllerWithTitle:@"Pairing" - message:[NSString stringWithFormat:@"Enter the following PIN on the host machine: %@", PIN] - preferredStyle:UIAlertControllerStyleAlert]; - [self->_pairAlert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action) { - self->_pairAlert = nil; - [self->_discMan startDiscovery]; - [self hideLoadingFrame]; - }]]; - [[self activeViewController] presentViewController:self->_pairAlert animated:YES completion:nil]; - }); -} - -- (void)displayFailureDialog:(NSString *)message { - UIAlertController* failedDialog = [UIAlertController alertControllerWithTitle:@"Pairing Failed" - message:message - preferredStyle:UIAlertControllerStyleAlert]; - [failedDialog addAction:[UIAlertAction actionWithTitle:@"Help" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){ - [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"]]; - }]]; - [failedDialog addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]]; - [[self activeViewController] presentViewController:failedDialog animated:YES completion:nil]; - - [_discMan startDiscovery]; - [self hideLoadingFrame]; -} - -- (void)pairFailed:(NSString *)message { - dispatch_async(dispatch_get_main_queue(), ^{ - if (self->_pairAlert != nil) { - [self->_pairAlert dismissViewControllerAnimated:YES completion:^{ - [self displayFailureDialog:message]; - }]; - self->_pairAlert = nil; - } - else { - [self displayFailureDialog:message]; - } - }); -} - -- (void)pairSuccessful { - dispatch_async(dispatch_get_main_queue(), ^{ - [self->_pairAlert dismissViewControllerAnimated:YES completion:nil]; - self->_pairAlert = nil; - - [self->_discMan startDiscovery]; - [self alreadyPaired]; - }); -} - -- (void)alreadyPaired { - BOOL usingCachedAppList = false; - - // Capture the host here because it can change once we - // leave the main thread - TemporaryHost* host = _selectedHost; - - if ([host.appList count] > 0) { - usingCachedAppList = true; - dispatch_async(dispatch_get_main_queue(), ^{ - if (host != self->_selectedHost) { - return; - } - - // TODO: self->_computerNameButton.title = host.name; - [self.navigationController.navigationBar setNeedsLayout]; - - [self updateAppsForHost:host]; - [self hideLoadingFrame]; - }); - } - Log(LOG_I, @"Using cached app list: %d", usingCachedAppList); - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - // Exempt this host from discovery while handling the applist query - [self->_discMan removeHostFromDiscovery:host]; - - AppListResponse* appListResp = [ConnectionHelper getAppListForHostWithHostIP:host.activeAddress deviceName:deviceName cert:self->_cert uniqueID:self->_uniqueId]; - - [self->_discMan addHostToDiscovery:host]; - - if (appListResp == nil || ![appListResp isStatusOk] || [appListResp getAppList] == nil) { - Log(LOG_W, @"Failed to get applist: %@", appListResp.statusMessage); - dispatch_async(dispatch_get_main_queue(), ^{ - [self hideLoadingFrame]; - - if (host != self->_selectedHost) { - return; - } - - UIAlertController* applistAlert = [UIAlertController alertControllerWithTitle:@"Fetching App List Failed" - message:@"The connection to the PC was interrupted." - preferredStyle:UIAlertControllerStyleAlert]; - [applistAlert addAction:[UIAlertAction actionWithTitle:@"Help" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){ - [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"]]; - }]]; - [applistAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]]; - [[self activeViewController] presentViewController:applistAlert animated:YES completion:nil]; - host.online = NO; - [self showHostSelectionView]; - }); - } else { - dispatch_async(dispatch_get_main_queue(), ^{ - [self updateApplist:[appListResp getAppList] forHost:host]; - - if (host != self->_selectedHost) { - return; - } - - // TODO: self->_computerNameButton.title = host.name; - [self.navigationController.navigationBar setNeedsLayout]; - - [self updateAppsForHost:host]; - [self->_appManager stopRetrieving]; - [self->_appManager retrieveAssetsFromHost:host]; - [self hideLoadingFrame]; - }); - } - }); -} - -- (void) updateApplist:(NSSet*) newList forHost:(TemporaryHost*)host { - DataManager* database = [[DataManager alloc] init]; - - for (TemporaryApp* app in newList) { - BOOL appAlreadyInList = NO; - for (TemporaryApp* savedApp in host.appList) { - if ([app.id isEqualToString:savedApp.id]) { - savedApp.name = app.name; - savedApp.hdrSupported = app.hdrSupported; - appAlreadyInList = YES; - break; - } - } - if (!appAlreadyInList) { - app.host = host; - [host.appList addObject:app]; - } - } - - BOOL appWasRemoved; - do { - appWasRemoved = NO; - - for (TemporaryApp* app in host.appList) { - appWasRemoved = YES; - for (TemporaryApp* mergedApp in newList) { - if ([mergedApp.id isEqualToString:app.id]) { - appWasRemoved = NO; - break; - } - } - if (appWasRemoved) { - // Removing the app mutates the list we're iterating (which isn't legal). - // We need to jump out of this loop and restart enumeration. - - [host.appList removeObject:app]; - - // It's important to remove the app record from the database - // since we'll have a constraint violation now that appList - // doesn't have this app in it. - [database removeApp:app]; - - break; - } - } - - // Keep looping until the list is no longer being mutated - } while (appWasRemoved); - - [database updateAppsForExistingHost:host]; -} - -- (void)showHostSelectionView { - [_appManager stopRetrieving]; - _selectedHost = nil; - // TODO: _computerNameButton.title = @"No Host Selected"; - [self.collectionView reloadData]; - [self.view addSubview:hostScrollView]; -} - -- (void) receivedAssetForApp:(TemporaryApp*)app { - // Update the box art cache now so we don't have to do it - // on the main thread - [self updateBoxArtCacheForApp:app]; - - DataManager* dataManager = [[DataManager alloc] init]; - [dataManager updateIconForExistingApp: app]; - - dispatch_async(dispatch_get_main_queue(), ^{ - [self.collectionView reloadData]; - }); -} - -- (void)displayDnsFailedDialog { - UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Network Error" - message:@"Failed to resolve host." - preferredStyle:UIAlertControllerStyleAlert]; - [alert addAction:[UIAlertAction actionWithTitle:@"Help" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){ - [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"]]; - }]]; - [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]]; - [[self activeViewController] presentViewController:alert animated:YES completion:nil]; -} - -- (void) hostClicked:(TemporaryHost *)host view:(UIView *)view { - // Treat clicks on offline hosts to be long clicks - // This shows the context menu with wake, delete, etc. rather - // than just hanging for a while and failing as we would in this - // code path. - if (!host.online && view != nil) { - [self hostLongClicked:host view:view]; - return; - } - - Log(LOG_D, @"Clicked host: %@", host.name); - _selectedHost = host; - [self disableNavigation]; - - // If we are online, paired, and have a cached app list, skip straight - // to the app grid without a loading frame. This is the fast path that users - // should hit most. Check for a valid view because we don't want to hit the fast - // path after coming back from streaming, since we need to fetch serverinfo too - // so that our active game data is correct. - if (host.online && host.pairState == PairStatePaired && host.appList.count > 0 && view != nil) { - [self alreadyPaired]; - return; - } - - [self showLoadingFrame]; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - HttpManager* hMan = [[HttpManager alloc] initWithHost:host.activeAddress uniqueId:self->_uniqueId deviceName:deviceName cert:self->_cert]; - ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init]; - - // Exempt this host from discovery while handling the serverinfo request - [self->_discMan removeHostFromDiscovery:host]; - [hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest] - fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]]; - [self->_discMan addHostToDiscovery:host]; - - if (serverInfoResp == nil || ![serverInfoResp isStatusOk]) { - Log(LOG_W, @"Failed to get server info: %@", serverInfoResp.statusMessage); - dispatch_async(dispatch_get_main_queue(), ^{ - [self hideLoadingFrame]; - - if (host != self->_selectedHost) { - return; - } - - UIAlertController* applistAlert = [UIAlertController alertControllerWithTitle:@"Fetching Server Info Failed" - message:@"The connection to the PC was interrupted." - preferredStyle:UIAlertControllerStyleAlert]; - [applistAlert addAction:[UIAlertAction actionWithTitle:@"Help" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){ - [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"]]; - }]]; - [applistAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]]; - - if (view != nil) { - // Only display an alert if this was the result of a real - // user action, not just passively entering the foreground again - [[self activeViewController] presentViewController:applistAlert animated:YES completion:nil]; - } - - host.online = NO; - [self showHostSelectionView]; - }); - } else { - Log(LOG_D, @"server info pair status: %@", [serverInfoResp getStringTag:@"PairStatus"]); - if ([[serverInfoResp getStringTag:@"PairStatus"] isEqualToString:@"1"]) { - Log(LOG_I, @"Already Paired"); - [self alreadyPaired]; - } - // Only pair when this was the result of explicit user action - else if (view != nil) { - Log(LOG_I, @"Trying to pair"); - // Polling the server while pairing causes the server to screw up - [self->_discMan stopDiscoveryBlocking]; - PairManager* pMan = [[PairManager alloc] initWithManager:hMan andCert:self->_cert callback:self]; - [self->_opQueue addOperation:pMan]; - } - else { - // Not user action, so just return to host screen - dispatch_async(dispatch_get_main_queue(), ^{ - [self hideLoadingFrame]; - [self showHostSelectionView]; - }); - } - } - }); -} - -- (UIViewController*) activeViewController { - UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController; - - while (topController.presentedViewController) { - topController = topController.presentedViewController; - } - - return topController; -} - -- (void)hostLongClicked:(TemporaryHost *)host view:(UIView *)view { - Log(LOG_D, @"Long clicked host: %@", host.name); - UIAlertController* longClickAlert = [UIAlertController alertControllerWithTitle:host.name message:@"" preferredStyle:UIAlertControllerStyleActionSheet]; - if (!host.online) { - [longClickAlert addAction:[UIAlertAction actionWithTitle:@"Wake" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){ - UIAlertController* wolAlert = [UIAlertController alertControllerWithTitle:@"Wake On LAN" message:@"" preferredStyle:UIAlertControllerStyleAlert]; - [wolAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]]; - if (host.pairState != PairStatePaired) { - wolAlert.message = @"Cannot wake host because you are not paired"; - } else if (host.mac == nil || [host.mac isEqualToString:@"00:00:00:00:00:00"]) { - wolAlert.message = @"Host MAC unknown, unable to send WOL Packet"; - } else { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [WakeOnLanManager wakeHost:host]; - }); - wolAlert.message = @"Sent WOL Packet"; - } - [[self activeViewController] presentViewController:wolAlert animated:YES completion:nil]; - }]]; - - [longClickAlert addAction:[UIAlertAction actionWithTitle:@"Connection Help" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){ - [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"]]; - }]]; - } - [longClickAlert addAction:[UIAlertAction actionWithTitle:@"Remove Host" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action) { - [self->_discMan removeHostFromDiscovery:host]; - DataManager* dataMan = [[DataManager alloc] init]; - [dataMan removeHost:host]; - @synchronized(hostList) { - [hostList removeObject:host]; - [self updateAllHosts:[hostList allObjects]]; - } - - }]]; - [longClickAlert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]]; - - // these two lines are required for iPad support of UIAlertSheet - longClickAlert.popoverPresentationController.sourceView = view; - - longClickAlert.popoverPresentationController.sourceRect = CGRectMake(view.bounds.size.width / 2.0, view.bounds.size.height / 2.0, 1.0, 1.0); // center of the view - [[self activeViewController] presentViewController:longClickAlert animated:YES completion:^{ - [self updateHosts]; - }]; -} - -- (void) addHostClicked { - Log(LOG_D, @"Clicked add host"); - [self showLoadingFrame]; - UIAlertController* alertController = [UIAlertController alertControllerWithTitle:@"Host Address" message:@"Please enter a hostname or IP address" preferredStyle:UIAlertControllerStyleAlert]; - [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]]; - [alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){ - NSString* hostAddress = ((UITextField*)[[alertController textFields] objectAtIndex:0]).text; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ - [self->_discMan discoverHost:hostAddress withCallback:^(TemporaryHost* host, NSString* error){ - if (host != nil) { - dispatch_async(dispatch_get_main_queue(), ^{ - @synchronized(hostList) { - [hostList addObject:host]; - } - [self updateHosts]; - }); - } else { - UIAlertController* hostNotFoundAlert = [UIAlertController alertControllerWithTitle:@"Add Host" message:error preferredStyle:UIAlertControllerStyleAlert]; - [hostNotFoundAlert addAction:[UIAlertAction actionWithTitle:@"Help" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){ - [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"]]; - }]]; - [hostNotFoundAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]]; - dispatch_async(dispatch_get_main_queue(), ^{ - [[self activeViewController] presentViewController:hostNotFoundAlert animated:YES completion:nil]; - }); - } - }];}); - }]]; - [alertController addTextFieldWithConfigurationHandler:nil]; - [self hideLoadingFrame]; - [[self activeViewController] presentViewController:alertController animated:YES completion:nil]; -} - -- (void) prepareToStreamApp:(TemporaryApp *)app { - _streamConfig = [[StreamConfiguration alloc] init]; - _streamConfig.host = app.host.activeAddress; - _streamConfig.appID = app.id; - _streamConfig.appName = app.name; - - DataManager* dataMan = [[DataManager alloc] init]; - TemporarySettings* streamSettings = [dataMan getSettings]; - - _streamConfig.frameRate = [streamSettings.framerate intValue]; - _streamConfig.bitRate = [streamSettings.bitrate intValue]; - _streamConfig.height = [streamSettings.height intValue]; - _streamConfig.width = [streamSettings.width intValue]; - _streamConfig.streamingRemotely = streamSettings.streamingRemotely; - _streamConfig.optimizeGameSettings = streamSettings.optimizeGames; - _streamConfig.playAudioOnPC = streamSettings.playAudioOnPC; - _streamConfig.allowHevc = streamSettings.useHevc; - - // multiController must be set before calling getConnectedGamepadMask - _streamConfig.multiController = streamSettings.multiController; - _streamConfig.gamepadMask = [ControllerSupport getConnectedGamepadMask:_streamConfig]; - - // TODO: Detect attached surround sound system then address 5.1 TODOs - // in Connection.m - _streamConfig.audioChannelCount = 2; - _streamConfig.audioChannelMask = 0x3; - - // HDR requires HDR10 game, HDR10 display, and HEVC Main10 decoder on the client. - // It additionally requires an HEVC Main10 encoder on the server (GTX 1000+). - // - // It should also be a user preference, since some games may require higher peak - // brightness than the iOS device can support to look correct in HDR mode. - if (@available(iOS 11.3, *)) { - _streamConfig.enableHdr = - app.hdrSupported && // App supported - (app.host.serverCodecModeSupport & 0x200) != 0 && // HEVC Main10 encoding on host PC GPU - VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC) && // Decoder supported - (AVPlayer.availableHDRModes & AVPlayerHDRModeHDR10) != 0 && // Display supported - streamSettings.enableHdr; // User wants it enabled - } -} - -- (void) appClicked:(TemporaryApp *)app { - Log(LOG_D, @"Clicked app: %@", app.name); - - [_appManager stopRetrieving]; - - TemporaryApp* currentApp = [self findRunningApp:app.host]; - if (currentApp != nil) { - UIAlertController* alertController = [UIAlertController - alertControllerWithTitle: app.name - message: [app.id isEqualToString:currentApp.id] ? @"" : [NSString stringWithFormat:@"%@ is currently running", currentApp.name]preferredStyle:UIAlertControllerStyleAlert]; - [alertController addAction:[UIAlertAction - actionWithTitle:[app.id isEqualToString:currentApp.id] ? @"Resume App" : @"Resume Running App" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){ - Log(LOG_I, @"Resuming application: %@", currentApp.name); - [self prepareToStreamApp:currentApp]; - [self performSegueWithIdentifier:@"createStreamFrame" sender:nil]; - }]]; - [alertController addAction:[UIAlertAction actionWithTitle: - [app.id isEqualToString:currentApp.id] ? @"Quit App" : @"Quit Running App and Start" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action){ - Log(LOG_I, @"Quitting application: %@", currentApp.name); - [self showLoadingFrame]; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - HttpManager* hMan = [[HttpManager alloc] initWithHost:app.host.activeAddress uniqueId:self->_uniqueId deviceName:deviceName cert:self->_cert]; - HttpResponse* quitResponse = [[HttpResponse alloc] init]; - HttpRequest* quitRequest = [HttpRequest requestForResponse: quitResponse withUrlRequest:[hMan newQuitAppRequest]]; - - // Exempt this host from discovery while handling the quit operation - [self->_discMan removeHostFromDiscovery:app.host]; - [hMan executeRequestSynchronously:quitRequest]; - if (quitResponse.statusCode == 200) { - ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init]; - [hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest] - fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]]; - if (![serverInfoResp isStatusOk] || [[serverInfoResp getStringTag:@"state"] hasSuffix:@"_SERVER_BUSY"]) { - // On newer GFE versions, the quit request succeeds even though the app doesn't - // really quit if another client tries to kill your app. We'll patch the response - // to look like the old error in that case, so the UI behaves. - quitResponse.statusCode = 599; - } - } - [self->_discMan addHostToDiscovery:app.host]; - - UIAlertController* alert; - - // If it fails, display an error and stop the current operation - if (quitResponse.statusCode != 200) { - alert = [UIAlertController alertControllerWithTitle:@"Quitting App Failed" - message:@"Failed to quit app. If this app was started by " - "another device, you'll need to quit from that device." - preferredStyle:UIAlertControllerStyleAlert]; - } - // If it succeeds and we're to start streaming, segue to the stream and return - else if (![app.id isEqualToString:currentApp.id]) { - app.host.currentGame = @"0"; - - dispatch_async(dispatch_get_main_queue(), ^{ - [self updateAppsForHost:app.host]; - [self hideLoadingFrame]; - [self prepareToStreamApp:app]; - [self performSegueWithIdentifier:@"createStreamFrame" sender:nil]; - }); - - return; - } - // Otherwise, display a dialog to notify the user that the app was quit - else { - app.host.currentGame = @"0"; - - alert = [UIAlertController alertControllerWithTitle:@"Quitting App" - message:@"The app was quit successfully." - preferredStyle:UIAlertControllerStyleAlert]; - } - - [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]]; - dispatch_async(dispatch_get_main_queue(), ^{ - [self updateAppsForHost:app.host]; - [self hideLoadingFrame]; - [[self activeViewController] presentViewController:alert animated:YES completion:nil]; - }); - }); - }]]; - [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]]; - [[self activeViewController] presentViewController:alertController animated:YES completion:nil]; - } else { - [self prepareToStreamApp:app]; - [self performSegueWithIdentifier:@"createStreamFrame" sender:nil]; - } -} - -- (TemporaryApp*) findRunningApp:(TemporaryHost*)host { - for (TemporaryApp* app in host.appList) { - if ([app.id isEqualToString:host.currentGame]) { - return app; - } - } - return nil; -} - - -- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { - [self appClicked:_sortedAppList[indexPath.row]]; -} - -- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { - if ([segue.destinationViewController isKindOfClass:[StreamFrameViewController class]]) { - StreamFrameViewController* streamFrame = segue.destinationViewController; - streamFrame.streamConfig = _streamConfig; - } -} - -- (void) showLoadingFrame { - _loadingFrame = [self.storyboard instantiateViewControllerWithIdentifier:@"loadingFrame"]; - - // Avoid animating this as it significantly prolongs the loading frame's - // time on screen and can lead to warnings about dismissing while it's - // still animating. - [[self activeViewController] presentViewController:_loadingFrame animated:NO completion:nil]; -} - -- (void) hideLoadingFrame { - // See comment above in showLoadingFrame about why we don't animate this - [_loadingFrame dismissViewControllerAnimated:NO completion:nil]; - [self enableNavigation]; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - - // Set up crypto - [CryptoManager generateKeyPairUsingSSl]; - _uniqueId = [IdManager getUniqueId]; - _cert = [CryptoManager readCertFromFile]; - - _appManager = [[AppAssetManager alloc] initWithCallback:self]; - _opQueue = [[NSOperationQueue alloc] init]; - - // Only initialize the host picker list once - if (hostList == nil) { - hostList = [[NSMutableSet alloc] init]; - } - - _boxArtCache = [[NSCache alloc] init]; - - [self setAutomaticallyAdjustsScrollViewInsets:NO]; - - hostScrollView = [[ComputerScrollView alloc] init]; - hostScrollView.frame = CGRectMake(0, self.navigationController.navigationBar.frame.origin.y + self.navigationController.navigationBar.frame.size.height, self.view.frame.size.width, self.view.frame.size.height / 2); - [hostScrollView setShowsHorizontalScrollIndicator:NO]; - hostScrollView.delaysContentTouches = NO; - - _pullArrow = [[UIButton alloc] init]; - [_pullArrow setImage:[UIImage imageNamed:@"PullArrow"] forState:UIControlStateNormal]; - [_pullArrow sizeToFit]; - _pullArrow.frame = CGRectMake(0, - self.collectionView.frame.size.height / 6 - _pullArrow.frame.size.height / 2 - self.navigationController.navigationBar.frame.size.height, - _pullArrow.frame.size.width, - _pullArrow.frame.size.height); - - self.collectionView.delaysContentTouches = NO; - self.collectionView.allowsMultipleSelection = NO; - - [self retrieveSavedHosts]; - _discMan = [[DiscoveryManager alloc] initWithHosts:[hostList allObjects] andCallback:self]; - - [self.view addSubview:hostScrollView]; - [self.view addSubview:_pullArrow]; -} - --(void)beginForegroundRefresh -{ - if (!_background) { - // This will kick off box art caching - [self updateHosts]; - - [_discMan startDiscovery]; - - // This will refresh the applist when a paired host is selected - if (_selectedHost != nil && _selectedHost.pairState == PairStatePaired) { - [self hostClicked:_selectedHost view:nil]; - } - } -} - --(void)handleReturnToForeground -{ - _background = NO; - - [self beginForegroundRefresh]; -} - --(void)handleEnterBackground -{ - _background = YES; - - [_discMan stopDiscovery]; -} - -- (void)viewDidAppear:(BOOL)animated -{ - [super viewDidAppear:animated]; - - [self.navigationController setNavigationBarHidden:NO animated:YES]; - - // Hide 1px border line - UIImage* fakeImage = [[UIImage alloc] init]; - [self.navigationController.navigationBar setShadowImage:fakeImage]; - [self.navigationController.navigationBar setBackgroundImage:fakeImage forBarPosition:UIBarPositionAny barMetrics:UIBarMetricsDefault]; - - [[NSNotificationCenter defaultCenter] addObserver: self - selector: @selector(handleReturnToForeground) - name: UIApplicationDidBecomeActiveNotification - object: nil]; - - [[NSNotificationCenter defaultCenter] addObserver: self - selector: @selector(handleEnterBackground) - name: UIApplicationWillResignActiveNotification - object: nil]; - - // We can get here on home press while streaming - // since the stream view segues to us just before - // entering the background. We can't check the app - // state here (since it's in transition), so we have - // to use this function that will use our internal - // state here to determine whether we're foreground. - // - // Note that this is neccessary here as we may enter - // this view via an error dialog from the stream - // view, so we won't get a return to active notification - // for that which would normally fire beginForegroundRefresh. - - // HACK tvOS: When this is enabled, it causes us to endlessly - // refresh the UI if you go to the homescreen and back from the app grid. - //[self beginForegroundRefresh]; -} - -- (void)viewDidDisappear:(BOOL)animated -{ - [super viewDidDisappear:animated]; - - // when discovery stops, we must create a new instance because - // you cannot restart an NSOperation when it is finished - [_discMan stopDiscovery]; - - // Purge the box art cache - [_boxArtCache removeAllObjects]; - - // Remove our lifetime observers to avoid triggering them - // while streaming - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -- (void) retrieveSavedHosts { - DataManager* dataMan = [[DataManager alloc] init]; - NSArray* hosts = [dataMan getHosts]; - @synchronized(hostList) { - [hostList addObjectsFromArray:hosts]; - - // Initialize the non-persistent host state - for (TemporaryHost* host in hostList) { - if (host.activeAddress == nil) { - host.activeAddress = host.localAddress; - } - if (host.activeAddress == nil) { - host.activeAddress = host.externalAddress; - } - if (host.activeAddress == nil) { - host.activeAddress = host.address; - } - } - } -} - -- (void) updateAllHosts:(NSArray *)hosts { - dispatch_async(dispatch_get_main_queue(), ^{ - Log(LOG_D, @"New host list:"); - for (TemporaryHost* host in hosts) { - Log(LOG_D, @"Host: \n{\n\t name:%@ \n\t address:%@ \n\t localAddress:%@ \n\t externalAddress:%@ \n\t uuid:%@ \n\t mac:%@ \n\t pairState:%d \n\t online:%d \n\t activeAddress:%@ \n}", host.name, host.address, host.localAddress, host.externalAddress, host.uuid, host.mac, host.pairState, host.online, host.activeAddress); - } - @synchronized(hostList) { - [hostList removeAllObjects]; - [hostList addObjectsFromArray:hosts]; - } - [self updateHosts]; - }); -} - -- (void)updateHosts { - Log(LOG_I, @"Updating hosts..."); - [[hostScrollView subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)]; - UIComputerView* addComp = [[UIComputerView alloc] initForAddWithCallback:self]; - UIComputerView* compView; - float prevEdge = -1; - @synchronized (hostList) { - // Sort the host list in alphabetical order - NSArray* sortedHostList = [[hostList allObjects] sortedArrayUsingSelector:@selector(compareName:)]; - for (TemporaryHost* comp in sortedHostList) { - compView = [[UIComputerView alloc] initWithComputer:comp andCallback:self]; - compView.center = CGPointMake([self getCompViewX:compView addComp:addComp prevEdge:prevEdge], hostScrollView.frame.size.height / 2); - prevEdge = compView.frame.origin.x + compView.frame.size.width; - [hostScrollView addSubview:compView]; - - // Start jobs to decode the box art in advance - for (TemporaryApp* app in comp.appList) { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ - [self updateBoxArtCacheForApp:app]; - }); - } - } - } - - prevEdge = [self getCompViewX:addComp addComp:addComp prevEdge:prevEdge]; - addComp.center = CGPointMake(prevEdge, hostScrollView.frame.size.height / 2); - - [hostScrollView addSubview:addComp]; - [hostScrollView setContentSize:CGSizeMake(prevEdge + addComp.frame.size.width, hostScrollView.frame.size.height)]; -} - -- (float) getCompViewX:(UIComputerView*)comp addComp:(UIComputerView*)addComp prevEdge:(float)prevEdge { - if (prevEdge == -1) { - return hostScrollView.frame.origin.x + comp.frame.size.width / 2 + addComp.frame.size.width / 2; - } else { - return prevEdge + addComp.frame.size.width / 2 + comp.frame.size.width / 2; - } -} - -// This function forces immediate decoding of the UIImage, rather -// than the default lazy decoding that results in janky scrolling. -+ (UIImage*) loadBoxArtForCaching:(TemporaryApp*)app { - UIImage* boxArt; - - CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)app.image, NULL); - CGImageRef cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil); - - size_t width = CGImageGetWidth(cgImage); - size_t height = CGImageGetHeight(cgImage); - - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGContextRef imageContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, - kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little); - CGColorSpaceRelease(colorSpace); - - CGContextDrawImage(imageContext, CGRectMake(0, 0, width, height), cgImage); - - CGImageRef outputImage = CGBitmapContextCreateImage(imageContext); - - boxArt = [UIImage imageWithCGImage:outputImage]; - - CGImageRelease(outputImage); - CGContextRelease(imageContext); - - CGImageRelease(cgImage); - CFRelease(source); - - return boxArt; -} - -- (void) updateBoxArtCacheForApp:(TemporaryApp*)app { - if (app.image == nil) { - [_boxArtCache removeObjectForKey:app]; - } - else if ([_boxArtCache objectForKey:app] == nil) { - [_boxArtCache setObject:[ViewController loadBoxArtForCaching:app] forKey:app]; - } -} - -- (void) updateAppsForHost:(TemporaryHost*)host { - if (host != _selectedHost) { - Log(LOG_W, @"Mismatched host during app update"); - return; - } - - _sortedAppList = [host.appList allObjects]; - _sortedAppList = [_sortedAppList sortedArrayUsingSelector:@selector(compareName:)]; - - [hostScrollView removeFromSuperview]; - [self.collectionView reloadData]; -} - -- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { - UICollectionViewCell* cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"AppCell" forIndexPath:indexPath]; - - TemporaryApp* app = _sortedAppList[indexPath.row]; - UIAppView* appView = [[UIAppView alloc] initWithApp:app cache:_boxArtCache andCallback:self]; - [appView updateAppImage]; - - if (appView.bounds.size.width > 10.0) { - CGFloat scale = cell.bounds.size.width / appView.bounds.size.width; - [appView setCenter:CGPointMake(appView.bounds.size.width / 2 * scale, appView.bounds.size.height / 2 * scale)]; - appView.transform = CGAffineTransformMakeScale(scale, scale); - } - - [cell.subviews.firstObject removeFromSuperview]; // Remove a view that was previously added - [cell addSubview:appView]; - - - UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRect:cell.bounds]; - cell.layer.masksToBounds = NO; - cell.layer.shadowColor = [UIColor blackColor].CGColor; - cell.layer.shadowOffset = CGSizeMake(1.0f, 5.0f); - cell.layer.shadowOpacity = 0.5f; - cell.layer.shadowPath = shadowPath.CGPath; - - cell.layer.borderColor = [[UIColor colorWithRed:0 green:0 blue:0 alpha:0.3f] CGColor]; - cell.layer.borderWidth = 1; - - return cell; -} - -- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { - return 1; // App collection only -} - -- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { - if (_selectedHost != nil) { - return _selectedHost.appList.count; - } - else { - return 0; - } -} - -- (void)didReceiveMemoryWarning -{ - [super didReceiveMemoryWarning]; - // Dispose of any resources that can be recreated. -} - -- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { - [self.view endEditing:YES]; -} - -- (BOOL)textFieldShouldReturn:(UITextField *)textField { - [textField resignFirstResponder]; - return YES; -} - -- (void) disableNavigation { - self.navigationController.navigationBar.topItem.rightBarButtonItem.enabled = NO; -} - -- (void) enableNavigation { - self.navigationController.navigationBar.topItem.rightBarButtonItem.enabled = YES; -} - -- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator { - - if (context.nextFocusedView != nil) { - [context.nextFocusedView setAlpha:0.8]; - } - [context.previouslyFocusedView setAlpha:1.0]; -} - -@end diff --git a/Moonlight.xcodeproj/project.pbxproj b/Moonlight.xcodeproj/project.pbxproj index e6117398..a5fcedbc 100644 --- a/Moonlight.xcodeproj/project.pbxproj +++ b/Moonlight.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 9865DC37213287FE0005B9B9 /* StreamFrameViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89462719F646E200339C8A /* StreamFrameViewController.m */; }; 9865DC38213287FE0005B9B9 /* LoadingFrameViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FB4A23B71A9D3637004D2EF2 /* LoadingFrameViewController.m */; }; 9865DC3C2132922E0005B9B9 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9865DC3B2132922E0005B9B9 /* GameController.framework */; }; + 9865DC3E21332D660005B9B9 /* MainFrameViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89462519F646E200339C8A /* MainFrameViewController.m */; }; 9890CF6B203B7EE1006C4B06 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 9890CF6A203B7EE1006C4B06 /* libxml2.tbd */; }; 98CFB82F1CAD481B0048EF74 /* libmoonlight-common.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98AB2E841CAD46840089BB98 /* libmoonlight-common.a */; }; 98D5856D1C0EA79600F6CC00 /* TemporaryHost.m in Sources */ = {isa = PBXBuildFile; fileRef = 98D5856C1C0EA79600F6CC00 /* TemporaryHost.m */; }; @@ -24,7 +25,6 @@ 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 */; }; - FB1A67A02132457D00507771 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FB1A67592132419700507771 /* ViewController.m */; }; FB1A67A12132458C00507771 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = FB1A67642132419A00507771 /* main.m */; }; FB1A67A3213245BD00507771 /* Connection.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461719F646E200339C8A /* Connection.m */; }; FB1A67A5213245BD00507771 /* StreamConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89461919F646E200339C8A /* StreamConfiguration.m */; }; @@ -182,8 +182,6 @@ 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; }; - FB1A67582132419700507771 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; - FB1A67592132419700507771 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; FB1A675F2132419700507771 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; FB1A67612132419A00507771 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; FB1A67632132419A00507771 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -437,8 +435,6 @@ FB1A67542132419700507771 /* Moonlight TV */ = { isa = PBXGroup; children = ( - FB1A67582132419700507771 /* ViewController.h */, - FB1A67592132419700507771 /* ViewController.m */, FB1A675E2132419700507771 /* Main.storyboard */, FB1A67612132419A00507771 /* Assets.xcassets */, FB1A67632132419A00507771 /* Info.plist */, @@ -1004,6 +1000,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9865DC3E21332D660005B9B9 /* MainFrameViewController.m in Sources */, 9865DC36213287F30005B9B9 /* AppDelegate.m in Sources */, FB1A6819213284FB00507771 /* UIComputerView.m in Sources */, FB1A681A213284FB00507771 /* UIAppView.m in Sources */, @@ -1045,7 +1042,6 @@ FB1A67A7213245BD00507771 /* StreamManager.m in Sources */, FB1A67A9213245BD00507771 /* VideoDecoderRenderer.m in Sources */, FB1A67A12132458C00507771 /* main.m in Sources */, - FB1A67A02132457D00507771 /* ViewController.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };