Share MainFrameViewController between iOS and tvOS

This commit is contained in:
Cameron Gutman 2018-08-26 11:56:27 -07:00
parent f5b99b6b9c
commit 1ef5c87afc
6 changed files with 83 additions and 986 deletions

View File

@ -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 <VideoToolbox/VideoToolbox.h>
@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

View File

@ -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];
}

View File

@ -26,10 +26,10 @@
</objects>
<point key="canvasLocation" x="-4555" y="-1581"/>
</scene>
<!--View Controller-->
<!--Main Frame View Controller-->
<scene sceneID="duv-ef-Eeh">
<objects>
<collectionViewController id="vKn-MI-uG7" customClass="ViewController" sceneMemberID="viewController">
<collectionViewController id="vKn-MI-uG7" customClass="MainFrameViewController" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" prefetchingEnabled="YES" id="8P0-wP-bDe">
<rect key="frame" x="0.0" y="0.0" width="1920" height="1080"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>

View File

@ -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 <UIKit/UIKit.h>
#import "DiscoveryManager.h"
#import "PairManager.h"
#import "StreamConfiguration.h"
#import "UIComputerView.h"
#import "UIAppView.h"
#import "AppAssetManager.h"
@interface ViewController : UICollectionViewController <DiscoveryCallback, PairCallback, HostCallback, AppCallback, AppAssetCallback, NSURLConnectionDelegate>
@end

View File

@ -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 <VideoToolbox/VideoToolbox.h>
@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

View File

@ -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 = "<group>"; };
FB1A674C2131E65900507771 /* KeyboardSupport.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KeyboardSupport.m; sourceTree = "<group>"; };
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 = "<group>"; };
FB1A67592132419700507771 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = "<group>"; };
FB1A675F2132419700507771 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
FB1A67612132419A00507771 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
FB1A67632132419A00507771 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -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;
};