diff --git a/Limelight.xcodeproj/project.pbxproj b/Limelight.xcodeproj/project.pbxproj index 458f187..0c12d33 100644 --- a/Limelight.xcodeproj/project.pbxproj +++ b/Limelight.xcodeproj/project.pbxproj @@ -47,6 +47,7 @@ FB8946EB19F6AFE100339C8A /* libcrypto.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB8946E019F6AFB800339C8A /* libcrypto.a */; }; FB8946EC19F6AFE400339C8A /* libssl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB8946E119F6AFB800339C8A /* libssl.a */; }; FB8946ED19F6AFE800339C8A /* libopus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB8946EA19F6AFB800339C8A /* libopus.a */; }; + FBD3494319FC9C04002D2A60 /* AppManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD3494219FC9C04002D2A60 /* AppManager.m */; }; FBDE86E019F7A837001C18A8 /* UIComputerView.m in Sources */ = {isa = PBXBuildFile; fileRef = FBDE86DF19F7A837001C18A8 /* UIComputerView.m */; }; FBDE86E619F82297001C18A8 /* UIAppView.m in Sources */ = {isa = PBXBuildFile; fileRef = FBDE86E519F82297001C18A8 /* UIAppView.m */; }; FBDE86E919F82315001C18A8 /* App.m in Sources */ = {isa = PBXBuildFile; fileRef = FBDE86E819F82315001C18A8 /* App.m */; }; @@ -214,12 +215,14 @@ FB8946E719F6AFB800339C8A /* opus_multistream.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = opus_multistream.h; sourceTree = ""; }; FB8946E819F6AFB800339C8A /* opus_types.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = opus_types.h; sourceTree = ""; }; FB8946EA19F6AFB800339C8A /* libopus.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libopus.a; sourceTree = ""; }; + FBD3494119FC9C04002D2A60 /* AppManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppManager.h; sourceTree = ""; }; + FBD3494219FC9C04002D2A60 /* AppManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppManager.m; sourceTree = ""; }; FBDE86DE19F7A837001C18A8 /* UIComputerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIComputerView.h; sourceTree = ""; }; FBDE86DF19F7A837001C18A8 /* UIComputerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIComputerView.m; sourceTree = ""; }; FBDE86E419F82297001C18A8 /* UIAppView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIAppView.h; sourceTree = ""; }; FBDE86E519F82297001C18A8 /* UIAppView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIAppView.m; sourceTree = ""; }; - FBDE86E719F82315001C18A8 /* App.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = App.h; sourceTree = ""; }; - FBDE86E819F82315001C18A8 /* App.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = App.m; sourceTree = ""; }; + FBDE86E719F82315001C18A8 /* App.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = App.h; path = ../App.h; sourceTree = ""; }; + FBDE86E819F82315001C18A8 /* App.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = App.m; path = ../App.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -320,8 +323,6 @@ FBDE86DF19F7A837001C18A8 /* UIComputerView.m */, FBDE86E419F82297001C18A8 /* UIAppView.h */, FBDE86E519F82297001C18A8 /* UIAppView.m */, - FBDE86E719F82315001C18A8 /* App.h */, - FBDE86E819F82315001C18A8 /* App.m */, ); path = Limelight; sourceTree = ""; @@ -386,6 +387,8 @@ FB89461219F646E200339C8A /* MDNSManager.m */, FB89461319F646E200339C8A /* PairManager.h */, FB89461419F646E200339C8A /* PairManager.m */, + FBD3494119FC9C04002D2A60 /* AppManager.h */, + FBD3494219FC9C04002D2A60 /* AppManager.m */, ); path = Network; sourceTree = ""; @@ -410,6 +413,8 @@ children = ( FB89461F19F646E200339C8A /* Computer.h */, FB89462019F646E200339C8A /* Computer.m */, + FBDE86E719F82315001C18A8 /* App.h */, + FBDE86E819F82315001C18A8 /* App.m */, FB89462119F646E200339C8A /* Utils.h */, FB89462219F646E200339C8A /* Utils.m */, ); @@ -725,6 +730,7 @@ FB89462819F646E200339C8A /* CryptoManager.m in Sources */, FB89462E19F646E200339C8A /* PairManager.m in Sources */, FB290D0019B2C406004C83CF /* main.m in Sources */, + FBD3494319FC9C04002D2A60 /* AppManager.m in Sources */, FB89462A19F646E200339C8A /* ControllerSupport.m in Sources */, FB89463119F646E200339C8A /* StreamManager.m in Sources */, ); diff --git a/Limelight/App.h b/Limelight/App.h index 31eab9d..2c35cea 100644 --- a/Limelight/App.h +++ b/Limelight/App.h @@ -10,6 +10,8 @@ @interface App : NSObject -@property NSString* displayName; +@property NSString* appId; +@property NSString* appName; +@property UIImage* appImage; @end diff --git a/Limelight/App.m b/Limelight/App.m index 2e71b67..f83476d 100644 --- a/Limelight/App.m +++ b/Limelight/App.m @@ -7,7 +7,9 @@ // #import "App.h" +#import "HttpManager.h" @implementation App +@synthesize appId, appName, appImage; @end diff --git a/Limelight/Images.xcassets/AddComputer.imageset/Contents.json b/Limelight/Images.xcassets/AddComputer.imageset/Contents.json new file mode 100644 index 0000000..142a64c --- /dev/null +++ b/Limelight/Images.xcassets/AddComputer.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x", + "filename" : "limelight_computer_add_1x.png" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "limelight_computer_add_2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x", + "filename" : "limelight_computer_add_3x.png" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Limelight/Images.xcassets/AddComputer.imageset/limelight_computer_add_1x.png b/Limelight/Images.xcassets/AddComputer.imageset/limelight_computer_add_1x.png new file mode 100644 index 0000000..ae65f59 Binary files /dev/null and b/Limelight/Images.xcassets/AddComputer.imageset/limelight_computer_add_1x.png differ diff --git a/Limelight/Images.xcassets/AddComputer.imageset/limelight_computer_add_2x.png b/Limelight/Images.xcassets/AddComputer.imageset/limelight_computer_add_2x.png new file mode 100644 index 0000000..77eab3b Binary files /dev/null and b/Limelight/Images.xcassets/AddComputer.imageset/limelight_computer_add_2x.png differ diff --git a/Limelight/Images.xcassets/AddComputer.imageset/limelight_computer_add_3x.png b/Limelight/Images.xcassets/AddComputer.imageset/limelight_computer_add_3x.png new file mode 100644 index 0000000..a63a263 Binary files /dev/null and b/Limelight/Images.xcassets/AddComputer.imageset/limelight_computer_add_3x.png differ diff --git a/Limelight/Images.xcassets/AddComputerIcon.imageset/Contents.json b/Limelight/Images.xcassets/AddComputerIcon.imageset/Contents.json new file mode 100644 index 0000000..86541cf --- /dev/null +++ b/Limelight/Images.xcassets/AddComputerIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "limelight_computer_add_icon_2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Limelight/Images.xcassets/AddComputerIcon.imageset/limelight_computer_add_icon_2x.png b/Limelight/Images.xcassets/AddComputerIcon.imageset/limelight_computer_add_icon_2x.png new file mode 100644 index 0000000..661eddd Binary files /dev/null and b/Limelight/Images.xcassets/AddComputerIcon.imageset/limelight_computer_add_icon_2x.png differ diff --git a/Limelight/Images.xcassets/Computer.imageset/Contents.json b/Limelight/Images.xcassets/Computer.imageset/Contents.json index 8b6e0cc..3a06eed 100644 --- a/Limelight/Images.xcassets/Computer.imageset/Contents.json +++ b/Limelight/Images.xcassets/Computer.imageset/Contents.json @@ -3,7 +3,7 @@ { "idiom" : "universal", "scale" : "1x", - "filename" : "limelight_computer_2x-1.png" + "filename" : "limelight_computer_1x.png" }, { "idiom" : "universal", @@ -13,7 +13,7 @@ { "idiom" : "universal", "scale" : "3x", - "filename" : "limelight_computer_2x-2.png" + "filename" : "limelight_computer_3x.png" } ], "info" : { diff --git a/Limelight/Images.xcassets/Computer.imageset/limelight_computer_1x.png b/Limelight/Images.xcassets/Computer.imageset/limelight_computer_1x.png new file mode 100644 index 0000000..7c2e71f Binary files /dev/null and b/Limelight/Images.xcassets/Computer.imageset/limelight_computer_1x.png differ diff --git a/Limelight/Images.xcassets/Computer.imageset/limelight_computer_2x-1.png b/Limelight/Images.xcassets/Computer.imageset/limelight_computer_2x-1.png deleted file mode 100644 index df5cde9..0000000 Binary files a/Limelight/Images.xcassets/Computer.imageset/limelight_computer_2x-1.png and /dev/null differ diff --git a/Limelight/Images.xcassets/Computer.imageset/limelight_computer_2x-2.png b/Limelight/Images.xcassets/Computer.imageset/limelight_computer_2x-2.png deleted file mode 100644 index df5cde9..0000000 Binary files a/Limelight/Images.xcassets/Computer.imageset/limelight_computer_2x-2.png and /dev/null differ diff --git a/Limelight/Images.xcassets/Computer.imageset/limelight_computer_2x.png b/Limelight/Images.xcassets/Computer.imageset/limelight_computer_2x.png index df5cde9..25468de 100644 Binary files a/Limelight/Images.xcassets/Computer.imageset/limelight_computer_2x.png and b/Limelight/Images.xcassets/Computer.imageset/limelight_computer_2x.png differ diff --git a/Limelight/Images.xcassets/Computer.imageset/limelight_computer_3x.png b/Limelight/Images.xcassets/Computer.imageset/limelight_computer_3x.png new file mode 100644 index 0000000..e93d417 Binary files /dev/null and b/Limelight/Images.xcassets/Computer.imageset/limelight_computer_3x.png differ diff --git a/Limelight/Images.xcassets/NoAppImage.imageset/Contents.json b/Limelight/Images.xcassets/NoAppImage.imageset/Contents.json new file mode 100644 index 0000000..05a6f1b --- /dev/null +++ b/Limelight/Images.xcassets/NoAppImage.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x", + "filename" : "limelight_no_app_image_2x.png" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Limelight/Images.xcassets/NoAppImage.imageset/limelight_no_app_image_2x.png b/Limelight/Images.xcassets/NoAppImage.imageset/limelight_no_app_image_2x.png new file mode 100644 index 0000000..c043cd1 Binary files /dev/null and b/Limelight/Images.xcassets/NoAppImage.imageset/limelight_no_app_image_2x.png differ diff --git a/Limelight/Network/AppManager.h b/Limelight/Network/AppManager.h new file mode 100644 index 0000000..d70b103 --- /dev/null +++ b/Limelight/Network/AppManager.h @@ -0,0 +1,23 @@ +// +// AppManager.h +// Limelight +// +// Created by Diego Waxemberg on 10/25/14. +// Copyright (c) 2014 Limelight Stream. All rights reserved. +// + +#import +#import "App.h" +#import "HttpManager.h" + +@protocol AppAssetCallback + +- (void) receivedAssetForApp:(App*)app; + +@end + +@interface AppManager : NSObject + ++ (void) retrieveAppAssets:(NSArray*)apps withManager:(HttpManager*)hMan andCallback:(id)callback; + +@end diff --git a/Limelight/Network/AppManager.m b/Limelight/Network/AppManager.m new file mode 100644 index 0000000..df0c4b2 --- /dev/null +++ b/Limelight/Network/AppManager.m @@ -0,0 +1,45 @@ +// +// AppManager.m +// Limelight +// +// Created by Diego Waxemberg on 10/25/14. +// Copyright (c) 2014 Limelight Stream. All rights reserved. +// + +#import "AppManager.h" + +@implementation AppManager { + App* _app; + HttpManager* _hMan; + id _callback; +} + ++ (void) retrieveAppAssets:(NSArray*)apps withManager:(HttpManager*)hMan andCallback:(id)callback { + for (App* app in apps) { + AppManager* manager = [[AppManager alloc] initWithApp:app httpManager:hMan andCallback:callback]; + [manager retrieveAsset]; + } + +} + +- (id) initWithApp:(App*)app httpManager:(HttpManager*)hMan andCallback:(id)callback { + self = [super init]; + _app = app; + _hMan = hMan; + _callback = callback; + return self; +} + +- (void) retrieveAsset { + NSData* appAsset = [_hMan executeRequestSynchronously:[_hMan newAppAssetRequestWithAppId:_app.appId]]; + UIImage* appImage = [UIImage imageWithData:appAsset]; + _app.appImage = appImage; + NSLog(@"App Name: %@ id:%@ image: %@", _app.appName, _app.appId, _app.appImage); + [self performSelectorOnMainThread:@selector(sendCallBack) withObject:self waitUntilDone:NO]; +} + +- (void) sendCallBack { + [_callback receivedAssetForApp:_app]; +} + +@end diff --git a/Limelight/Network/HttpManager.h b/Limelight/Network/HttpManager.h index d24f6b4..c7c4b36 100644 --- a/Limelight/Network/HttpManager.h +++ b/Limelight/Network/HttpManager.h @@ -10,6 +10,7 @@ @interface HttpManager : NSObject ++ (NSArray*) getAppListFromXML:(NSData*)xml; + (NSString*) getStringFromXML:(NSData*)xml tag:(NSString*)tag; + (NSString*) getStatusStringFromXML:(NSData*)xml; @@ -24,6 +25,7 @@ - (NSURLRequest*) newServerInfoRequest; - (NSURLRequest*) newLaunchRequest:(NSString*)appId width:(int)width height:(int)height refreshRate:(int)refreshRate rikey:(NSString*)rikey rikeyid:(int)rikeyid; - (NSURLRequest*) newResumeRequestWithRiKey:(NSString*)riKey riKeyId:(int)riKeyId; +- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString*)appId; - (NSData*) executeRequestSynchronously:(NSURLRequest*)request; @end diff --git a/Limelight/Network/HttpManager.m b/Limelight/Network/HttpManager.m index 76f9a8f..3719607 100644 --- a/Limelight/Network/HttpManager.m +++ b/Limelight/Network/HttpManager.m @@ -8,6 +8,7 @@ #import "HttpManager.h" #import "CryptoManager.h" +#import "App.h" #include #include @@ -25,6 +26,60 @@ static const NSString* PORT = @"47984"; ++ (NSArray*) getAppListFromXML:(NSData*)xml { + xmlDocPtr docPtr = xmlParseMemory([xml bytes], (int)[xml length]); + + if (docPtr == NULL) { + NSLog(@"ERROR: An error occured trying to parse xml."); + return NULL; + } + + xmlNodePtr node; + xmlNodePtr rootNode = node = xmlDocGetRootElement(docPtr); + + // Check root status_code + if (![HttpManager verifyStatus: rootNode]) { + NSLog(@"ERROR: Request returned with failure status"); + return NULL; + } + + // Skip the root node + node = node->children; + + NSMutableArray* appList = [[NSMutableArray alloc] init]; + + while (node != NULL) { + NSLog(@"node: %s", node->name); + if (!xmlStrcmp(node->name, (const xmlChar*)"App")) { + xmlNodePtr appInfoNode = node->xmlChildrenNode; + NSString* appName; + NSString* appId; + while (appInfoNode != NULL) { + NSLog(@"appInfoNode: %s", appInfoNode->name); + if (!xmlStrcmp(appInfoNode->name, (const xmlChar*)"AppTitle")) { + xmlChar* nodeVal = xmlNodeListGetString(docPtr, appInfoNode->xmlChildrenNode, 1); + appName = [[NSString alloc] initWithCString:(const char*)nodeVal encoding:NSUTF8StringEncoding]; + xmlFree(nodeVal); + } else if (!xmlStrcmp(appInfoNode->name, (const xmlChar*)"ID")) { + xmlChar* nodeVal = xmlNodeListGetString(docPtr, appInfoNode->xmlChildrenNode, 1); + appId = [[NSString alloc] initWithCString:(const char*)nodeVal encoding:NSUTF8StringEncoding]; + xmlFree(nodeVal); + } + appInfoNode = appInfoNode->next; + } + App* app = [[App alloc] init]; + app.appName = appName; + app.appId = appId; + [appList addObject:app]; + } + node = node->next; + } + xmlFree(rootNode); + xmlFree(docPtr); + + return appList; +} + + (NSString*) getStatusStringFromXML:(NSData*)xml { xmlDocPtr docPtr = xmlParseMemory([xml bytes], (int)[xml length]); @@ -198,6 +253,11 @@ static const NSString* PORT = @"47984"; return [self createRequestFromString:urlString enableTimeout:FALSE]; } +- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString *)appId { + NSString* urlString = [NSString stringWithFormat:@"%@/appasset?uniqueid=%@&appid=%@&AssetType=2&AssetIdx=0", _baseURL, _uniqueId, appId]; + return [self createRequestFromString:urlString enableTimeout:FALSE]; +} + - (NSString*) bytesToHex:(NSData*)data { const unsigned char* bytes = [data bytes]; NSMutableString *hex = [[NSMutableString alloc] init]; @@ -217,7 +277,11 @@ static const NSString* PORT = @"47984"; } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { - _requestResp = [HttpManager fixXmlVersion:_respData]; + if ([[NSString alloc] initWithData:_respData encoding:NSUTF8StringEncoding] != nil) { + _requestResp = [HttpManager fixXmlVersion:_respData]; + } else { + _requestResp = _respData; + } dispatch_semaphore_signal(_requestLock); } diff --git a/Limelight/Network/PairManager.h b/Limelight/Network/PairManager.h index 12cd30e..0dc3f45 100644 --- a/Limelight/Network/PairManager.h +++ b/Limelight/Network/PairManager.h @@ -14,6 +14,7 @@ - (void) showPIN:(NSString*)PIN; - (void) pairSuccessful; - (void) pairFailed:(NSString*)message; +- (void) alreadyPaired; @end diff --git a/Limelight/Network/PairManager.m b/Limelight/Network/PairManager.m index b4c6488..0f72ecf 100644 --- a/Limelight/Network/PairManager.m +++ b/Limelight/Network/PairManager.m @@ -32,14 +32,13 @@ [_callback pairFailed:@"Unable to connect to PC"]; return; } - if (![[HttpManager getStringFromXML:serverInfo tag:@"currentgame"] isEqual:@"0"]) { [_callback pairFailed:@"You must stop streaming before attempting to pair."]; } else if (![[HttpManager getStringFromXML:serverInfo tag:@"PairStatus"] isEqual:@"1"]) { [self initiatePair]; } else { - [_callback pairFailed:@"This device is already paired."]; + [_callback alreadyPaired]; } } diff --git a/Limelight/Stream/StreamConfiguration.h b/Limelight/Stream/StreamConfiguration.h index 6129ed3..0490a0a 100644 --- a/Limelight/Stream/StreamConfiguration.h +++ b/Limelight/Stream/StreamConfiguration.h @@ -11,6 +11,7 @@ @interface StreamConfiguration : NSObject @property NSString* host; +@property NSString* appID; @property int hostAddr; @property int width; @property int height; diff --git a/Limelight/Stream/StreamConfiguration.m b/Limelight/Stream/StreamConfiguration.m index b2607bb..ea6ac30 100644 --- a/Limelight/Stream/StreamConfiguration.m +++ b/Limelight/Stream/StreamConfiguration.m @@ -9,5 +9,5 @@ #import "StreamConfiguration.h" @implementation StreamConfiguration -@synthesize host, hostAddr, width, height, frameRate, bitRate, riKeyId, riKey; +@synthesize host, appID, hostAddr, width, height, frameRate, bitRate, riKeyId, riKey; @end diff --git a/Limelight/Stream/StreamManager.m b/Limelight/Stream/StreamManager.m index d35d19f..bc64800 100644 --- a/Limelight/Stream/StreamManager.m +++ b/Limelight/Stream/StreamManager.m @@ -42,6 +42,7 @@ NSData* serverInfoResp = [hMan executeRequestSynchronously:[hMan newServerInfoRequest]]; NSString* currentGame = [HttpManager getStringFromXML:serverInfoResp tag:@"currentgame"]; NSString* pairStatus = [HttpManager getStringFromXML:serverInfoResp tag:@"PairStatus"]; + NSString* currentClient = [HttpManager getStringFromXML:serverInfoResp tag:@"CurrentClient"]; if (currentGame == NULL || pairStatus == NULL) { [_callbacks launchFailed:@"Failed to connect to PC"]; return; @@ -55,6 +56,11 @@ // resumeApp and launchApp handle calling launchFailed if (![currentGame isEqualToString:@"0"]) { + if (![currentClient isEqualToString:@"1"]) { + // The server is streaming to someone else + [_callbacks launchFailed:@"There is another stream in progress"]; + return; + } // App already running, resume it if (![self resumeApp:hMan]) { return; @@ -79,7 +85,7 @@ - (BOOL) launchApp:(HttpManager*)hMan { NSData* launchResp = [hMan executeRequestSynchronously: - [hMan newLaunchRequest:@"67339056" + [hMan newLaunchRequest:_config.appID width:_config.width height:_config.height refreshRate:_config.frameRate diff --git a/Limelight/UIAppView.h b/Limelight/UIAppView.h index a184b1d..9b202d5 100644 --- a/Limelight/UIAppView.h +++ b/Limelight/UIAppView.h @@ -9,8 +9,15 @@ #import #import "App.h" -@interface UIAppView : UIView +@protocol AppCallback -- (id) initWithApp:(App*)app; +- (void) appClicked:(App*) app; + +@end + +@interface UIAppView : UIView + +- (id) initWithApp:(App*)app andCallback:(id)callback; +- (void) updateAppImage; @end diff --git a/Limelight/UIAppView.m b/Limelight/UIAppView.m index 1d2ea6e..645d65d 100644 --- a/Limelight/UIAppView.m +++ b/Limelight/UIAppView.m @@ -12,32 +12,54 @@ App* _app; UIButton* _appButton; UILabel* _appLabel; + id _callback; } static int LABEL_DY = 20; -- (id) initWithApp:(App*)app { +- (id) initWithApp:(App*)app andCallback:(id)callback { self = [super init]; _app = app; + _callback = callback; _appButton = [UIButton buttonWithType:UIButtonTypeCustom]; [_appButton setContentEdgeInsets:UIEdgeInsetsMake(0, 4, 0, 4)]; - [_appButton setBackgroundImage:[[UIImage imageNamed:@"Left4Dead2"] resizableImageWithCapInsets:UIEdgeInsetsMake(10, 10, 10, 10)] forState:UIControlStateNormal]; + [_appButton setBackgroundImage:[UIImage imageNamed:@"NoAppImage"] forState:UIControlStateNormal]; [_appButton sizeToFit]; + [_appButton addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventTouchUpInside]; + _appButton.layer.shadowColor = [[UIColor blackColor] CGColor]; + _appButton.layer.shadowOffset = CGSizeMake(5,8); + _appButton.layer.shadowOpacity = 0.7; _appLabel = [[UILabel alloc] init]; - [_appLabel setText:_app.displayName]; + [_appLabel setText:_app.appName]; [_appLabel sizeToFit]; _appLabel.center = CGPointMake(_appButton.bounds.origin.x + (_appButton.bounds.size.width / 2), _appButton.bounds.origin.y + _appButton.bounds.size.height + LABEL_DY); + [self updateBounds]; [self addSubview:_appButton]; [self addSubview:_appLabel]; - self.frame = CGRectMake(0, 0, _appButton.frame.size.width > _appLabel.frame.size.width ? _appButton.frame.size.width : _appLabel.frame.size.width, _appButton.frame.size.height + _appLabel.frame.size.height); return self; } +- (void) updateBounds { + float x = _appButton.frame.origin.x < _appLabel.frame.origin.x ? _appButton.frame.origin.x : _appLabel.frame.origin.x; + float y = _appButton.frame.origin.y < _appLabel.frame.origin.y ? _appButton.frame.origin.y : _appLabel.frame.origin.y; + self.bounds = CGRectMake(x , y, _appButton.frame.size.width > _appLabel.frame.size.width ? _appButton.frame.size.width : _appLabel.frame.size.width, _appButton.frame.size.height + _appLabel.frame.size.height + LABEL_DY / 2); + self.frame = CGRectMake(x , y, _appButton.frame.size.width > _appLabel.frame.size.width ? _appButton.frame.size.width : _appLabel.frame.size.width, _appButton.frame.size.height + _appLabel.frame.size.height + LABEL_DY / 2); +} +- (void) appClicked { + [_callback appClicked:_app]; +} + +- (void) updateAppImage { + if (_app.appImage != nil) { + [_appButton setBackgroundImage:_app.appImage forState:UIControlStateNormal]; + [self setNeedsDisplay]; + } +} /* // Only override drawRect: if you perform custom drawing. diff --git a/Limelight/UIComputerView.h b/Limelight/UIComputerView.h index c603bd0..74b282a 100644 --- a/Limelight/UIComputerView.h +++ b/Limelight/UIComputerView.h @@ -9,8 +9,16 @@ #import #import "Computer.h" -@interface UIComputerView : UIView +@protocol HostCallback -- (id) initWithComputer:(Computer*)computer; +- (void) hostClicked:(Computer*)computer; +- (void) addHostClicked; + +@end + +@interface UIComputerView : UIView + +- (id) initWithComputer:(Computer*)computer andCallback:(id)callback; +- (id) initForAddWithCallback:(id)callback; @end diff --git a/Limelight/UIComputerView.m b/Limelight/UIComputerView.m index 0efb111..b97b91e 100644 --- a/Limelight/UIComputerView.m +++ b/Limelight/UIComputerView.m @@ -12,31 +12,82 @@ Computer* _computer; UIButton* _hostButton; UILabel* _hostLabel; + id _callback; + CGSize _labelSize; } static int LABEL_DY = 20; -- (id) initWithComputer:(Computer*)computer { +- (id) init { self = [super init]; - _computer = computer; - _hostButton = [UIButton buttonWithType:UIButtonTypeCustom]; [_hostButton setContentEdgeInsets:UIEdgeInsetsMake(0, 4, 0, 4)]; - [_hostButton setBackgroundImage:[[UIImage imageNamed:@"Computer"] resizableImageWithCapInsets:UIEdgeInsetsMake(10, 10, 10, 10)] forState:UIControlStateNormal]; + [_hostButton setBackgroundImage:[UIImage imageNamed:@"Computer"] forState:UIControlStateNormal]; [_hostButton sizeToFit]; + _hostButton.layer.shadowColor = [[UIColor blackColor] CGColor]; + _hostButton.layer.shadowOffset = CGSizeMake(5,8); + _hostButton.layer.shadowOpacity = 0.7; + _hostLabel = [[UILabel alloc] init]; + + return self; +} + +- (id) initWithComputer:(Computer*)computer andCallback:(id)callback { + self = [self init]; + _computer = computer; + _callback = callback; + [_hostLabel setText:[_computer displayName]]; [_hostLabel sizeToFit]; - _hostLabel.center = CGPointMake(_hostButton.bounds.origin.x + (_hostButton.bounds.size.width / 2), _hostButton.bounds.origin.y + _hostButton.bounds.size.height + LABEL_DY); - + [_hostButton addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventTouchUpInside]; + _hostLabel.center = CGPointMake(_hostButton.frame.origin.x + (_hostButton.frame.size.width / 2), _hostButton.frame.origin.y + _hostButton.frame.size.height + LABEL_DY); + [self updateBounds]; [self addSubview:_hostButton]; [self addSubview:_hostLabel]; - self.frame = CGRectMake(0, 0, _hostButton.frame.size.width > _hostLabel.frame.size.width ? _hostButton.frame.size.width : _hostLabel.frame.size.width, _hostButton.frame.size.height + _hostLabel.frame.size.height); + return self; +} + +- (void) updateBounds { + float x = _hostButton.frame.origin.x < _hostLabel.frame.origin.x ? _hostButton.frame.origin.x : _hostLabel.frame.origin.x; + float y = _hostButton.frame.origin.y < _hostLabel.frame.origin.y ? _hostButton.frame.origin.y : _hostLabel.frame.origin.y; + self.bounds = CGRectMake(x , y, _hostButton.frame.size.width > _hostLabel.frame.size.width ? _hostButton.frame.size.width : _hostLabel.frame.size.width, _hostButton.frame.size.height + _hostLabel.frame.size.height + LABEL_DY / 2); + self.frame = CGRectMake(x , y, _hostButton.frame.size.width > _hostLabel.frame.size.width ? _hostButton.frame.size.width : _hostLabel.frame.size.width, _hostButton.frame.size.height + _hostLabel.frame.size.height + LABEL_DY / 2); +} + +- (id) initForAddWithCallback:(id)callback { + self = [self init]; + _callback = callback; + + [_hostButton setBackgroundImage:[UIImage imageNamed:@"Computer"] forState:UIControlStateNormal]; + [_hostButton sizeToFit]; + [_hostButton addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventTouchUpInside]; + + [_hostLabel setText:@"Add Host"]; + [_hostLabel sizeToFit]; + _hostLabel.center = CGPointMake(_hostButton.frame.origin.x + (_hostButton.frame.size.width / 2), _hostButton.frame.origin.y + _hostButton.frame.size.height + LABEL_DY); + + UIImageView* addIcon = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"AddComputerIcon"]]; + [addIcon sizeToFit]; + addIcon.center = CGPointMake(_hostButton.frame.origin.x + _hostButton.frame.size.width, _hostButton.frame.origin.y); + + [self updateBounds]; + [self addSubview:_hostButton]; + [self addSubview:_hostLabel]; + [self addSubview:addIcon]; return self; } +- (void) hostClicked { + [_callback hostClicked:_computer]; +} + +- (void) addClicked { + [_callback addHostClicked]; +} + /* // Only override drawRect: if you perform custom drawing. // An empty implementation adversely affects performance during animation. diff --git a/Limelight/Utility/Computer.h b/Limelight/Utility/Computer.h index dc0058c..854a3dd 100644 --- a/Limelight/Utility/Computer.h +++ b/Limelight/Utility/Computer.h @@ -15,6 +15,5 @@ - (id) initWithHost:(NSNetService*)host; - (id) initWithIp:(NSString*)host; -- (id) initPlaceholder; @end diff --git a/Limelight/Utility/Computer.m b/Limelight/Utility/Computer.m index fa8deb9..6a7cb22 100644 --- a/Limelight/Utility/Computer.m +++ b/Limelight/Utility/Computer.m @@ -28,13 +28,4 @@ return self; } -- (id) initPlaceholder { - self = [super init]; - - self.hostName = NULL; - self.displayName = @"No computers found"; - - return self; -} - @end diff --git a/Limelight/ViewControllers/MainFrameViewController.h b/Limelight/ViewControllers/MainFrameViewController.h index 7265ca9..70e106e 100644 --- a/Limelight/ViewControllers/MainFrameViewController.h +++ b/Limelight/ViewControllers/MainFrameViewController.h @@ -10,8 +10,11 @@ #import "MDNSManager.h" #import "PairManager.h" #import "StreamConfiguration.h" +#import "UIComputerView.h" +#import "UIAppView.h" +#import "AppManager.h" -@interface MainFrameViewController : UIViewController +@interface MainFrameViewController : UIViewController + (StreamConfiguration*) getStreamConfiguration; diff --git a/Limelight/ViewControllers/MainFrameViewController.m b/Limelight/ViewControllers/MainFrameViewController.m index 1ebcd91..328b0f1 100644 --- a/Limelight/ViewControllers/MainFrameViewController.m +++ b/Limelight/ViewControllers/MainFrameViewController.m @@ -1,4 +1,3 @@ - // MainFrameViewController.m // Limelight-iOS // @@ -22,45 +21,21 @@ NSOperationQueue* _opQueue; MDNSManager* _mDNSManager; Computer* _selectedHost; + NSString* _uniqueId; + NSData* _cert; + UIAlertView* _pairAlert; + UIScrollView* hostScrollView; + UIScrollView* appScrollView; } +static NSString* deviceName = @"roth"; +static NSMutableSet* hostList; static StreamConfiguration* streamConfig; + (StreamConfiguration*) getStreamConfiguration { return streamConfig; } -//TODO: no more pair button -/* -- (void)PairButton:(UIButton *)sender -{ - NSLog(@"Pair Button Pressed!"); - if ([self.hostTextField.text length] > 0) { - _selectedHost = [[Computer alloc] initWithIp:self.hostTextField.text]; - NSLog(@"Using custom host: %@", self.hostTextField.text); - } - - if (![self validatePcSelected]) { - NSLog(@"No valid PC selected"); - return; - } - - [CryptoManager generateKeyPairUsingSSl]; - NSString* uniqueId = [CryptoManager getUniqueID]; - NSData* cert = [CryptoManager readCertFromFile]; - - if ([Utils resolveHost:_selectedHost.hostName] == 0) { - [self displayDnsFailedDialog]; - return; - } - - HttpManager* hMan = [[HttpManager alloc] initWithHost:_selectedHost.hostName uniqueId:uniqueId deviceName:@"roth" cert:cert]; - PairManager* pMan = [[PairManager alloc] initWithManager:hMan andCert:cert callback:self]; - - [_opQueue addOperation:pMan]; -} -*/ - - (void)showPIN:(NSString *)PIN { dispatch_sync(dispatch_get_main_queue(), ^{ _pairAlert = [[UIAlertView alloc] initWithTitle:@"Pairing" message:[NSString stringWithFormat:@"Enter the following PIN on the host machine: %@", PIN]delegate:self cancelButtonTitle:@"Ok" otherButtonTitles:nil, nil]; @@ -84,6 +59,26 @@ static StreamConfiguration* streamConfig; }); } +- (void)alreadyPaired { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + + HttpManager* hMan = [[HttpManager alloc] initWithHost:_selectedHost.hostName uniqueId:_uniqueId deviceName:deviceName cert:_cert]; + NSData* appListResp = [hMan executeRequestSynchronously:[hMan newAppListRequest]]; + NSArray* appList = [HttpManager getAppListFromXML:appListResp]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self updateApps:appList]; + }); + [AppManager retrieveAppAssets:appList withManager:hMan andCallback:self]; + }); +} + +- (void) receivedAssetForApp:(App*)app { + NSArray* subviews = [appScrollView subviews]; + for (UIAppView* appView in subviews) { + [appView updateAppImage]; + } +} + - (void)displayDnsFailedDialog { UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Network Error" message:@"Failed to resolve host." @@ -92,31 +87,55 @@ static StreamConfiguration* streamConfig; [self presentViewController:alert animated:YES completion:nil]; } -//TODO: No more stream button -/* -- (void)StreamButton:(UIButton *)sender -{ - NSLog(@"Stream Button Pressed!"); - - if ([self.hostTextField.text length] > 0) { - _selectedHost = [[Computer alloc] initWithIp:self.hostTextField.text]; - NSLog(@"Using custom host: %@", self.hostTextField.text); - } - - if (![self validatePcSelected]) { - NSLog(@"No valid PC selected"); - return; - } - +- (void) hostClicked:(Computer *)computer { + NSLog(@"Clicked host: %@", computer.displayName); + _selectedHost = computer; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + HttpManager* hMan = [[HttpManager alloc] initWithHost:computer.hostName uniqueId:_uniqueId deviceName:deviceName cert:_cert]; + NSData* serverInfoResp = [hMan executeRequestSynchronously:[hMan newServerInfoRequest]]; + if ([[HttpManager getStringFromXML:serverInfoResp tag:@"PairStatus"] isEqualToString:@"1"]) { + NSLog(@"Already Paired"); + [self alreadyPaired]; + } else { + NSLog(@"Trying to pair"); + PairManager* pMan = [[PairManager alloc] initWithManager:hMan andCert:_cert callback:self]; + [_opQueue addOperation:pMan]; + } + }); +} + +- (void) addHostClicked { + NSLog(@"Clicked add host"); + 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* host = ((UITextField*)[[alertController textFields] objectAtIndex:0]).text; + Computer* newHost = [[Computer alloc] initWithIp:host]; + [hostList addObject:newHost]; + [self updateHosts:[hostList allObjects]]; + + + //TODO: get pair state + + + }]]; + [alertController addTextFieldWithConfigurationHandler:nil]; + [self presentViewController:alertController animated:YES completion:nil]; +} + +- (void) appClicked:(App *)app { + NSLog(@"Clicked app: %@", app.appName); streamConfig = [[StreamConfiguration alloc] init]; streamConfig.host = _selectedHost.hostName; streamConfig.hostAddr = [Utils resolveHost:_selectedHost.hostName]; + streamConfig.appID = app.appId; if (streamConfig.hostAddr == 0) { [self displayDnsFailedDialog]; return; } - unsigned long selectedConf = [self.StreamConfigs selectedRowInComponent:0]; + // TODO: actually allow the user to choose the config + unsigned long selectedConf = 1; NSLog(@"selectedConf: %ld", selectedConf); switch (selectedConf) { case 0: @@ -148,19 +167,6 @@ static StreamConfiguration* streamConfig; NSLog(@"StreamConfig: %@, %d, %dx%dx%d at %d Mbps", streamConfig.host, streamConfig.hostAddr, streamConfig.width, streamConfig.height, streamConfig.frameRate, streamConfig.bitRate); [self performSegueWithIdentifier:@"createStreamFrame" sender:self]; } -*/ - -/* -- (void)setSelectedHost:(NSInteger)selectedIndex -{ - _selectedHost = (Computer*)([self.hostPickerVals objectAtIndex:selectedIndex]); - if (_selectedHost.hostName == NULL) { - // This must be the placeholder computer - _selectedHost = NULL; - } -} -*/ - - (void)viewDidLoad { @@ -169,68 +175,91 @@ static StreamConfiguration* streamConfig; NSArray* streamConfigVals = [[NSArray alloc] initWithObjects:@"1280x720 (30Hz)", @"1280x720 (60Hz)", @"1920x1080 (30Hz)", @"1920x1080 (60Hz)",nil]; _opQueue = [[NSOperationQueue alloc] init]; + [CryptoManager generateKeyPairUsingSSl]; + _uniqueId = [CryptoManager getUniqueID]; + _cert = [CryptoManager readCertFromFile]; // Initialize the host picker list - //[self updateHosts:[[NSArray alloc] init]]; + if (hostList == nil) { + hostList = [[NSMutableSet alloc] init]; + } - Computer* test = [[Computer alloc] initWithIp:@"CEMENT-TRUCK"]; - - - UIScrollView* hostScrollView = [[UIScrollView alloc] init]; + hostScrollView = [[UIScrollView alloc] init]; hostScrollView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height / 2); [hostScrollView setShowsHorizontalScrollIndicator:NO]; - UIComputerView* compView; - for (int i = 0; i < 5; i++) { - compView = [[UIComputerView alloc] initWithComputer:test]; - [hostScrollView addSubview:compView]; - [compView sizeToFit]; - compView.center = CGPointMake((compView.frame.size.width + 20) * i + compView.frame.size.width, hostScrollView.frame.size.height / 2); - - } - - [hostScrollView setContentSize:CGSizeMake(compView.frame.size.width * 5 + compView.frame.size.width, hostScrollView.frame.size.height)]; - - UIScrollView* appScrollView = [[UIScrollView alloc] init]; + appScrollView = [[UIScrollView alloc] init]; appScrollView.frame = CGRectMake(0, hostScrollView.frame.size.height, self.view.frame.size.width, self.view.frame.size.height / 2); [appScrollView setShowsHorizontalScrollIndicator:NO]; - - - App* testApp = [[App alloc] init]; - testApp.displayName = @"Left 4 Dead 2"; - UIAppView* appView; - for (int i = 0; i < 5; i++) { - appView = [[UIAppView alloc] initWithApp:testApp]; - [appScrollView addSubview:appView]; - [appView sizeToFit]; - appView.center = CGPointMake((appView.frame.size.width + 20) * i + compView.frame.size.width, appScrollView.frame.size.height / 2); - } - - [appScrollView setContentSize:CGSizeMake(appView.frame.size.width * 5 + appView.frame.size.width, appScrollView.frame.size.height)]; - + + [self updateHosts:[hostList allObjects]]; [self.view addSubview:hostScrollView]; [self.view addSubview:appScrollView]; - } - (void)viewDidAppear:(BOOL)animated { [super viewDidDisappear:animated]; - //_mDNSManager = [[MDNSManager alloc] initWithCallback:self]; - // [_mDNSManager searchForHosts]; + _mDNSManager = [[MDNSManager alloc] initWithCallback:self]; + [_mDNSManager searchForHosts]; } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; - // [_mDNSManager stopSearching]; + [_mDNSManager stopSearching]; } - (void)updateHosts:(NSArray *)hosts { - NSMutableArray *hostPickerValues = [[NSMutableArray alloc] initWithArray:hosts]; + [hostList addObjectsFromArray:hosts]; + [[hostScrollView subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)]; + UIComputerView* addComp = [[UIComputerView alloc] initForAddWithCallback:self]; + UIComputerView* compView; + float prevEdge = -1; + for (Computer* comp in hostList) { + 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]; + } - if ([hostPickerValues count] == 0) { - [hostPickerValues addObject:[[Computer alloc] initPlaceholder]]; + 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; + } +} + +- (void) updateApps:(NSArray*)apps { + [[appScrollView subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)]; + App* fakeApp = [[App alloc] init]; + fakeApp.appName = @"No App Name"; + UIAppView* noAppImage = [[UIAppView alloc] initWithApp:fakeApp andCallback:nil]; + float prevEdge = -1; + UIAppView* appView; + for (App* app in apps) { + appView = [[UIAppView alloc] initWithApp:app andCallback:self]; + prevEdge = [self getAppViewX:appView noApp:noAppImage prevEdge:prevEdge]; + appView.center = CGPointMake(prevEdge, appScrollView.frame.size.height / 2); + prevEdge = appView.frame.origin.x + appView.frame.size.width; + [appScrollView addSubview:appView]; + } + [appScrollView setContentSize:CGSizeMake(prevEdge + noAppImage.frame.size.width, appScrollView.frame.size.height)]; +} + +- (float) getAppViewX:(UIAppView*)app noApp:(UIAppView*)noAppImage prevEdge:(float)prevEdge { + if (prevEdge == -1) { + return appScrollView.frame.origin.x + app.frame.size.width / 2 + noAppImage.frame.size.width / 2; + } else { + return prevEdge + app.frame.size.width / 2 + noAppImage.frame.size.width / 2; } } diff --git a/Limelight/ViewControllers/StreamFrameViewController.m b/Limelight/ViewControllers/StreamFrameViewController.m index e05aec0..0331ee3 100644 --- a/Limelight/ViewControllers/StreamFrameViewController.m +++ b/Limelight/ViewControllers/StreamFrameViewController.m @@ -26,6 +26,8 @@ [super viewDidLoad]; [self.stageLabel setText:@"Starting App"]; + [self.stageLabel sizeToFit]; + self.stageLabel.center = CGPointMake(self.view.frame.size.width / 2, self.stageLabel.center.y); [UIApplication sharedApplication].idleTimerDisabled = YES; @@ -53,6 +55,8 @@ dispatch_async(dispatch_get_main_queue(), ^{ [self.spinner stopAnimating]; [self.stageLabel setText:@"Waiting for first frame..."]; + [self.stageLabel sizeToFit]; + self.stageLabel.center = CGPointMake(self.view.frame.size.width / 2, self.stageLabel.center.y); }); } @@ -74,6 +78,8 @@ NSString* lowerCase = [NSString stringWithFormat:@"%s in progress...", stageName]; NSString* titleCase = [[[lowerCase substringToIndex:1] uppercaseString] stringByAppendingString:[lowerCase substringFromIndex:1]]; [self.stageLabel setText:titleCase]; + [self.stageLabel sizeToFit]; + self.stageLabel.center = CGPointMake(self.view.frame.size.width / 2, self.stageLabel.center.y); }); } diff --git a/iPad.storyboard b/iPad.storyboard index eb77d96..37d9a06 100644 --- a/iPad.storyboard +++ b/iPad.storyboard @@ -1,5 +1,5 @@ - + @@ -8,21 +8,9 @@ - - - - - - - @@ -38,31 +26,23 @@ - - - - - + + - - - - - - - diff --git a/iPhone.storyboard b/iPhone.storyboard index 098b0cc..efe9f1d 100644 --- a/iPhone.storyboard +++ b/iPhone.storyboard @@ -1,5 +1,5 @@ - + @@ -8,10 +8,6 @@ - - - - @@ -32,31 +28,23 @@ - - - - - + + - - - - - - -