2014-08-31 13:13:46 -04:00
// MainFrameViewController . m
2015-05-12 21:48:30 -04:00
// Moonlight
2014-08-31 13:13:46 -04:00
//
// Created by Diego Waxemberg on 1 / 17 / 14.
2015-05-12 21:48:30 -04:00
// Copyright ( c ) 2014 Moonlight Stream . All rights reserved .
2014-08-31 13:13:46 -04:00
//
2015-11-10 21:23:00 -08:00
@ import ImageIO ;
2014-08-31 13:13:46 -04:00
# import "MainFrameViewController.h"
2014-10-16 21:20:27 -04:00
# import "CryptoManager.h"
2014-10-18 21:13:48 -04:00
# import "HttpManager.h"
2014-10-20 11:31:40 -04:00
# import "Connection.h"
2014-10-20 13:35:42 -04:00
# import "StreamManager.h"
2014-10-20 20:54:03 -04:00
# import "Utils.h"
2014-10-24 16:02:10 -04:00
# import "UIComputerView.h"
# import "UIAppView.h"
2014-10-29 00:47:51 -04:00
# import "DataManager.h"
2015-12-02 00:25:38 -08:00
# import "TemporarySettings.h"
2015-01-02 19:15:47 -05:00
# import "WakeOnLanManager.h"
2015-02-01 04:26:47 -05:00
# import "AppListResponse.h"
# import "ServerInfoResponse.h"
2015-02-01 19:46:28 -05:00
# import "StreamFrameViewController.h"
2015-02-24 22:13:33 -05:00
# import "LoadingFrameViewController.h"
2015-09-30 19:13:37 -07:00
# import "ComputerScrollView.h"
2015-09-30 20:34:52 -07:00
# import "TemporaryApp.h"
2015-10-31 23:03:21 -07:00
# import "IdManager.h"
2018-03-27 08:50:40 +02:00
# import "ConnectionHelper.h"
2014-08-31 13:13:46 -04:00
2018-08-26 11:56:27 -07:00
# if ! TARGET_OS _TV
# import "SettingsViewController.h"
2019-10-12 18:07:47 -07:00
# else
# import < sys / utsname . h >
2018-08-26 11:56:27 -07:00
# endif
2018-06-02 13:25:53 -07:00
# import < VideoToolbox / VideoToolbox . h >
2020-04-03 17:46:04 -07:00
# include < Limelight . h >
2014-10-20 11:31:40 -04:00
@ implementation MainFrameViewController {
NSOperationQueue * _opQueue ;
2015-12-01 20:31:24 -08:00
TemporaryHost * _selectedHost ;
2020-11-02 20:32:57 -06:00
BOOL _showHiddenApps ;
2014-10-26 02:15:53 -04:00
NSString * _uniqueId ;
2018-12-22 00:05:48 -08:00
NSData * _clientCert ;
2015-01-01 22:30:03 -05:00
DiscoveryManager * _discMan ;
2015-01-31 20:10:14 -05:00
AppAssetManager * _appManager ;
2015-02-01 19:46:28 -05:00
StreamConfiguration * _streamConfig ;
2015-06-23 22:23:12 -07:00
UIAlertController * _pairAlert ;
2018-08-26 11:56:27 -07:00
LoadingFrameViewController * _loadingFrame ;
2014-10-26 02:15:53 -04:00
UIScrollView * hostScrollView ;
2019-09-11 19:07:53 -07:00
FrontViewPosition currentPosition ;
2015-07-11 01:28:52 -07:00
NSArray * _sortedAppList ;
2015-11-10 21:23:00 -08:00
NSCache * _boxArtCache ;
2018-05-31 00:46:41 -07:00
bool _background ;
2018-08-26 15:05:32 -07:00
# if TARGET_OS _TV
UITapGestureRecognizer * _menuRecognizer ;
2018-08-26 11:56:27 -07:00
# endif
2014-10-20 11:31:40 -04:00
}
2014-10-26 02:15:53 -04:00
static NSMutableSet * hostList ;
2014-08-31 13:13:46 -04:00
2019-11-04 21:41:42 -08:00
- ( void ) startPairing : ( NSString * ) PIN {
// Needs to be synchronous to ensure the alert is shown before any potential
// failure callback could be invoked .
dispatch_sync ( dispatch_get _main _queue ( ) , ^ {
2018-04-21 22:20:27 -07:00
self -> _pairAlert = [ UIAlertController alertControllerWithTitle : @ "Pairing"
2019-07-05 22:18:30 -07:00
message : [ NSString stringWithFormat : @ "Enter the following PIN on the host machine: %@" , PIN ]
2018-08-26 11:56:27 -07:00
preferredStyle : UIAlertControllerStyleAlert ] ;
2018-04-21 22:20:27 -07:00
[ self -> _pairAlert addAction : [ UIAlertAction actionWithTitle : @ "Cancel" style : UIAlertActionStyleDestructive handler : ^ ( UIAlertAction * action ) {
self -> _pairAlert = nil ;
[ self -> _discMan startDiscovery ] ;
2019-11-03 15:11:02 -08:00
[ self hideLoadingFrame : ^ {
[ self showHostSelectionView ] ;
} ] ;
2016-10-21 12:07:29 -07:00
} ] ] ;
2018-08-26 11:56:27 -07:00
[ [ self activeViewController ] presentViewController : self -> _pairAlert animated : YES completion : nil ] ;
2014-10-20 13:35:42 -04:00
} ) ;
}
2019-11-03 15:11:02 -08:00
- ( void ) displayPairingFailureDialog : ( NSString * ) message {
2017-05-07 16:51:57 -07:00
UIAlertController * failedDialog = [ UIAlertController alertControllerWithTitle : @ "Pairing Failed"
2018-08-26 11:56:27 -07:00
message : message
preferredStyle : UIAlertControllerStyleAlert ] ;
2018-08-29 09:36:51 -07:00
[ Utils addHelpOptionToDialog : failedDialog ] ;
2018-05-28 18:20:59 -07:00
[ failedDialog addAction : [ UIAlertAction actionWithTitle : @ "OK" style : UIAlertActionStyleDefault handler : nil ] ] ;
2018-08-26 12:25:22 -07:00
2019-11-03 15:11:02 -08:00
[ _discMan startDiscovery ] ;
2018-08-26 12:25:22 -07:00
[ self hideLoadingFrame : ^ {
2019-11-03 15:11:02 -08:00
[ self showHostSelectionView ] ;
2018-08-26 12:25:22 -07:00
[ [ self activeViewController ] presentViewController : failedDialog animated : YES completion : nil ] ;
} ] ;
2017-05-07 16:51:57 -07:00
}
2014-10-20 13:35:42 -04:00
- ( void ) pairFailed : ( NSString * ) message {
2015-07-09 21:02:19 -07:00
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
2018-04-21 22:20:27 -07:00
if ( self -> _pairAlert ! = nil ) {
[ self -> _pairAlert dismissViewControllerAnimated : YES completion : ^ {
2019-11-03 15:11:02 -08:00
[ self displayPairingFailureDialog : message ] ;
2017-05-07 16:51:57 -07:00
} ] ;
2018-04-21 22:20:27 -07:00
self -> _pairAlert = nil ;
2017-05-07 16:51:57 -07:00
}
2014-10-20 13:35:42 -04:00
} ) ;
}
2018-12-22 00:05:48 -08:00
- ( void ) pairSuccessful : ( NSData * ) serverCert {
2015-07-09 21:02:19 -07:00
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
2018-12-22 00:05:48 -08:00
// Store the cert from pairing with the host
self -> _selectedHost . serverCert = serverCert ;
2018-04-21 22:20:27 -07:00
[ self -> _pairAlert dismissViewControllerAnimated : YES completion : nil ] ;
self -> _pairAlert = nil ;
2018-12-22 00:05:48 -08:00
2018-04-21 22:20:27 -07:00
[ self -> _discMan startDiscovery ] ;
2015-07-09 20:39:15 -07:00
[ self alreadyPaired ] ;
2014-10-20 13:35:42 -04:00
} ) ;
}
2019-09-14 12:46:50 -07:00
- ( void ) disableUpButton {
# if ! TARGET_OS _TV
[ self -> _upButton setTitle : nil ] ;
# endif
}
- ( void ) enableUpButton {
# if ! TARGET_OS _TV
[ self -> _upButton setTitle : @ "Select New Host" ] ;
# endif
}
2020-06-05 16:03:38 -07:00
- ( void ) updateTitle {
if ( _selectedHost ! = nil ) {
self . title = _selectedHost . name ;
}
else if ( [ hostList count ] = = 0 ) {
self . title = @ "Searching for PCs on your network..." ;
}
else {
self . title = @ "Select Host" ;
}
}
2014-10-26 02:15:53 -04:00
- ( void ) alreadyPaired {
2015-07-10 21:22:57 -07:00
BOOL usingCachedAppList = false ;
2015-07-11 23:59:09 -07:00
// Capture the host here because it can change once we
// leave the main thread
2015-12-01 20:31:24 -08:00
TemporaryHost * host = _selectedHost ;
2020-04-18 17:16:53 -07:00
if ( host = = nil ) {
[ self hideLoadingFrame : nil ] ;
return ;
}
2015-07-11 23:59:09 -07:00
if ( [ host . appList count ] > 0 ) {
2015-07-10 21:22:57 -07:00
usingCachedAppList = true ;
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
2018-04-21 22:20:27 -07:00
if ( host ! = self -> _selectedHost ) {
2018-11-16 20:17:03 -08:00
[ self hideLoadingFrame : nil ] ;
2015-07-11 23:59:09 -07:00
return ;
}
2020-08-30 15:30:58 -07:00
[ self updateAppsForHost : host ] ;
[ self hideLoadingFrame : nil ] ;
2015-07-10 21:22:57 -07:00
} ) ;
}
Log ( LOG_I , @ "Using cached app list: %d" , usingCachedAppList ) ;
2014-10-26 02:15:53 -04:00
dispatch_async ( dispatch_get _global _queue ( DISPATCH_QUEUE _PRIORITY _DEFAULT , 0 ) , ^ {
2015-07-12 00:23:48 -07:00
// Exempt this host from discovery while handling the applist query
2020-04-18 17:16:53 -07:00
[ self -> _discMan pauseDiscoveryForHost : host ] ;
2015-11-11 18:14:59 -08:00
2018-12-22 00:05:48 -08:00
AppListResponse * appListResp = [ ConnectionHelper getAppListForHostWithHostIP : host . activeAddress serverCert : host . serverCert uniqueID : self -> _uniqueId ] ;
2015-11-11 18:14:59 -08:00
2020-04-18 17:16:53 -07:00
[ self -> _discMan resumeDiscoveryForHost : host ] ;
2015-07-12 00:23:48 -07:00
2019-10-12 16:41:32 -07:00
if ( ! [ appListResp isStatusOk ] || [ appListResp getAppList ] = = nil ) {
2015-02-10 21:26:55 -05:00
Log ( LOG_W , @ "Failed to get applist: %@" , appListResp . statusMessage ) ;
2015-07-09 21:15:52 -07:00
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
2018-04-21 22:20:27 -07:00
if ( host ! = self -> _selectedHost ) {
2018-11-16 20:17:03 -08:00
[ self hideLoadingFrame : nil ] ;
2015-07-12 00:30:44 -07:00
return ;
}
2019-10-12 16:41:32 -07:00
UIAlertController * applistAlert = [ UIAlertController alertControllerWithTitle : @ "Connection Interrupted"
message : appListResp . statusMessage
2015-07-09 21:15:52 -07:00
preferredStyle : UIAlertControllerStyleAlert ] ;
2018-08-29 09:36:51 -07:00
[ Utils addHelpOptionToDialog : applistAlert ] ;
2018-05-28 18:20:59 -07:00
[ applistAlert addAction : [ UIAlertAction actionWithTitle : @ "OK" style : UIAlertActionStyleDefault handler : nil ] ] ;
2018-08-26 12:25:22 -07:00
[ self hideLoadingFrame : ^ {
2019-11-03 15:11:02 -08:00
[ self showHostSelectionView ] ;
2018-08-26 12:25:22 -07:00
[ [ self activeViewController ] presentViewController : applistAlert animated : YES completion : nil ] ;
} ] ;
2019-08-30 19:19:23 -07:00
host . state = StateOffline ;
2015-07-09 21:15:52 -07:00
} ) ;
2015-01-31 01:57:23 -05:00
} else {
2015-07-09 21:15:52 -07:00
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
2015-12-01 20:56:08 -08:00
[ self updateApplist : [ appListResp getAppList ] forHost : host ] ;
2015-07-11 23:59:09 -07:00
2018-04-21 22:20:27 -07:00
if ( host ! = self -> _selectedHost ) {
2018-11-16 20:17:03 -08:00
[ self hideLoadingFrame : nil ] ;
2015-07-11 23:59:09 -07:00
return ;
}
2020-08-30 15:30:58 -07:00
[ self updateAppsForHost : host ] ;
[ self -> _appManager stopRetrieving ] ;
[ self -> _appManager retrieveAssetsFromHost : host ] ;
[ self hideLoadingFrame : nil ] ;
2015-07-09 21:15:52 -07:00
} ) ;
2015-01-31 01:57:23 -05:00
}
2014-10-26 02:15:53 -04:00
} ) ;
}
2020-11-02 20:32:57 -06:00
- ( void ) updateAppEntry : ( TemporaryApp * ) app forHost : ( TemporaryHost * ) host {
DataManager * database = [ [ DataManager alloc ] init ] ;
NSMutableSet * newHostAppList = [ NSMutableSet setWithSet : host . appList ] ;
for ( TemporaryApp * savedApp in newHostAppList ) {
if ( [ app . id isEqualToString : savedApp . id ] ) {
savedApp . name = app . name ;
savedApp . hdrSupported = app . hdrSupported ;
savedApp . hidden = app . hidden ;
host . appList = newHostAppList ;
[ database updateAppsForExistingHost : host ] ;
return ;
}
}
}
2015-12-01 20:56:08 -08:00
- ( void ) updateApplist : ( NSSet * ) newList forHost : ( TemporaryHost * ) host {
2015-07-11 18:47:05 -07:00
DataManager * database = [ [ DataManager alloc ] init ] ;
2019-11-06 21:51:23 -08:00
NSMutableSet * newHostAppList = [ NSMutableSet setWithSet : host . appList ] ;
2015-07-10 22:19:25 -07:00
2016-01-21 14:03:07 -05:00
for ( TemporaryApp * app in newList ) {
BOOL appAlreadyInList = NO ;
2019-11-06 21:51:23 -08:00
for ( TemporaryApp * savedApp in newHostAppList ) {
2016-01-21 14:03:07 -05:00
if ( [ app . id isEqualToString : savedApp . id ] ) {
savedApp . name = app . name ;
2018-06-02 12:32:12 -07:00
savedApp . hdrSupported = app . hdrSupported ;
2020-11-02 20:32:57 -06:00
// Don ' t propagate hidden , because we want the local data to prevail
2016-01-21 14:03:07 -05:00
appAlreadyInList = YES ;
break ;
}
}
if ( ! appAlreadyInList ) {
app . host = host ;
2019-11-06 21:51:23 -08:00
[ newHostAppList addObject : app ] ;
2016-01-21 14:03:07 -05:00
}
}
BOOL appWasRemoved ;
do {
appWasRemoved = NO ;
2019-11-06 21:51:23 -08:00
for ( TemporaryApp * app in newHostAppList ) {
2016-01-21 14:03:07 -05:00
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 .
2019-11-06 21:51:23 -08:00
[ newHostAppList removeObject : app ] ;
2016-01-21 14:03:07 -05:00
2016-01-21 16:24:02 -05:00
// 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 ] ;
2016-01-21 14:03:07 -05:00
break ;
}
}
// Keep looping until the list is no longer being mutated
} while ( appWasRemoved ) ;
2019-11-06 21:51:23 -08:00
host . appList = newHostAppList ;
2015-09-30 20:44:14 -07:00
2016-01-21 14:03:07 -05:00
[ database updateAppsForExistingHost : host ] ;
2019-11-03 13:38:52 -08:00
// This host may be eligible for a shortcut now that the app list
// has been populated
[ self updateHostShortcuts ] ;
2015-07-10 21:22:57 -07:00
}
2014-12-04 19:54:21 +09:00
- ( void ) showHostSelectionView {
2018-08-26 15:05:32 -07:00
# if TARGET_OS _TV
// Remove the menu button intercept to allow the app to exit
// when at the host selection view .
2019-09-24 19:08:22 -07:00
[ self . navigationController . view removeGestureRecognizer : _menuRecognizer ] ;
2018-08-26 15:05:32 -07:00
# endif
2015-02-10 18:40:22 -05:00
[ _appManager stopRetrieving ] ;
2020-11-02 20:32:57 -06:00
_showHiddenApps = NO ;
2015-07-11 01:28:52 -07:00
_selectedHost = nil ;
2019-11-03 14:40:32 -08:00
_sortedAppList = nil ;
2019-08-30 21:15:11 -07:00
2020-06-05 16:03:38 -07:00
[ self updateTitle ] ;
2019-09-14 12:46:50 -07:00
[ self disableUpButton ] ;
2019-08-30 21:15:11 -07:00
2014-12-04 19:54:21 +09:00
[ self . collectionView reloadData ] ;
[ self . view addSubview : hostScrollView ] ;
}
2015-12-01 20:31:24 -08:00
- ( void ) receivedAssetForApp : ( TemporaryApp * ) app {
2015-11-10 21:23:00 -08:00
// Update the box art cache now so we don ' t have to do it
// on the main thread
[ self updateBoxArtCacheForApp : app ] ;
2015-07-11 23:59:09 -07:00
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
[ self . collectionView reloadData ] ;
} ) ;
2014-10-26 02:15:53 -04:00
}
2014-10-21 14:31:03 -04:00
- ( void ) displayDnsFailedDialog {
UIAlertController * alert = [ UIAlertController alertControllerWithTitle : @ "Network Error"
message : @ "Failed to resolve host."
preferredStyle : UIAlertControllerStyleAlert ] ;
2018-08-29 09:36:51 -07:00
[ Utils addHelpOptionToDialog : alert ] ;
2018-05-28 18:20:59 -07:00
[ alert addAction : [ UIAlertAction actionWithTitle : @ "OK" style : UIAlertActionStyleDefault handler : nil ] ] ;
2018-08-26 11:56:27 -07:00
[ [ self activeViewController ] presentViewController : alert animated : YES completion : nil ] ;
2014-10-21 14:31:03 -04:00
}
2015-12-01 20:31:24 -08:00
- ( void ) hostClicked : ( TemporaryHost * ) host view : ( UIView * ) view {
2015-07-09 21:15:52 -07:00
// 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 .
2019-08-30 19:19:23 -07:00
if ( host . state ! = StateOnline && view ! = nil ) {
2015-07-09 21:15:52 -07:00
[ self hostLongClicked : host view : view ] ;
return ;
}
2015-02-10 21:26:55 -05:00
Log ( LOG_D , @ "Clicked host: %@" , host . name ) ;
2015-01-01 22:30:03 -05:00
_selectedHost = host ;
2020-06-05 16:03:38 -07:00
[ self updateTitle ] ;
2019-11-03 15:12:43 -08:00
[ self enableUpButton ] ;
2015-07-11 23:59:09 -07:00
[ self disableNavigation ] ;
2015-07-11 21:59:41 -07:00
2018-08-26 15:05:32 -07:00
# if TARGET_OS _TV
// Intercept the menu key to go back to the host page
2019-09-24 19:08:22 -07:00
[ self . navigationController . view addGestureRecognizer : _menuRecognizer ] ;
2018-08-26 15:05:32 -07:00
# endif
2015-07-11 21:59:41 -07:00
// 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
2017-05-07 23:32:31 -07:00
// 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 .
2019-08-30 19:19:23 -07:00
if ( host . state = = StateOnline && host . pairState = = PairStatePaired && host . appList . count > 0 && view ! = nil ) {
2015-07-11 23:59:09 -07:00
[ self alreadyPaired ] ;
2015-07-11 21:59:41 -07:00
return ;
}
2018-11-15 22:02:05 -08:00
[ self showLoadingFrame : ^ {
dispatch_async ( dispatch_get _global _queue ( DISPATCH_QUEUE _PRIORITY _DEFAULT , 0 ) , ^ {
2019-11-03 15:17:27 -08:00
// Wait for the PC ' s status to be known
while ( host . state = = StateUnknown ) {
sleep ( 1 ) ;
}
// Don ' t bother polling if the server is already offline
if ( host . state = = StateOffline ) {
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
[ self hideLoadingFrame : ^ {
[ self showHostSelectionView ] ;
} ] ;
} ) ;
return ;
}
2018-12-22 00:05:48 -08:00
HttpManager * hMan = [ [ HttpManager alloc ] initWithHost : host . activeAddress uniqueId : self -> _uniqueId serverCert : host . serverCert ] ;
2018-11-15 22:02:05 -08:00
ServerInfoResponse * serverInfoResp = [ [ ServerInfoResponse alloc ] init ] ;
// Exempt this host from discovery while handling the serverinfo request
2020-04-18 17:16:53 -07:00
[ self -> _discMan pauseDiscoveryForHost : host ] ;
2018-11-16 19:02:31 -08:00
[ hMan executeRequestSynchronously : [ HttpRequest requestForResponse : serverInfoResp withUrlRequest : [ hMan newServerInfoRequest : false ]
2018-11-15 22:02:05 -08:00
fallbackError : 401 fallbackRequest : [ hMan newHttpServerInfoRequest ] ] ] ;
2020-04-18 17:16:53 -07:00
[ self -> _discMan resumeDiscoveryForHost : host ] ;
2018-11-15 22:02:05 -08:00
2019-10-12 16:41:32 -07:00
if ( ! [ serverInfoResp isStatusOk ] ) {
2018-11-15 22:02:05 -08:00
Log ( LOG_W , @ "Failed to get server info: %@" , serverInfoResp . statusMessage ) ;
2018-05-31 01:53:09 -07:00
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
2018-11-15 22:02:05 -08:00
if ( host ! = self -> _selectedHost ) {
[ self hideLoadingFrame : nil ] ;
return ;
}
2019-10-12 16:41:32 -07:00
UIAlertController * applistAlert = [ UIAlertController alertControllerWithTitle : @ "Connection Failed"
message : serverInfoResp . statusMessage
2018-11-15 22:02:05 -08:00
preferredStyle : UIAlertControllerStyleAlert ] ;
[ Utils addHelpOptionToDialog : applistAlert ] ;
[ applistAlert addAction : [ UIAlertAction actionWithTitle : @ "OK" style : UIAlertActionStyleDefault handler : nil ] ] ;
// Only display an alert if this was the result of a real
// user action , not just passively entering the foreground again
[ self hideLoadingFrame : ^ {
2019-11-03 15:11:02 -08:00
[ self showHostSelectionView ] ;
2018-11-15 22:02:05 -08:00
if ( view ! = nil ) {
[ [ self activeViewController ] presentViewController : applistAlert animated : YES completion : nil ] ;
}
2018-08-26 12:25:22 -07:00
} ] ;
2018-11-15 22:02:05 -08:00
2019-08-30 19:19:23 -07:00
host . state = StateOffline ;
2018-05-31 01:53:09 -07:00
} ) ;
2018-11-15 22:02:05 -08:00
} else {
2018-12-28 00:32:19 -08:00
// Update the host object with this data
[ serverInfoResp populateHost : host ] ;
if ( host . pairState = = PairStatePaired ) {
2018-11-15 22:02:05 -08:00
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 ] ;
2018-12-22 00:05:48 -08:00
PairManager * pMan = [ [ PairManager alloc ] initWithManager : hMan clientCert : self -> _clientCert callback : self ] ;
2018-11-15 22:02:05 -08:00
[ self -> _opQueue addOperation : pMan ] ;
}
else {
// Not user action , so just return to host screen
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
[ self hideLoadingFrame : ^ {
[ self showHostSelectionView ] ;
} ] ;
} ) ;
}
2018-05-31 01:53:09 -07:00
}
2018-11-15 22:02:05 -08:00
} ) ;
} ] ;
2014-10-26 02:15:53 -04:00
}
2018-08-26 11:56:27 -07:00
- ( UIViewController * ) activeViewController {
UIViewController * topController = [ UIApplication sharedApplication ] . keyWindow . rootViewController ;
while ( topController . presentedViewController ) {
topController = topController . presentedViewController ;
}
return topController ;
}
2015-12-01 20:31:24 -08:00
- ( void ) hostLongClicked : ( TemporaryHost * ) host view : ( UIView * ) view {
2015-02-10 21:26:55 -05:00
Log ( LOG_D , @ "Long clicked host: %@" , host . name ) ;
2019-08-30 19:19:23 -07:00
NSString * message ;
switch ( host . state ) {
case StateOffline :
message = @ "Offline" ;
break ;
case StateOnline :
if ( host . pairState = = PairStatePaired ) {
message = @ "Online - Paired" ;
}
else {
message = @ "Online - Not Paired" ;
}
break ;
case StateUnknown :
message = @ "Connecting" ;
break ;
default :
break ;
}
UIAlertController * longClickAlert = [ UIAlertController alertControllerWithTitle : host . name message : message preferredStyle : UIAlertControllerStyleActionSheet ] ;
if ( host . state ! = StateOnline ) {
2020-08-12 22:20:15 -07:00
[ longClickAlert addAction : [ UIAlertAction actionWithTitle : @ "Wake PC" style : UIAlertActionStyleDefault handler : ^ ( UIAlertAction * action ) {
2019-10-12 16:03:35 -07:00
UIAlertController * wolAlert = [ UIAlertController alertControllerWithTitle : @ "Wake-On-LAN" message : @ "" preferredStyle : UIAlertControllerStyleAlert ] ;
2015-01-02 19:15:47 -05:00
[ wolAlert addAction : [ UIAlertAction actionWithTitle : @ "OK" style : UIAlertActionStyleDefault handler : nil ] ] ;
2018-11-16 19:30:05 -08:00
if ( host . mac = = nil || [ host . mac isEqualToString : @ "00:00:00:00:00:00" ] ) {
2015-01-02 19:15:47 -05: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 ] ;
} ) ;
2019-10-12 16:03:35 -07:00
wolAlert . message = @ "Successfully sent wake-up request. It may take a few moments for the PC to wake. If it never wakes up, ensure it's properly configured for Wake-on-LAN." ;
2015-01-02 19:15:47 -05:00
}
2018-08-26 11:56:27 -07:00
[ [ self activeViewController ] presentViewController : wolAlert animated : YES completion : nil ] ;
2015-01-02 19:15:47 -05:00
} ] ] ;
2020-08-12 22:20:15 -07:00
}
2020-11-02 20:32:57 -06:00
else if ( host . pairState = = PairStatePaired ) {
2020-11-28 18:41:44 -06:00
[ longClickAlert addAction : [ UIAlertAction actionWithTitle : @ "View All Apps" style : UIAlertActionStyleDefault handler : ^ ( UIAlertAction * action ) {
2020-11-02 20:32:57 -06:00
self -> _showHiddenApps = YES ;
[ self hostClicked : host view : view ] ;
} ] ] ;
}
2020-08-12 22:20:15 -07:00
[ longClickAlert addAction : [ UIAlertAction actionWithTitle : @ "Test Network" style : UIAlertActionStyleDefault handler : ^ ( UIAlertAction * action ) {
[ self showLoadingFrame : ^ {
dispatch_async ( dispatch_get _global _queue ( DISPATCH_QUEUE _PRIORITY _DEFAULT , 0 ) , ^ {
// Perform the network test on a GCD worker thread . It may take a while .
unsigned int portTestResult = LiTestClientConnectivity ( CONN_TEST _SERVER , 443 , ML_PORT _FLAG _ALL ) ;
dispatch_sync ( dispatch_get _main _queue ( ) , ^ {
[ self hideLoadingFrame : ^ {
NSString * message ;
if ( portTestResult = = 0 ) {
2020-08-30 15:37:52 -07:00
message = @ "This network does not appear to be blocking Moonlight. If you still have trouble connecting, check your PC's firewall settings.\n\nVisit the Moonlight Setup Guide on GitHub for additional setup help and troubleshooting steps." ;
2020-08-12 22:20:15 -07:00
}
else if ( portTestResult = = ML_TEST _RESULT _INCONCLUSIVE ) {
message = @ "The network test could not be performed because none of Moonlight's connection testing servers were reachable. Check your Internet connection or try again later." ;
}
else {
2020-12-23 14:39:51 -06:00
char blockedPorts [ 512 ] ;
LiStringifyPortFlags ( portTestResult , "\n" , blockedPorts , sizeof ( blockedPorts ) ) ;
message = [ NSString stringWithFormat : @ "Your current network connection seems to be blocking Moonlight. Streaming may not work while connected to this network.\n\nThe following network ports were blocked:\n%s" , blockedPorts ] ;
2020-08-12 22:20:15 -07:00
}
UIAlertController * netTestAlert = [ UIAlertController alertControllerWithTitle : @ "Network Test Complete" message : message preferredStyle : UIAlertControllerStyleAlert ] ;
[ netTestAlert addAction : [ UIAlertAction actionWithTitle : @ "OK" style : UIAlertActionStyleDefault handler : nil ] ] ;
[ [ self activeViewController ] presentViewController : netTestAlert animated : YES completion : nil ] ;
} ] ;
} ) ;
} ) ;
} ] ;
} ] ] ;
2018-08-29 09:36:51 -07:00
# if ! TARGET_OS _TV
2020-08-12 22:20:15 -07:00
if ( host . state ! = StateOnline ) {
2018-05-28 18:00:19 -07:00
[ 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" ] ] ;
} ] ] ;
2015-01-02 19:15:47 -05:00
}
2020-08-12 22:20:15 -07:00
# endif
2015-01-01 23:25:09 -05:00
[ longClickAlert addAction : [ UIAlertAction actionWithTitle : @ "Remove Host" style : UIAlertActionStyleDestructive handler : ^ ( UIAlertAction * action ) {
2018-04-21 22:20:27 -07:00
[ self -> _discMan removeHostFromDiscovery : host ] ;
2015-01-01 23:40:52 -05:00
DataManager * dataMan = [ [ DataManager alloc ] init ] ;
[ dataMan removeHost : host ] ;
2015-01-08 19:37:51 -05:00
@ synchronized ( hostList ) {
[ hostList removeObject : host ] ;
2015-12-01 20:31:24 -08:00
[ self updateAllHosts : [ hostList allObjects ] ] ;
2015-01-08 19:37:51 -05:00
}
2015-01-01 23:25:09 -05:00
} ] ] ;
[ longClickAlert addAction : [ UIAlertAction actionWithTitle : @ "Cancel" style : UIAlertActionStyleCancel handler : nil ] ] ;
2015-01-08 19:37:51 -05:00
// these two lines are required for iPad support of UIAlertSheet
longClickAlert . popoverPresentationController . sourceView = view ;
2015-01-31 01:57:23 -05:00
2015-01-08 19:37:51 -05:00
longClickAlert . popoverPresentationController . sourceRect = CGRectMake ( view . bounds . size . width / 2.0 , view . bounds . size . height / 2.0 , 1.0 , 1.0 ) ; // center of the view
2020-11-14 15:15:46 -06:00
[ [ self activeViewController ] presentViewController : longClickAlert animated : YES completion : nil ] ;
2015-01-01 23:25:09 -05:00
}
2014-10-26 02:15:53 -04:00
- ( void ) addHostClicked {
2015-02-10 21:26:55 -05:00
Log ( LOG_D , @ "Clicked add host" ) ;
2020-06-05 16:03:38 -07:00
UIAlertController * alertController = [ UIAlertController alertControllerWithTitle : @ "Add Host Manually" message : @ "If Moonlight doesn't find your local gaming PC automatically,\nenter the IP address of your PC" preferredStyle : UIAlertControllerStyleAlert ] ;
2018-11-28 18:43:45 -08:00
[ alertController addAction : [ UIAlertAction actionWithTitle : @ "Cancel" style : UIAlertActionStyleCancel handler : nil ] ] ;
[ alertController addAction : [ UIAlertAction actionWithTitle : @ "OK" style : UIAlertActionStyleDefault handler : ^ ( UIAlertAction * action ) {
2020-10-06 21:57:22 -05:00
NSString * hostAddress = [ ( ( UITextField * ) [ [ alertController textFields ] objectAtIndex : 0 ] ) . text trim ] ;
2020-08-30 20:39:11 -07:00
[ self showLoadingFrame : ^ {
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 ( ) , ^ {
[ self hideLoadingFrame : ^ {
@ synchronized ( hostList ) {
[ hostList addObject : host ] ;
}
[ self updateHosts ] ;
} ] ;
} ) ;
} else {
unsigned int portTestResults = LiTestClientConnectivity ( CONN_TEST _SERVER , 443 ,
ML_PORT _FLAG _TCP _47984 | ML_PORT _FLAG _TCP _47989 ) ;
if ( portTestResults ! = ML_TEST _RESULT _INCONCLUSIVE && portTestResults ! = 0 ) {
error = [ error stringByAppendingString : @ "\n\nYour device's network connection is blocking Moonlight. Streaming may not work while connected to this network." ] ;
2018-11-28 18:43:45 -08:00
}
2020-08-30 20:39:11 -07:00
UIAlertController * hostNotFoundAlert = [ UIAlertController alertControllerWithTitle : @ "Add Host Manually" message : error preferredStyle : UIAlertControllerStyleAlert ] ;
[ Utils addHelpOptionToDialog : hostNotFoundAlert ] ;
[ hostNotFoundAlert addAction : [ UIAlertAction actionWithTitle : @ "OK" style : UIAlertActionStyleDefault handler : nil ] ] ;
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
[ self hideLoadingFrame : ^ {
[ [ self activeViewController ] presentViewController : hostNotFoundAlert animated : YES completion : nil ] ;
} ] ;
} ) ;
2020-08-10 22:56:47 -07:00
}
2020-08-30 20:39:11 -07:00
} ] ;
} ) ;
} ] ;
2018-11-28 18:43:45 -08:00
} ] ] ;
[ alertController addTextFieldWithConfigurationHandler : nil ] ;
[ [ self activeViewController ] presentViewController : alertController animated : YES completion : nil ] ;
2014-10-26 02:15:53 -04:00
}
2018-06-02 16:38:02 -07:00
- ( void ) prepareToStreamApp : ( TemporaryApp * ) app {
2015-02-01 19:46:28 -05:00
_streamConfig = [ [ StreamConfiguration alloc ] init ] ;
2015-07-11 23:59:09 -07:00
_streamConfig . host = app . host . activeAddress ;
2015-07-10 21:22:57 -07:00
_streamConfig . appID = app . id ;
2018-06-01 19:11:10 -07:00
_streamConfig . appName = app . name ;
2018-12-22 00:05:48 -08:00
_streamConfig . serverCert = app . host . serverCert ;
2014-10-20 20:54:03 -04:00
2014-10-29 00:47:51 -04:00
DataManager * dataMan = [ [ DataManager alloc ] init ] ;
2015-12-02 00:25:38 -08:00
TemporarySettings * streamSettings = [ dataMan getSettings ] ;
2014-10-29 00:47:51 -04:00
2015-02-01 19:46:28 -05:00
_streamConfig . frameRate = [ streamSettings . framerate intValue ] ;
2019-10-12 18:07:47 -07:00
if ( @ available ( iOS 10.3 , * ) ) {
// Don ' t stream more FPS than the display can show
if ( _streamConfig . frameRate > [ UIScreen mainScreen ] . maximumFramesPerSecond ) {
_streamConfig . frameRate = ( int ) [ UIScreen mainScreen ] . maximumFramesPerSecond ;
Log ( LOG_W , @ "Clamping FPS to maximum refresh rate: %d" , _streamConfig . frameRate ) ;
}
}
2015-02-01 19:46:28 -05:00
_streamConfig . height = [ streamSettings . height intValue ] ;
_streamConfig . width = [ streamSettings . width intValue ] ;
2019-10-12 19:20:26 -07:00
# if TARGET_OS _TV
2019-10-12 18:07:47 -07:00
// Don ' t allow streaming 4 K on the Apple TV HD
struct utsname systemInfo ;
uname ( & systemInfo ) ;
if ( strcmp ( systemInfo . machine , "AppleTV5,3" ) = = 0 && _streamConfig . height >= 2160 ) {
Log ( LOG_W , @ "4K streaming not supported on Apple TV HD" ) ;
_streamConfig . width = 1920 ;
_streamConfig . height = 1080 ;
}
# endif
_streamConfig . bitRate = [ streamSettings . bitrate intValue ] ;
2018-06-02 17:07:38 -07:00
_streamConfig . optimizeGameSettings = streamSettings . optimizeGames ;
_streamConfig . playAudioOnPC = streamSettings . playAudioOnPC ;
2018-06-02 18:13:32 -07:00
_streamConfig . allowHevc = streamSettings . useHevc ;
2022-02-02 13:37:07 -03:00
_streamConfig . useFramePacing = streamSettings . useFramePacing ;
2018-06-02 13:25:53 -07:00
2018-06-02 13:52:58 -07:00
// multiController must be set before calling getConnectedGamepadMask
2018-06-02 17:07:38 -07:00
_streamConfig . multiController = streamSettings . multiController ;
2018-06-02 13:52:58 -07:00
_streamConfig . gamepadMask = [ ControllerSupport getConnectedGamepadMask : _streamConfig ] ;
2022-02-06 18:00:14 -06:00
// Probe for supported channel configurations
int physicalOutputChannels = ( int ) [ AVAudioSession sharedInstance ] . maximumOutputNumberOfChannels ;
Log ( LOG_I , @ "Audio device supports %d channels" , physicalOutputChannels ) ;
int numberOfChannels = MIN ( [ streamSettings . audioConfig intValue ] , physicalOutputChannels ) ;
Log ( LOG_I , @ "Selected number of audio channels %d" , numberOfChannels ) ;
2021-07-11 09:10:55 -07:00
if ( numberOfChannels >= 8 ) {
2020-04-03 17:46:04 -07:00
_streamConfig . audioConfiguration = AUDIO_CONFIGURATION _71 _SURROUND ;
}
2021-07-11 09:10:55 -07:00
else if ( numberOfChannels >= 6 ) {
2020-04-03 17:46:04 -07:00
_streamConfig . audioConfiguration = AUDIO_CONFIGURATION _51 _SURROUND ;
2019-08-26 22:46:35 -07:00
}
else {
2020-04-03 17:46:04 -07:00
_streamConfig . audioConfiguration = AUDIO_CONFIGURATION _STEREO ;
2019-08-26 22:46:35 -07:00
}
2018-06-02 13:25:53 -07:00
// 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 + ) .
//
2018-06-02 17:07:38 -07:00
// 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 .
2022-02-02 13:37:07 -03:00
if ( @ available ( iOS 11.3 , tvOS 11.2 , * ) ) {
2018-06-02 13:25:53 -07:00
_streamConfig . enableHdr =
app . hdrSupported && // App supported
( app . host . serverCodecModeSupport & 0 x200 ) ! = 0 && // HEVC Main10 encoding on host PC GPU
VTIsHardwareDecodeSupported ( kCMVideoCodecType_HEVC ) && // Decoder supported
( AVPlayer . availableHDRModes & AVPlayerHDRModeHDR10 ) ! = 0 && // Display supported
2018-06-02 17:07:38 -07:00
streamSettings . enableHdr ; // User wants it enabled
2018-06-02 13:25:53 -07:00
}
2018-06-02 16:38:02 -07:00
}
2020-11-04 14:11:27 +10:30
- ( void ) appLongClicked : ( TemporaryApp * ) app view : ( UIView * ) view {
2020-11-02 20:32:57 -06:00
Log ( LOG_D , @ "Long clicked app: %@" , app . name ) ;
2014-10-29 00:47:51 -04:00
2015-05-28 23:47:30 -07:00
[ _appManager stopRetrieving ] ;
2014-11-27 11:36:28 -05:00
2018-08-26 11:56:27 -07:00
# if ! TARGET_OS _TV
2014-11-27 11:36:28 -05:00
if ( currentPosition ! = FrontViewPositionLeft ) {
2018-10-27 11:57:29 -07:00
// This must not be animated because we need the position
// to change ( and notify our callback to save settings data )
// before we call prepareToStreamApp .
[ [ self revealViewController ] revealToggleAnimated : NO ] ;
2014-11-27 11:36:28 -05:00
}
2018-08-26 11:56:27 -07:00
# endif
2020-11-02 20:32:57 -06:00
2015-12-01 20:31:24 -08:00
TemporaryApp * currentApp = [ self findRunningApp : app . host ] ;
2020-11-02 20:32:57 -06:00
NSString * message ;
if ( currentApp = = nil || [ app . id isEqualToString : currentApp . id ] ) {
if ( app . hidden ) {
message = @ "Hidden" ;
}
else {
message = @ "" ;
}
}
else {
message = [ NSString stringWithFormat : @ "%@ is currently running" , currentApp . name ] ;
}
UIAlertController * alertController = [ UIAlertController
alertControllerWithTitle : app . name
message : message
preferredStyle : UIAlertControllerStyleActionSheet ] ;
[ alertController addAction : [ UIAlertAction
actionWithTitle : currentApp = = nil ? @ "Launch App" : ( [ app . id isEqualToString : currentApp . id ] ? @ "Resume App" : @ "Resume Running App" ) style : UIAlertActionStyleDefault handler : ^ ( UIAlertAction * action ) {
if ( currentApp ! = nil ) {
Log ( LOG_I , @ "Resuming application: %@" , currentApp . name ) ;
[ self prepareToStreamApp : currentApp ] ;
}
else {
Log ( LOG_I , @ "Launching application: %@" , app . name ) ;
[ self prepareToStreamApp : app ] ;
}
[ self performSegueWithIdentifier : @ "createStreamFrame" sender : nil ] ;
} ] ] ;
2015-01-01 16:50:10 -05:00
if ( currentApp ! = nil ) {
2015-01-09 17:25:18 -05:00
[ alertController addAction : [ UIAlertAction actionWithTitle :
2015-07-10 21:22:57 -07:00
[ app . id isEqualToString : currentApp . id ] ? @ "Quit App" : @ "Quit Running App and Start" style : UIAlertActionStyleDestructive handler : ^ ( UIAlertAction * action ) {
Log ( LOG_I , @ "Quitting application: %@" , currentApp . name ) ;
2018-11-15 22:02:05 -08:00
[ self showLoadingFrame : ^ {
dispatch_async ( dispatch_get _global _queue ( DISPATCH_QUEUE _PRIORITY _DEFAULT , 0 ) , ^ {
2018-12-22 00:05:48 -08:00
HttpManager * hMan = [ [ HttpManager alloc ] initWithHost : app . host . activeAddress uniqueId : self -> _uniqueId serverCert : app . host . serverCert ] ;
2018-11-15 22:02:05 -08:00
HttpResponse * quitResponse = [ [ HttpResponse alloc ] init ] ;
HttpRequest * quitRequest = [ HttpRequest requestForResponse : quitResponse withUrlRequest : [ hMan newQuitAppRequest ] ] ;
// Exempt this host from discovery while handling the quit operation
2020-04-18 17:16:53 -07:00
[ self -> _discMan pauseDiscoveryForHost : app . host ] ;
2018-11-15 22:02:05 -08:00
[ hMan executeRequestSynchronously : quitRequest ] ;
if ( quitResponse . statusCode = = 200 ) {
ServerInfoResponse * serverInfoResp = [ [ ServerInfoResponse alloc ] init ] ;
2018-11-16 19:02:31 -08:00
[ hMan executeRequestSynchronously : [ HttpRequest requestForResponse : serverInfoResp withUrlRequest : [ hMan newServerInfoRequest : false ]
2018-11-15 22:02:05 -08:00
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 ;
}
2018-12-28 00:32:19 -08:00
else if ( [ serverInfoResp isStatusOk ] ) {
// Update the host object with this info
[ serverInfoResp populateHost : app . host ] ;
}
2016-09-24 21:26:50 -07:00
}
2020-04-18 17:16:53 -07:00
[ self -> _discMan resumeDiscoveryForHost : app . host ] ;
2019-09-14 13:08:47 -07:00
2018-11-15 22:02:05 -08:00
// If it fails , display an error and stop the current operation
if ( quitResponse . statusCode ! = 200 ) {
2019-09-14 13:08:47 -07:00
UIAlertController * alert = [ UIAlertController alertControllerWithTitle : @ "Quitting App Failed"
2018-11-15 22:02:05 -08:00
message : @ "Failed to quit app. If this app was started by "
"another device, you'll need to quit from that device."
preferredStyle : UIAlertControllerStyleAlert ] ;
2019-09-14 13:08:47 -07:00
[ alert addAction : [ UIAlertAction actionWithTitle : @ "OK" style : UIAlertActionStyleDefault handler : nil ] ] ;
2018-11-15 22:02:05 -08:00
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
[ self updateAppsForHost : app . host ] ;
[ self hideLoadingFrame : ^ {
2019-09-14 13:08:47 -07:00
[ [ self activeViewController ] presentViewController : alert animated : YES completion : nil ] ;
2018-11-15 22:02:05 -08:00
} ] ;
} ) ;
}
else {
app . host . currentGame = @ "0" ;
2019-09-14 13:08:47 -07:00
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
// If it succeeds and we ' re to start streaming , segue to the stream
if ( ! [ app . id isEqualToString : currentApp . id ] ) {
[ self prepareToStreamApp : app ] ;
[ self hideLoadingFrame : ^ {
[ self performSegueWithIdentifier : @ "createStreamFrame" sender : nil ] ;
} ] ;
}
else {
// Otherwise , just hide the loading icon
[ self hideLoadingFrame : nil ] ;
}
} ) ;
2018-11-15 22:02:05 -08:00
}
2015-07-09 20:33:23 -07:00
} ) ;
2018-11-15 22:02:05 -08:00
} ] ;
2015-01-31 01:57:23 -05:00
} ] ] ;
2020-11-02 20:32:57 -06:00
}
if ( currentApp = = nil || ! [ app . id isEqualToString : currentApp . id ] || app . hidden ) {
2020-11-28 18:41:44 -06:00
[ alertController addAction : [ UIAlertAction actionWithTitle : app . hidden ? @ "Show App" : @ "Hide App"
style : app . hidden ? UIAlertActionStyleDefault : UIAlertActionStyleDestructive
handler : ^ ( UIAlertAction * action ) {
2020-11-02 20:32:57 -06:00
app . hidden = ! app . hidden ;
[ self updateAppEntry : app forHost : app . host ] ;
// Don ' t call updateAppsForHost because that will nuke this
// app immediately if we ' re not showing hidden apps .
} ] ] ;
}
[ alertController addAction : [ UIAlertAction actionWithTitle : @ "Cancel" style : UIAlertActionStyleCancel handler : nil ] ] ;
2020-11-04 14:11:27 +10:30
// these two lines are required for iPad support of UIAlertSheet
alertController . popoverPresentationController . sourceView = view ;
alertController . popoverPresentationController . sourceRect = CGRectMake ( view . bounds . size . width / 2.0 , view . bounds . size . height / 2.0 , 1.0 , 1.0 ) ; // center of the view
2020-11-02 20:32:57 -06:00
[ [ self activeViewController ] presentViewController : alertController animated : YES completion : nil ] ;
}
2020-11-04 14:11:27 +10:30
- ( void ) appClicked : ( TemporaryApp * ) app view : ( UIView * ) view {
2020-11-02 20:32:57 -06:00
Log ( LOG_D , @ "Clicked app: %@" , app . name ) ;
[ _appManager stopRetrieving ] ;
# if ! TARGET_OS _TV
if ( currentPosition ! = FrontViewPositionLeft ) {
// This must not be animated because we need the position
// to change ( and notify our callback to save settings data )
// before we call prepareToStreamApp .
[ [ self revealViewController ] revealToggleAnimated : NO ] ;
}
# endif
if ( [ self findRunningApp : app . host ] ) {
// If there ' s a running app , display a menu
2020-11-04 14:11:27 +10:30
[ self appLongClicked : app view : view ] ;
2015-01-01 16:50:10 -05:00
} else {
2018-06-02 16:38:02 -07:00
[ self prepareToStreamApp : app ] ;
2015-01-01 16:50:10 -05:00
[ self performSegueWithIdentifier : @ "createStreamFrame" sender : nil ] ;
}
}
2015-12-01 20:31:24 -08:00
- ( TemporaryApp * ) findRunningApp : ( TemporaryHost * ) host {
for ( TemporaryApp * app in host . appList ) {
2016-10-21 11:41:42 -07:00
if ( [ app . id isEqualToString : host . currentGame ] ) {
2015-01-01 16:50:10 -05:00
return app ;
}
}
return nil ;
}
2018-08-26 11:56:27 -07:00
# if ! TARGET_OS _TV
2014-10-29 00:47:51 -04:00
- ( void ) revealController : ( SWRevealViewController * ) revealController didMoveToPosition : ( FrontViewPosition ) position {
// If we moved back to the center position , we should save the settings
if ( position = = FrontViewPositionLeft ) {
[ ( SettingsViewController * ) [ revealController rearViewController ] saveSettings ] ;
2014-10-20 20:54:03 -04:00
}
2018-05-28 12:04:38 -07:00
2014-11-27 11:36:28 -05:00
currentPosition = position ;
2014-08-31 13:13:46 -04:00
}
2018-08-26 11:56:27 -07:00
# endif
# if TARGET_OS _TV
- ( void ) collectionView : ( UICollectionView * ) collectionView didSelectItemAtIndexPath : ( NSIndexPath * ) indexPath {
2020-11-04 14:11:27 +10:30
[ self appClicked : _sortedAppList [ indexPath . row ] view : nil ] ;
2018-08-26 11:56:27 -07:00
}
# endif
2014-08-31 13:13:46 -04:00
2015-02-01 19:46:28 -05:00
- ( void ) prepareForSegue : ( UIStoryboardSegue * ) segue sender : ( id ) sender {
if ( [ segue . destinationViewController isKindOfClass : [ StreamFrameViewController class ] ] ) {
StreamFrameViewController * streamFrame = segue . destinationViewController ;
streamFrame . streamConfig = _streamConfig ;
}
}
2018-11-15 22:02:05 -08:00
- ( void ) showLoadingFrame : ( void ( ^ ) ( void ) ) completion {
2018-12-28 16:45:05 -08:00
[ _loadingFrame showLoadingFrame : completion ] ;
2015-02-24 22:13:33 -05:00
}
2018-08-26 12:25:22 -07:00
- ( void ) hideLoadingFrame : ( void ( ^ ) ( void ) ) completion {
2018-12-28 16:45:05 -08:00
[ self enableNavigation ] ;
[ _loadingFrame dismissLoadingFrame : completion ] ;
2015-02-24 22:13:33 -05:00
}
2019-09-14 12:28:21 -07:00
- ( void ) adjustScrollViewForSafeArea : ( UIScrollView * ) view {
if ( @ available ( iOS 11.0 , * ) ) {
if ( self . view . safeAreaInsets . left >= 20 || self . view . safeAreaInsets . right >= 20 ) {
view . contentInset = UIEdgeInsetsMake ( 0 , 20 , 0 , 20 ) ;
}
}
}
// Adjust the subviews for the safe area on the iPhone X .
- ( void ) viewSafeAreaInsetsDidChange {
[ super viewSafeAreaInsetsDidChange ] ;
[ self adjustScrollViewForSafeArea : self . collectionView ] ;
[ self adjustScrollViewForSafeArea : self -> hostScrollView ] ;
}
2014-08-31 13:13:46 -04:00
- ( void ) viewDidLoad
{
[ super viewDidLoad ] ;
2019-11-03 15:17:27 -08:00
2018-08-26 11:56:27 -07:00
# if ! TARGET_OS _TV
2015-02-01 19:46:28 -05:00
// Set the side bar button action . When it ' s tapped , it ' ll show the sidebar .
2019-08-30 21:15:11 -07:00
[ _settingsButton setTarget : self . revealViewController ] ;
[ _settingsButton setAction : @ selector ( revealToggle : ) ] ;
2014-10-29 00:47:51 -04:00
2015-02-01 19:46:28 -05:00
// Set the host name button action . When it ' s tapped , it ' ll show the host selection view .
2019-08-30 21:15:11 -07:00
[ _upButton setTarget : self ] ;
[ _upButton setAction : @ selector ( showHostSelectionView ) ] ;
2019-09-14 12:46:50 -07:00
[ self disableUpButton ] ;
2014-10-29 00:47:51 -04:00
// Set the gesture
[ self . view addGestureRecognizer : self . revealViewController . panGestureRecognizer ] ;
2014-11-27 13:56:39 -05:00
// Get callbacks associated with the viewController
2014-10-29 00:47:51 -04:00
[ self . revealViewController setDelegate : self ] ;
2019-09-18 19:59:24 -07:00
// Disable bounce - back on reveal VC otherwise the settings will snap closed
// if the user drags all the way off the screen opposite the settings pane .
self . revealViewController . bounceBackOnOverdraw = NO ;
2018-08-26 15:05:32 -07:00
# else
2019-09-24 18:56:37 -07:00
// The settings button will direct the user into the Settings app on tvOS
[ _settingsButton setTarget : self ] ;
[ _settingsButton setAction : @ selector ( openTvSettings : ) ] ;
2019-09-22 20:33:38 -07:00
// Restore focus on the selected app on view controller pop navigation
self . restoresFocusAfterTransition = NO ;
self . collectionView . remembersLastFocusedIndexPath = YES ;
2018-08-26 15:05:32 -07:00
_menuRecognizer = [ [ UITapGestureRecognizer alloc ] init ] ;
[ _menuRecognizer addTarget : self action : @ selector ( showHostSelectionView ) ] ;
_menuRecognizer . allowedPressTypes = [ [ NSArray alloc ] initWithObjects : [ NSNumber numberWithLong : UIPressTypeMenu ] , nil ] ;
2018-08-26 19:00:16 -07:00
self . navigationController . navigationBar . titleTextAttributes = [ NSDictionary dictionaryWithObject : [ UIColor whiteColor ] forKey : NSForegroundColorAttributeName ] ;
2018-08-26 11:56:27 -07:00
# endif
2014-10-29 00:47:51 -04:00
2018-08-26 12:25:22 -07:00
_loadingFrame = [ self . storyboard instantiateViewControllerWithIdentifier : @ "loadingFrame" ] ;
2014-11-27 11:36:28 -05:00
// Set the current position to the center
currentPosition = FrontViewPositionLeft ;
2014-10-19 04:19:54 -04:00
2014-11-27 13:56:39 -05:00
// Set up crypto
2018-08-27 00:34:34 -07:00
[ CryptoManager generateKeyPairUsingSSL ] ;
2015-10-31 23:03:21 -07:00
_uniqueId = [ IdManager getUniqueId ] ;
2018-12-22 00:05:48 -08:00
_clientCert = [ CryptoManager readCertFromFile ] ;
2015-01-31 20:10:14 -05:00
_appManager = [ [ AppAssetManager alloc ] initWithCallback : self ] ;
2015-02-01 19:46:28 -05:00
_opQueue = [ [ NSOperationQueue alloc ] init ] ;
2015-01-08 22:18:26 -05:00
2014-10-29 00:47:51 -04:00
// Only initialize the host picker list once
2014-10-26 02:15:53 -04:00
if ( hostList = = nil ) {
hostList = [ [ NSMutableSet alloc ] init ] ;
}
2014-10-24 16:02:10 -04:00
2015-11-10 21:23:00 -08:00
_boxArtCache = [ [ NSCache alloc ] init ] ;
2019-09-15 15:46:23 -07:00
2015-09-30 19:13:37 -07:00
hostScrollView = [ [ ComputerScrollView alloc ] init ] ;
2014-10-29 00:47:51 -04:00
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 ) ;
2014-10-24 16:02:10 -04:00
[ hostScrollView setShowsHorizontalScrollIndicator : NO ] ;
2015-09-30 19:13:37 -07:00
hostScrollView . delaysContentTouches = NO ;
self . collectionView . delaysContentTouches = NO ;
2015-10-18 16:52:14 -07:00
self . collectionView . allowsMultipleSelection = NO ;
2018-08-26 11:56:27 -07:00
# if ! TARGET_OS _TV
2015-10-18 16:52:14 -07:00
self . collectionView . multipleTouchEnabled = NO ;
2020-11-28 20:16:26 -06:00
# else
// This is the only way to get long press events on a UICollectionViewCell : (
UILongPressGestureRecognizer * cellLongPress = [ [ UILongPressGestureRecognizer alloc ] initWithTarget : self action : @ selector ( handleCollectionViewLongPress : ) ] ;
cellLongPress . delaysTouchesBegan = YES ;
[ self . collectionView addGestureRecognizer : cellLongPress ] ;
2018-08-26 11:56:27 -07:00
# endif
2014-10-29 00:47:51 -04:00
2014-11-26 17:57:16 -05:00
[ self retrieveSavedHosts ] ;
2015-01-01 22:30:03 -05:00
_discMan = [ [ DiscoveryManager alloc ] initWithHosts : [ hostList allObjects ] andCallback : self ] ;
2019-11-03 15:17:27 -08:00
2020-08-30 15:30:58 -07:00
if ( [ hostList count ] = = 1 ) {
2019-11-03 15:17:27 -08:00
[ self hostClicked : [ hostList anyObject ] view : nil ] ;
}
else {
2020-06-05 16:03:38 -07:00
[ self updateTitle ] ;
2019-11-03 15:17:27 -08:00
[ self . view addSubview : hostScrollView ] ;
}
2014-10-21 05:11:51 -04:00
}
2019-09-24 18:56:37 -07:00
# if TARGET_OS _TV
2020-11-28 20:16:26 -06:00
- ( void ) handleCollectionViewLongPress : ( UILongPressGestureRecognizer * ) gestureRecognizer
{
// FIXME : Something is delaying touches so we only get to the Begin state
// before we actually want to signal the long press .
if ( gestureRecognizer . state ! = UIGestureRecognizerStateBegan ) {
return ;
}
CGPoint point = [ gestureRecognizer locationInView : self . collectionView ] ;
NSIndexPath * indexPath = [ self . collectionView indexPathForItemAtPoint : point ] ;
if ( indexPath ! = nil ) {
[ self appLongClicked : _sortedAppList [ indexPath . row ] view : nil ] ;
}
}
2019-09-24 18:56:37 -07:00
- ( void ) openTvSettings : ( id ) sender
{
[ [ UIApplication sharedApplication ] openURL : [ NSURL URLWithString : UIApplicationOpenSettingsURLString ] options : @ { } completionHandler : nil ] ;
}
# endif
2020-04-19 14:40:39 -07:00
- ( void ) beginForegroundRefresh
2018-06-01 00:47:44 -07:00
{
if ( ! _background ) {
2018-06-02 16:31:16 -07:00
// This will kick off box art caching
[ self updateHosts ] ;
2020-04-18 15:03:52 -07:00
// Reset state first so we can rediscover hosts that were deleted before
[ _discMan resetDiscoveryState ] ;
2018-06-01 00:47:44 -07:00
[ _discMan startDiscovery ] ;
// This will refresh the applist when a paired host is selected
2020-04-19 14:40:39 -07:00
if ( _selectedHost ! = nil && _selectedHost . pairState = = PairStatePaired ) {
2018-06-01 00:47:44 -07:00
[ self hostClicked : _selectedHost view : nil ] ;
}
}
}
2019-11-03 13:38:52 -08:00
- ( void ) handlePendingShortcutAction
{
// Check if we have a pending shortcut action
AppDelegate * delegate = ( AppDelegate * ) [ UIApplication sharedApplication ] . delegate ;
if ( delegate . pcUuidToLoad ! = nil ) {
// Find the host it corresponds to
TemporaryHost * matchingHost = nil ;
for ( TemporaryHost * host in hostList ) {
if ( [ host . uuid isEqualToString : delegate . pcUuidToLoad ] ) {
matchingHost = host ;
break ;
}
}
// Clear the pending shortcut action
delegate . pcUuidToLoad = nil ;
// Complete the request
if ( delegate . shortcutCompletionHandler ! = nil ) {
delegate . shortcutCompletionHandler ( matchingHost ! = nil ) ;
delegate . shortcutCompletionHandler = nil ;
}
2019-11-05 20:30:17 -08:00
if ( matchingHost ! = nil && _selectedHost ! = matchingHost ) {
2019-11-03 13:38:52 -08:00
// Navigate to the host page
[ self hostClicked : matchingHost view : nil ] ;
}
}
}
2015-11-10 21:23:00 -08:00
- ( void ) handleReturnToForeground
{
2018-05-31 00:46:41 -07:00
_background = NO ;
2020-04-19 14:40:39 -07:00
[ self beginForegroundRefresh ] ;
2019-11-03 13:38:52 -08:00
// Check for a pending shortcut action when returning to foreground
[ self handlePendingShortcutAction ] ;
2015-11-10 21:23:00 -08:00
}
2018-05-31 00:46:41 -07:00
- ( void ) handleEnterBackground
{
_background = YES ;
[ _discMan stopDiscovery ] ;
}
2014-10-21 05:11:51 -04:00
- ( void ) viewDidAppear : ( BOOL ) animated
{
2014-11-26 17:57:16 -05:00
[ super viewDidAppear : animated ] ;
2017-09-15 00:30:51 -07:00
2018-08-26 11:56:27 -07:00
# if ! TARGET_OS _TV
2017-09-15 00:30:51 -07:00
[ [ self revealViewController ] setPrimaryViewController : self ] ;
2018-08-26 11:56:27 -07:00
# endif
2017-09-15 00:30:51 -07:00
2014-10-29 00:47:51 -04:00
[ self . navigationController setNavigationBarHidden : NO animated : YES ] ;
2014-12-04 18:41:01 +09:00
// Hide 1 px border line
UIImage * fakeImage = [ [ UIImage alloc ] init ] ;
[ self . navigationController . navigationBar setShadowImage : fakeImage ] ;
[ self . navigationController . navigationBar setBackgroundImage : fakeImage forBarPosition : UIBarPositionAny barMetrics : UIBarMetricsDefault ] ;
2019-11-03 13:38:52 -08:00
// Check for a pending shortcut action when appearing
[ self handlePendingShortcutAction ] ;
2018-06-09 16:28:37 -07:00
[ [ NSNotificationCenter defaultCenter ] addObserver : self
selector : @ selector ( handleReturnToForeground )
name : UIApplicationDidBecomeActiveNotification
object : nil ] ;
[ [ NSNotificationCenter defaultCenter ] addObserver : self
selector : @ selector ( handleEnterBackground )
name : UIApplicationWillResignActiveNotification
object : nil ] ;
2018-08-26 12:25:22 -07:00
}
- ( void ) viewWillAppear : ( BOOL ) animated
{
[ super viewWillAppear : animated ] ;
2018-06-09 16:28:37 -07:00
2018-05-31 00:46:41 -07:00
// 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
2018-06-01 00:47:44 -07:00
// 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 .
2020-04-19 14:40:39 -07:00
[ self beginForegroundRefresh ] ;
2014-10-21 05:11:51 -04:00
}
- ( void ) viewDidDisappear : ( BOOL ) animated
{
2014-11-26 17:57:16 -05:00
[ super viewDidDisappear : animated ] ;
2018-06-09 16:28:37 -07:00
// when discovery stops , we must create a new instance because
// you cannot restart an NSOperation when it is finished
2015-01-01 22:30:03 -05:00
[ _discMan stopDiscovery ] ;
2015-01-31 01:57:23 -05:00
2015-11-10 21:23:00 -08:00
// Purge the box art cache
[ _boxArtCache removeAllObjects ] ;
2018-06-09 16:28:37 -07:00
// Remove our lifetime observers to avoid triggering them
// while streaming
[ [ NSNotificationCenter defaultCenter ] removeObserver : self ] ;
2014-10-14 19:21:48 -04:00
}
2014-11-26 17:57:16 -05:00
- ( void ) retrieveSavedHosts {
DataManager * dataMan = [ [ DataManager alloc ] init ] ;
2015-12-02 00:25:38 -08:00
NSArray * hosts = [ dataMan getHosts ] ;
2015-01-01 23:54:54 -05:00
@ synchronized ( hostList ) {
[ hostList addObjectsFromArray : hosts ] ;
2015-07-10 00:28:51 -07:00
// Initialize the non - persistent host state
2015-12-01 20:31:24 -08:00
for ( TemporaryHost * host in hostList ) {
2015-07-10 12:18:09 -07:00
if ( host . activeAddress = = nil ) {
host . activeAddress = host . localAddress ;
}
if ( host . activeAddress = = nil ) {
host . activeAddress = host . externalAddress ;
}
if ( host . activeAddress = = nil ) {
host . activeAddress = host . address ;
}
2019-07-14 19:17:17 -07:00
if ( host . activeAddress = = nil ) {
host . activeAddress = host . ipv6Address ;
}
2015-07-10 00:28:51 -07:00
}
2015-01-01 23:54:54 -05:00
}
2014-11-26 17:57:16 -05:00
}
2015-01-01 22:30:03 -05:00
- ( void ) updateAllHosts : ( NSArray * ) hosts {
2020-12-06 00:56:16 -06:00
// We must copy the array here because it could be modified
// before our main thread dispatch happens .
NSArray * hostsCopy = [ NSArray arrayWithArray : hosts ] ;
2015-01-01 22:30:03 -05:00
dispatch_async ( dispatch_get _main _queue ( ) , ^ {
2015-02-10 21:26:55 -05:00
Log ( LOG_D , @ "New host list:" ) ;
2020-12-06 00:56:16 -06:00
for ( TemporaryHost * host in hostsCopy ) {
2019-08-30 19:19:23 -07:00
Log ( LOG_D , @ "Host: \n{\n\t name:%@ \n\t address:%@ \n\t localAddress:%@ \n\t externalAddress:%@ \n\t ipv6Address:%@ \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 . ipv6Address , host . uuid , host . mac , host . pairState , host . state , host . activeAddress ) ;
2015-01-01 22:30:03 -05:00
}
2015-01-01 23:54:54 -05:00
@ synchronized ( hostList ) {
[ hostList removeAllObjects ] ;
2020-12-06 00:56:16 -06:00
[ hostList addObjectsFromArray : hostsCopy ] ;
2015-01-01 23:54:54 -05:00
}
2015-01-01 22:30:03 -05:00
[ self updateHosts ] ;
} ) ;
}
2019-11-03 13:38:52 -08:00
- ( void ) updateHostShortcuts {
2019-11-03 15:25:43 -08:00
# if ! TARGET_OS _TV
2020-11-14 15:27:49 -06:00
NSMutableArray * quickActions = [ [ NSMutableArray alloc ] init ] ;
@ synchronized ( hostList ) {
for ( TemporaryHost * host in hostList ) {
// Pair state may be unknown if we haven ' t polled it yet , but the app list
// count will persist from paired PCs
if ( [ host . appList count ] > 0 ) {
UIApplicationShortcutItem * shortcut = [ [ UIApplicationShortcutItem alloc ]
initWithType : @ "PC"
localizedTitle : host . name
localizedSubtitle : nil
icon : [ UIApplicationShortcutIcon iconWithType : UIApplicationShortcutIconTypePlay ]
userInfo : [ NSDictionary dictionaryWithObject : host . uuid forKey : @ "UUID" ] ] ;
[ quickActions addObject : shortcut ] ;
2019-11-03 13:38:52 -08:00
}
}
}
2020-11-14 15:27:49 -06:00
[ UIApplication sharedApplication ] . shortcutItems = quickActions ;
2019-11-03 15:25:43 -08:00
# endif
2019-11-03 13:38:52 -08:00
}
2015-01-01 22:30:03 -05:00
- ( void ) updateHosts {
2015-02-10 21:26:55 -05:00
Log ( LOG_I , @ "Updating hosts..." ) ;
2014-10-26 02:15:53 -04:00
[ [ hostScrollView subviews ] makeObjectsPerformSelector : @ selector ( removeFromSuperview ) ] ;
UIComputerView * addComp = [ [ UIComputerView alloc ] initForAddWithCallback : self ] ;
UIComputerView * compView ;
float prevEdge = -1 ;
2015-01-01 23:54:54 -05:00
@ synchronized ( hostList ) {
2015-11-13 19:55:18 -08:00
// Sort the host list in alphabetical order
NSArray * sortedHostList = [ [ hostList allObjects ] sortedArrayUsingSelector : @ selector ( compareName : ) ] ;
2015-12-01 20:31:24 -08:00
for ( TemporaryHost * comp in sortedHostList ) {
2015-01-01 23:54:54 -05:00
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 ] ;
2018-06-02 16:31:16 -07:00
// 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 ] ;
} ) ;
}
2015-01-01 23:54:54 -05:00
}
2014-10-26 02:15:53 -04:00
}
2015-11-10 21:23:00 -08:00
2019-11-03 13:38:52 -08:00
// Create or delete host shortcuts as needed
[ self updateHostShortcuts ] ;
2020-06-05 16:03:38 -07:00
// Update the title in case we now have a PC
[ self updateTitle ] ;
2014-10-26 02:15:53 -04:00
prevEdge = [ self getCompViewX : addComp addComp : addComp prevEdge : prevEdge ] ;
addComp . center = CGPointMake ( prevEdge , hostScrollView . frame . size . height / 2 ) ;
2014-10-21 13:39:14 -04:00
2014-10-26 02:15:53 -04:00
[ 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 {
2019-08-30 22:33:32 -07:00
float padding ;
# if TARGET_OS _TV
padding = 100 ;
# else
padding = addComp . frame . size . width / 2 ;
# endif
2014-10-26 02:15:53 -04:00
if ( prevEdge = = -1 ) {
2019-08-30 22:33:32 -07:00
return hostScrollView . frame . origin . x + comp . frame . size . width / 2 + padding ;
2014-10-26 02:15:53 -04:00
} else {
2019-08-30 22:33:32 -07:00
return prevEdge + comp . frame . size . width / 2 + padding ;
2014-10-26 02:15:53 -04:00
}
}
2015-11-10 21:23:00 -08:00
// This function forces immediate decoding of the UIImage , rather
// than the default lazy decoding that results in janky scrolling .
2015-12-01 20:31:24 -08:00
+ ( UIImage * ) loadBoxArtForCaching : ( TemporaryApp * ) app {
2018-06-02 16:31:16 -07:00
UIImage * boxArt ;
2015-11-10 21:23:00 -08:00
2018-08-27 01:54:58 -07:00
NSData * imageData = [ NSData dataWithContentsOfFile : [ AppAssetManager boxArtPathForApp : app ] ] ;
if ( imageData = = nil ) {
// No box art on disk
return nil ;
}
CGImageSourceRef source = CGImageSourceCreateWithData ( ( __bridge CFDataRef ) imageData , NULL ) ;
2018-06-02 16:31:16 -07:00
CGImageRef cgImage = CGImageSourceCreateImageAtIndex ( source , 0 , nil ) ;
2015-11-10 21:23:00 -08:00
2018-06-02 16:31:16 -07:00
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 ) ;
2015-11-10 21:23:00 -08:00
CGImageRelease ( cgImage ) ;
CFRelease ( source ) ;
return boxArt ;
}
2015-12-01 20:31:24 -08:00
- ( void ) updateBoxArtCacheForApp : ( TemporaryApp * ) app {
2018-08-27 01:54:58 -07:00
if ( [ _boxArtCache objectForKey : app ] = = nil ) {
UIImage * image = [ MainFrameViewController loadBoxArtForCaching : app ] ;
if ( image ! = nil ) {
// Add the image to our cache if it was present
[ _boxArtCache setObject : image forKey : app ] ;
}
2015-11-10 21:23:00 -08:00
}
}
2015-12-01 20:31:24 -08:00
- ( void ) updateAppsForHost : ( TemporaryHost * ) host {
2015-07-11 23:59:09 -07:00
if ( host ! = _selectedHost ) {
Log ( LOG_W , @ "Mismatched host during app update" ) ;
return ;
}
_sortedAppList = [ host . appList allObjects ] ;
2015-07-11 01:28:52 -07:00
_sortedAppList = [ _sortedAppList sortedArrayUsingSelector : @ selector ( compareName : ) ] ;
2020-11-02 20:32:57 -06:00
if ( ! _showHiddenApps ) {
NSMutableArray * visibleAppList = [ NSMutableArray array ] ;
for ( TemporaryApp * app in _sortedAppList ) {
if ( ! app . hidden ) {
[ visibleAppList addObject : app ] ;
}
}
_sortedAppList = visibleAppList ;
}
2014-12-04 18:41:01 +09:00
[ hostScrollView removeFromSuperview ] ;
[ self . collectionView reloadData ] ;
2014-10-26 02:15:53 -04:00
}
2014-12-04 18:41:01 +09:00
- ( UICollectionViewCell * ) collectionView : ( UICollectionView * ) collectionView cellForItemAtIndexPath : ( NSIndexPath * ) indexPath {
UICollectionViewCell * cell = [ collectionView dequeueReusableCellWithReuseIdentifier : @ "AppCell" forIndexPath : indexPath ] ;
2015-12-01 20:31:24 -08:00
TemporaryApp * app = _sortedAppList [ indexPath . row ] ;
2015-11-10 21:23:00 -08:00
UIAppView * appView = [ [ UIAppView alloc ] initWithApp : app cache : _boxArtCache andCallback : self ] ;
2015-01-01 16:50:10 -05:00
2014-12-04 18:41:01 +09:00
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 ) ;
2014-10-21 13:39:14 -04:00
}
2015-01-01 16:50:10 -05:00
2014-12-04 18:41:01 +09:00
[ cell . subviews . firstObject removeFromSuperview ] ; // Remove a view that was previously added
[ cell addSubview : appView ] ;
2015-09-28 21:18:35 -07:00
2020-11-14 16:45:43 -06:00
// Shadow opacity is controlled inside UIAppView based on whether the app
// is hidden or not during the update cycle .
2015-09-28 21:18:35 -07:00
UIBezierPath * shadowPath = [ UIBezierPath bezierPathWithRect : cell . bounds ] ;
cell . layer . masksToBounds = NO ;
cell . layer . shadowColor = [ UIColor blackColor ] . CGColor ;
cell . layer . shadowOffset = CGSizeMake ( 1.0 f , 5.0 f ) ;
cell . layer . shadowPath = shadowPath . CGPath ;
2018-08-26 11:56:27 -07:00
# if ! TARGET_OS _TV
2018-10-19 00:03:14 -04:00
cell . layer . borderWidth = 1 ;
2018-08-26 11:56:27 -07:00
cell . layer . borderColor = [ [ UIColor colorWithRed : 0 green : 0 blue : 0 alpha : 0.3 f ] CGColor ] ;
2015-10-18 16:52:14 -07:00
cell . exclusiveTouch = YES ;
2018-08-26 11:56:27 -07:00
# endif
2015-10-18 16:52:14 -07:00
2014-12-04 18:41:01 +09:00
return cell ;
}
- ( NSInteger ) numberOfSectionsInCollectionView : ( UICollectionView * ) collectionView {
return 1 ; // App collection only
}
- ( NSInteger ) collectionView : ( UICollectionView * ) collectionView numberOfItemsInSection : ( NSInteger ) section {
2019-11-03 14:40:32 -08:00
if ( _selectedHost ! = nil && _sortedAppList ! = nil ) {
return _sortedAppList . count ;
2015-07-11 01:28:52 -07:00
}
else {
return 0 ;
}
2014-08-31 13:13:46 -04:00
}
- ( void ) didReceiveMemoryWarning
{
[ super didReceiveMemoryWarning ] ;
2018-08-29 09:24:47 -07:00
// Purge the box art cache on low memory
[ _boxArtCache removeAllObjects ] ;
2014-08-31 13:13:46 -04:00
}
2014-10-20 21:39:40 -04:00
2014-10-20 21:45:37 -04:00
- ( void ) touchesBegan : ( NSSet * ) touches withEvent : ( UIEvent * ) event {
[ self . view endEditing : YES ] ;
}
- ( BOOL ) textFieldShouldReturn : ( UITextField * ) textField {
[ textField resignFirstResponder ] ;
return YES ;
}
2018-08-26 11:56:27 -07:00
# if ! TARGET_OS _TV
2014-10-20 21:39:40 -04:00
- ( BOOL ) shouldAutorotate {
return YES ;
}
2018-08-26 11:56:27 -07:00
# endif
2015-02-01 19:46:28 -05:00
2015-07-11 23:59:09 -07:00
- ( void ) disableNavigation {
self . navigationController . navigationBar . topItem . rightBarButtonItem . enabled = NO ;
2019-11-03 15:13:13 -08:00
self . navigationController . navigationBar . topItem . leftBarButtonItem . enabled = NO ;
2015-07-11 23:59:09 -07:00
}
- ( void ) enableNavigation {
self . navigationController . navigationBar . topItem . rightBarButtonItem . enabled = YES ;
2019-11-03 15:13:13 -08:00
self . navigationController . navigationBar . topItem . leftBarButtonItem . enabled = YES ;
2015-07-11 23:59:09 -07:00
}
2018-10-19 00:03:14 -04:00
# if TARGET_OS _TV
- ( BOOL ) canBecomeFocused {
return YES ;
}
# endif
2018-08-26 11:56:27 -07:00
- ( void ) didUpdateFocusInContext : ( UIFocusUpdateContext * ) context withAnimationCoordinator : ( UIFocusAnimationCoordinator * ) coordinator {
2018-10-19 00:03:14 -04:00
# if ! TARGET_OS _TV
2018-08-26 11:56:27 -07:00
if ( context . nextFocusedView ! = nil ) {
[ context . nextFocusedView setAlpha : 0.8 ] ;
}
[ context . previouslyFocusedView setAlpha : 1.0 ] ;
2018-10-19 00:03:14 -04:00
# endif
2018-08-26 11:56:27 -07:00
}
2014-08-31 13:13:46 -04:00
@ end