// // MPMainViewController.m // MasterPassword // // Created by Maarten Billemont on 24/11/11. // Copyright (c) 2011 Lyndir. All rights reserved. // #import "MPMainViewController.h" #import "MPiOSAppDelegate.h" #import "MPAppDelegate_Store.h" #import "MPElementListAllViewController.h" @interface MPMainViewController() @property(nonatomic) BOOL suppressOutdatedAlert; @end @implementation MPMainViewController { NSManagedObjectID *_activeElementOID; } #pragma mark - View lifecycle - (BOOL)shouldAutorotate { return YES; } - (NSUInteger)supportedInterfaceOrientations { return UIInterfaceOrientationMaskAllButUpsideDown; } - (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation { return UIInterfaceOrientationPortrait; } - (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { [self updateHelpHiddenAnimated:NO]; [self updateUserHiddenAnimated:NO]; } - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([[segue identifier] isEqualToString:@"MP_ChooseType"]) ((MPTypeViewController *)[segue destinationViewController]).delegate = self; if ([[segue identifier] isEqualToString:@"MP_AllSites"]) { ((MPElementListAllViewController *)[segue destinationViewController]).delegate = self; ((MPElementListAllViewController *)[segue destinationViewController]).filter = sender; } } - (void)viewDidLoad { self.searchDelegate = [MPElementListSearchController new]; self.searchDelegate.delegate = self; self.searchDelegate.searchDisplayController = self.searchDisplayController; self.searchDelegate.searchTipContainer = self.searchTipContainer; self.searchDisplayController.searchBar.delegate = self.searchDelegate; self.searchDisplayController.delegate = self.searchDelegate; self.searchDisplayController.searchResultsDelegate = self.searchDelegate; self.searchDisplayController.searchResultsDataSource = self.searchDelegate; [self.passwordIncrementer addGestureRecognizer:[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(resetPasswordCounter:)]]; [self.loginNameContainer addGestureRecognizer:[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(editLoginName:)]]; [self.loginNameContainer addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(copyLoginName:)]]; [self.outdatedAlertBack addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(infoOutdatedAlert)]]; self.view.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"ui_background"]]; self.alertBody.text = nil; self.toolTipEditIcon.hidden = YES; [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidEnterBackgroundNotification object:self queue:nil usingBlock:^(NSNotification *note) { self.suppressOutdatedAlert = NO; }]; [[NSNotificationCenter defaultCenter] addObserverForName:MPElementUpdatedNotification object:nil queue:nil usingBlock: ^void(NSNotification *note) { MPElementEntity *activeElement = [self activeElementForThread]; if (activeElement.type & MPElementTypeClassStored && ![[activeElement.content description] length]) [self showToolTip:@"Tap to set a password." withIcon:self.toolTipEditIcon]; if (activeElement.requiresExplicitMigration) [self showToolTip:@"Password outdated. Tap to upgrade it." withIcon:nil]; }]; [[NSNotificationCenter defaultCenter] addObserverForName:MPSignedOutNotification object:nil queue:nil usingBlock: ^(NSNotification *note) { BOOL animated = [[note.userInfo objectForKey:@"animated"] boolValue]; _activeElementOID = nil; self.suppressOutdatedAlert = NO; [self updateAnimated:NO]; [[PearlSheet activeSheets] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { [obj cancelSheetAnimated:NO]; }]; if (![self.navigationController presentedViewController]) [self.navigationController popToRootViewControllerAnimated:animated]; else [self.navigationController dismissViewControllerAnimated:animated completion:^{ [self.navigationController popToRootViewControllerAnimated:animated]; }]; }]; [super viewDidLoad]; } - (void)viewWillAppear:(BOOL)animated { MPElementEntity *activeElement = [self activeElementForThread]; if (activeElement.user != [[MPiOSAppDelegate get] activeUserForThread]) _activeElementOID = nil; self.searchDisplayController.searchBar.text = nil; self.alertContainer.hidden = NO; self.outdatedAlertContainer.hidden = NO; self.searchTipContainer.hidden = NO; self.actionsTipContainer.hidden = NO; self.typeTipContainer.hidden = NO; self.toolTipContainer.hidden = NO; self.contentTipContainer.hidden = NO; self.loginNameTipContainer.hidden = NO; [self updateAnimated:NO]; [super viewWillAppear:animated]; } - (void)viewDidAppear:(BOOL)animated { inf(@"Main will appear"); // Sometimes, the search bar gets stuck in some sort of first-responder mode that it can't get out of... [[self.view.window findFirstResponderInHierarchy] resignFirstResponder]; // Needed for when we appear after a modal VC dismisses: // We can't present until the other modal VC has been fully dismissed and presenting in -viewWillAppear: will fail. dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0 ), ^{ MPUserEntity *activeUser = [[MPiOSAppDelegate get] activeUserForThread]; if ([MPAlgorithmDefault migrateUser:activeUser] && !self.suppressOutdatedAlert) [UIView animateWithDuration:0.3f animations:^{ self.outdatedAlertContainer.alpha = 1; self.suppressOutdatedAlert = YES; }]; [activeUser.managedObjectContext saveToStore]; } ); if (![[MPiOSConfig get].actionsTipShown boolValue]) [UIView animateWithDuration:animated? 0.3f: 0 animations:^{ self.actionsTipContainer.alpha = 1; } completion:^(BOOL finished) { if (!finished) return; [MPiOSConfig get].actionsTipShown = @YES; dispatch_after( dispatch_time( DISPATCH_TIME_NOW, (int64_t)(5.0f * NSEC_PER_SEC) ), dispatch_get_main_queue(), ^{ [UIView animateWithDuration:0.2f animations:^{ self.actionsTipContainer.alpha = 0; } completion:^(BOOL finished_) { if (!_activeElementOID) [UIView animateWithDuration:animated? 0.3f: 0 animations:^{ self.searchTipContainer.alpha = 1; }]; }]; } ); }]; [[LocalyticsSession sharedLocalyticsSession] tagScreen:@"Main"]; [super viewDidAppear:animated]; } - (void)viewWillDisappear:(BOOL)animated { inf(@"Main will disappear."); [super viewWillDisappear:animated]; } - (void)updateAnimated:(BOOL)animated { if (animated) { [UIView animateWithDuration:0.3f animations:^{ [self updateAnimated:NO]; }]; return; } MPElementEntity *activeElement = [self activeElementForThread]; [self setHelpChapter:activeElement? @"2": @"1"]; [self updateHelpHiddenAnimated:NO]; self.passwordCounter.alpha = 0; self.passwordIncrementer.alpha = 0; self.passwordEdit.alpha = 0; self.passwordUpgrade.alpha = 0; self.passwordUser.alpha = 0; self.displayContainer.alpha = 0; if (activeElement) { self.passwordUser.alpha = 0.5f; self.displayContainer.alpha = 1.0f; } if (activeElement.requiresExplicitMigration) self.passwordUpgrade.alpha = 0.5f; else { if (activeElement.type & MPElementTypeClassGenerated) { self.passwordCounter.alpha = 0.5f; self.passwordIncrementer.alpha = 0.5f; } else if (activeElement.type & MPElementTypeClassStored) self.passwordEdit.alpha = 0.5f; } self.siteName.text = activeElement.name; self.typeButton.alpha = activeElement? 1: 0; [self.typeButton setTitle:activeElement.typeName forState:UIControlStateNormal]; if ([activeElement isKindOfClass:[MPElementGeneratedEntity class]]) self.passwordCounter.text = PearlString( @"%u", ((MPElementGeneratedEntity *)activeElement).counter ); self.contentField.enabled = NO; self.contentField.text = @""; if (activeElement.name && ![activeElement isDeleted]) dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0 ), ^{ NSString *description = [activeElement.content description]; dispatch_async( dispatch_get_main_queue(), ^{ self.contentField.text = description; } ); } ); self.loginNameField.enabled = NO; self.loginNameField.text = activeElement.loginName; self.siteInfoHidden = !activeElement || ([[MPiOSConfig get].siteInfoHidden boolValue] && (activeElement.loginName == nil)); [self updateUserHiddenAnimated:NO]; } - (void)toggleHelpAnimated:(BOOL)animated { [self setHelpHidden:![[MPiOSConfig get].helpHidden boolValue] animated:animated]; } - (void)setHelpHidden:(BOOL)hidden animated:(BOOL)animated { [MPiOSConfig get].helpHidden = @(hidden); [self updateHelpHiddenAnimated:animated]; } - (void)updateHelpHiddenAnimated:(BOOL)animated { if (animated) { [UIView animateWithDuration:0.3f animations:^{ [self updateHelpHiddenAnimated:NO]; }]; return; } self.pullUpView.hidden = ![[MPiOSConfig get].helpHidden boolValue]; self.pullDownView.hidden = [[MPiOSConfig get].helpHidden boolValue]; if ([[MPiOSConfig get].helpHidden boolValue]) { self.contentContainer.frame = CGRectSetHeight( self.contentContainer.frame, self.view.bounds.size.height - 44 /* search bar */); self.helpContainer.frame = CGRectSetY( self.helpContainer.frame, self.view.bounds.size.height - 20 /* pull-up */); } else { self.contentContainer.frame = CGRectSetHeight( self.contentContainer.frame, 225 ); [self.helpContainer setFrameFromCurrentSizeAndParentPaddingTop:CGFLOAT_MAX right:0 bottom:0 left:0]; } } - (IBAction)toggleUser { [self toggleUserAnimated:YES]; } - (void)toggleUserAnimated:(BOOL)animated { [MPiOSConfig get].siteInfoHidden = PearlBool(!self.siteInfoHidden); self.siteInfoHidden = [[MPiOSConfig get].siteInfoHidden boolValue]; [self updateUserHiddenAnimated:animated]; } - (void)updateUserHiddenAnimated:(BOOL)animated { if (animated) { [UIView animateWithDuration:0.3f animations:^{ [self updateUserHiddenAnimated:NO]; }]; return; } if (self.siteInfoHidden) { self.displayContainer.frame = CGRectSetHeight( self.displayContainer.frame, 87 ); } else { self.displayContainer.frame = CGRectSetHeight( self.displayContainer.frame, 137 ); } } - (void)setHelpChapter:(NSString *)chapter { MPCheckpoint( MPCheckpointHelpChapter, @{ @"chapter" : chapter } ); dispatch_async( dispatch_get_main_queue(), ^{ NSURL *url = [NSURL URLWithString:[@"#" stringByAppendingString:chapter] relativeToURL:[[NSBundle mainBundle] URLForResource:@"help" withExtension:@"html"]]; [self.helpView loadRequest:[NSURLRequest requestWithURL:url]]; } ); } - (IBAction)panHelpDown:(UIPanGestureRecognizer *)sender { CGFloat targetY = MIN(self.view.bounds.size.height - 20, 246 + [sender translationInView:self.helpContainer].y); BOOL hideHelp = YES; if (targetY <= 246) { hideHelp = NO; targetY = 246; } self.helpContainer.frame = CGRectSetY( self.helpContainer.frame, targetY ); if (sender.state == UIGestureRecognizerStateEnded) [self setHelpHidden:hideHelp animated:YES]; } - (IBAction)panHelpUp:(UIPanGestureRecognizer *)sender { CGFloat targetY = MAX(246, self.view.bounds.size.height - 20 + [sender translationInView:self.helpContainer].y); BOOL hideHelp = NO; if (targetY >= self.view.bounds.size.height - 20) { hideHelp = YES; targetY = self.view.bounds.size.height - 20; } self.helpContainer.frame = CGRectSetY( self.helpContainer.frame, targetY ); if (sender.state == UIGestureRecognizerStateEnded) [self setHelpHidden:hideHelp animated:YES]; } - (void)webViewDidFinishLoad:(UIWebView *)webView { MPElementEntity *activeElement = [self activeElementForThread]; NSString *error = [self.helpView stringByEvaluatingJavaScriptFromString: PearlString( @"setClass('%@');", activeElement.typeClassName )]; if (error.length) err(@"helpView.setClass: %@", error); } - (void)showContentTip:(NSString *)message withIcon:(UIImageView *)icon { dispatch_async( dispatch_get_main_queue(), ^{ if (self.contentTipCleanup) self.contentTipCleanup( NO ); __weak MPMainViewController *wSelf = self; self.contentTipBody.text = message; self.contentTipCleanup = ^(BOOL finished) { icon.hidden = YES; wSelf.contentTipCleanup = nil; }; icon.hidden = NO; [UIView animateWithDuration:0.3f animations:^{ self.contentTipContainer.alpha = 1; } completion:^(BOOL finished) { if (finished) { dispatch_time_t popTime = dispatch_time( DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC ); dispatch_after( popTime, dispatch_get_main_queue(), ^(void) { [UIView animateWithDuration:0.2f animations:^{ self.contentTipContainer.alpha = 0; } completion:self.contentTipCleanup]; } ); } }]; } ); } - (void)showLoginNameTip:(NSString *)message { dispatch_async( dispatch_get_main_queue(), ^{ self.loginNameTipBody.text = message; [UIView animateWithDuration:0.3f animations:^{ self.loginNameTipContainer.alpha = 1; } completion:^(BOOL finished) { if (finished) { dispatch_time_t popTime = dispatch_time( DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC ); dispatch_after( popTime, dispatch_get_main_queue(), ^(void) { [UIView animateWithDuration:0.2f animations:^{ self.loginNameTipContainer.alpha = 0; }]; } ); } }]; } ); } - (void)showToolTip:(NSString *)message withIcon:(UIImageView *)icon { dispatch_async( dispatch_get_main_queue(), ^{ if (self.toolTipCleanup) self.toolTipCleanup( NO ); __weak MPMainViewController *wSelf = self; self.toolTipBody.text = message; self.toolTipCleanup = ^(BOOL finished) { icon.hidden = YES; wSelf.toolTipCleanup = nil; }; icon.hidden = NO; [UIView animateWithDuration:0.3f animations:^{ self.toolTipContainer.alpha = 1; } completion:^(BOOL finished) { if (finished) { dispatch_time_t popTime = dispatch_time( DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC ); dispatch_after( popTime, dispatch_get_main_queue(), ^(void) { [UIView animateWithDuration:0.2f animations:^{ self.toolTipContainer.alpha = 0; } completion:self.toolTipCleanup]; } ); } }]; } ); } - (void)showAlertWithTitle:(NSString *)title message:(NSString *)message { dispatch_async( dispatch_get_main_queue(), ^{ self.alertTitle.text = title; NSRange scrollRange = NSMakeRange( self.alertBody.text.length, message.length ); if ([self.alertBody.text length]) self.alertBody.text = [NSString stringWithFormat:@"%@\n\n---\n\n%@", self.alertBody.text, message]; else self.alertBody.text = message; [self.alertBody scrollRangeToVisible:scrollRange]; [UIView animateWithDuration:0.3f animations:^{ self.alertContainer.alpha = 1; }]; } ); } #pragma mark - Protocols - (IBAction)copyContent { MPElementEntity *activeElement = [self activeElementForThread]; id content = activeElement.content; if (!content) // Nothing to copy. return; inf(@"Copying password for: %@", activeElement.name); [UIPasteboard generalPasteboard].string = [content description]; [self showContentTip:@"Copied!" withIcon:nil]; MPCheckpoint( MPCheckpointCopyToPasteboard, @{ @"type" : activeElement.typeName, @"version" : @(activeElement.version), @"emergency" : @NO } ); } - (IBAction)copyLoginName:(UITapGestureRecognizer *)sender { MPElementEntity *activeElement = [self activeElementForThread]; if (!activeElement.loginName) return; inf(@"Copying user name for: %@", activeElement.name); [UIPasteboard generalPasteboard].string = activeElement.loginName; [self showLoginNameTip:@"Copied!"]; MPCheckpoint( MPCheckpointCopyLoginNameToPasteboard, @{ @"type" : activeElement.typeName, @"version" : @(activeElement.version) } ); } - (IBAction)incrementPasswordCounter { [self changeActiveElementWithWarning: @"You are incrementing the site's password counter.\n\n" @"If you continue, a new password will be generated for this site. " @"You will then need to update your account's old password to this newly generated password.\n\n" @"You can reset the counter by holding down on this button." do:^BOOL(MPElementEntity *activeElement) { if (![activeElement isKindOfClass:[MPElementGeneratedEntity class]]) { // Not of a type that supports a password counter. err(@"Cannot increment password counter: Element is not generated: %@", activeElement.name); return NO; } MPElementGeneratedEntity *activeGeneratedElement = (MPElementGeneratedEntity *)activeElement; inf(@"Incrementing password counter for: %@", activeGeneratedElement.name); ++activeGeneratedElement.counter; MPCheckpoint( MPCheckpointIncrementPasswordCounter, @{ @"type" : activeGeneratedElement.typeName, @"version" : @(activeGeneratedElement.version), @"counter" : @(activeGeneratedElement.counter) } ); return YES; }]; } - (IBAction)resetPasswordCounter:(UILongPressGestureRecognizer *)sender { if (sender.state != UIGestureRecognizerStateBegan) // Only fire when the gesture was first detected. return; MPElementEntity *activeElement = [self activeElementForThread]; if (![activeElement isKindOfClass:[MPElementGeneratedEntity class]]) { // Not of a type that supports a password counter. err(@"Cannot reset password counter: Element is not generated: %@", activeElement.name); return; } else if (((MPElementGeneratedEntity *)activeElement).counter == 1) // Counter has initial value, no point resetting. return; [self changeActiveElementWithWarning: @"You are resetting the site's password counter.\n\n" @"If you continue, the site's password will change back to its original value. " @"You will then need to update your account's password back to this original value." do:^BOOL(MPElementEntity *activeElement_) { inf(@"Resetting password counter for: %@", activeElement_.name); ((MPElementGeneratedEntity *)activeElement_).counter = 1; MPCheckpoint( MPCheckpointResetPasswordCounter, @{ @"type" : activeElement_.typeName, @"version" : @(activeElement_.version) } ); return YES; }]; } - (IBAction)editLoginName:(UILongPressGestureRecognizer *)sender { if (sender.state != UIGestureRecognizerStateBegan) // Only fire when the gesture was first detected. return; MPElementEntity *activeElement = [self activeElementForThread]; if (!activeElement) return; self.loginNameField.enabled = YES; [self.loginNameField becomeFirstResponder]; MPCheckpoint( MPCheckpointEditLoginName, @{ @"type" : activeElement.typeName, @"version" : @(activeElement.version) } ); } - (void)changeActiveElementWithWarning:(NSString *)warning do:(BOOL (^)(MPElementEntity *activeElement))task; { [PearlAlert showAlertWithTitle:@"Password Change" message:warning viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) { if (buttonIndex == [alert cancelButtonIndex]) return; [self changeActiveElementWithoutWarningDo:task]; } cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:[PearlStrings get].commonButtonContinue, nil]; } - (void)changeActiveElementWithoutWarningDo:(BOOL (^)(MPElementEntity *activeElement))task; { MPElementEntity *activeElement = [self activeElementForThread]; NSString *oldPassword = [activeElement.content description]; if (!task( activeElement )) return; NSString *newPassword = [activeElement.content description]; // Save. [activeElement.managedObjectContext saveToStore]; // Update the UI. dispatch_async( dispatch_get_main_queue(), ^{ [self updateAnimated:YES]; // Show new and old password. if ([oldPassword length] && ![oldPassword isEqualToString:newPassword]) [self showAlertWithTitle:@"Password Changed!" message:PearlString( @"The password for %@ has changed.\n\n" @"IMPORTANT:\n" @"Don't forget to update the site with your new password! " @"Your old password was:\n" @"%@", activeElement.name, oldPassword )]; } ); } - (MPElementEntity *)activeElementForThread { if (!_activeElementOID) return nil; NSManagedObjectContext *moc = [MPiOSAppDelegate managedObjectContextForThreadIfReady]; if (!moc) return nil; NSError *error; MPElementEntity *activeElement = (MPElementEntity *)[moc existingObjectWithID:_activeElementOID error:&error]; if (!activeElement) err(@"Couldn't retrieve active element: %@", error); return activeElement; } - (IBAction)editPassword { MPElementEntity *activeElement = [self activeElementForThread]; if (!(activeElement.type & MPElementTypeClassStored)) { // Not of a type that supports editing the content. err(@"Cannot edit content: Element is not stored: %@", activeElement.name); return; } self.contentField.enabled = YES; [self.contentField becomeFirstResponder]; MPCheckpoint( MPCheckpointEditPassword, @{ @"type" : activeElement.typeName, @"version" : @(activeElement.version) } ); } - (IBAction)upgradePassword { MPElementEntity *activeElement = [self activeElementForThread]; if (!activeElement) return; NSString *warning = activeElement.type & MPElementTypeClassGenerated? @"You are upgrading the site.\n\n" @"This upgrade improves the site's compatibility with the latest version of Master Password.\n\n" @"Your password will change and you will need to update your site's account." : @"You are upgrading the site.\n\n" @"This upgrade improves the site's compatibility with the latest version of Master Password."; [self changeActiveElementWithWarning:warning do: ^BOOL(MPElementEntity *activeElement_) { inf(@"Explicitly migrating element: %@", activeElement_); [activeElement_ migrateExplicitly:YES]; MPCheckpoint( MPCheckpointExplicitMigration, @{ @"type" : activeElement_.typeName, @"version" : @(activeElement_.version) } ); return YES; }]; } - (IBAction)searchOutdatedElements { [self performSegueWithIdentifier:@"MP_AllSites" sender:MPElementListFilterOutdated]; } - (IBAction)closeAlert { [UIView animateWithDuration:0.3f animations:^{ self.alertContainer.alpha = 0; } completion:^(BOOL finished) { if (finished) self.alertBody.text = nil; }]; } - (IBAction)closeOutdatedAlert { [UIView animateWithDuration:0.3f animations:^{ self.outdatedAlertContainer.alpha = 0; }]; } - (IBAction)infoOutdatedAlert { [self setHelpChapter:@"outdated"]; [self setHelpHidden:NO animated:YES]; [self closeOutdatedAlert]; self.suppressOutdatedAlert = NO; } - (IBAction)action:(id)sender { [PearlSheet showSheetWithTitle:nil viewStyle:UIActionSheetStyleAutomatic initSheet:nil tappedButtonBlock:^(UIActionSheet *sheet, NSInteger buttonIndex) { if (buttonIndex == [sheet cancelButtonIndex]) return; switch (buttonIndex - [sheet firstOtherButtonIndex]) { case 0: { inf(@"Action: FAQ"); [self setHelpChapter:@"faq"]; [self setHelpHidden:NO animated:YES]; break; } case 1: { inf(@"Action: Guide"); [[MPiOSAppDelegate get] showGuide]; break; } case 2: { inf(@"Action: Preferences"); [self performSegueWithIdentifier:@"MP_UserProfile" sender:self]; break; } case 3: { inf(@"Action: Other Apps"); [self performSegueWithIdentifier:@"MP_OtherApps" sender:self]; break; } //#if defined(ADHOC) && defined(TESTFLIGHT_SDK_VERSION) // case 4: { // inf(@"Action: Feedback via TestFlight"); // [TestFlight openFeedbackView]; // break; // } //#else case 4: { inf(@"Action: Feedback via Mail"); [[MPiOSAppDelegate get] showFeedbackWithLogs:YES forVC:self]; break; } //#endif default: { wrn(@"Unsupported action: %u", buttonIndex - [sheet firstOtherButtonIndex]); break; } } } cancelTitle:[PearlStrings get].commonButtonCancel destructiveTitle:nil otherTitles: @"? FAQ ", @"ⓘ Quick Guide ", @"⚙ Preferences ", @"⚐ Other Apps ", @"✎ Feedback ", nil]; } - (MPElementType)selectedType { return [self selectedElement].type; } - (MPElementEntity *)selectedElement { return [self activeElementForThread]; } - (void)didSelectType:(MPElementType)type { [self changeActiveElementWithWarning: @"You are about to change the type of this password.\n\n" @"If you continue, the password for this site will change. " @"You will need to update your account's old password to the new one." do:^BOOL(MPElementEntity *activeElement) { if ([activeElement.algorithm classOfType:type] != activeElement.typeClass) { // Type requires a different class of element. Recreate the element. MPElementEntity *newElement = [NSEntityDescription insertNewObjectForEntityForName:[activeElement.algorithm classNameOfType:type] inManagedObjectContext:activeElement.managedObjectContext]; newElement.name = activeElement.name; newElement.user = activeElement.user; newElement.uses = activeElement.uses; newElement.lastUsed = activeElement.lastUsed; newElement.version = activeElement.version; newElement.loginName = activeElement.loginName; [activeElement.managedObjectContext deleteObject:activeElement]; _activeElementOID = newElement.objectID; activeElement = newElement; } activeElement.type = type; [[NSNotificationCenter defaultCenter] postNotificationName:MPElementUpdatedNotification object:activeElement.objectID]; return YES; }]; } - (void)didSelectElement:(MPElementEntity *)element { if (!element) return; _activeElementOID = element.objectID; [self closeAlert]; [self changeActiveElementWithoutWarningDo:^BOOL(MPElementEntity *activeElement) { if ([activeElement use] == 1) [self showAlertWithTitle:@"New Site" message: PearlString( @"You've just created a password for %@.\n\n" @"IMPORTANT:\n" @"Go to %@ and set or change the password for your account to the password above.\n" @"Do this right away: if you forget, you may have trouble remembering which password to use to log into the site later on.", activeElement.name, activeElement.name )]; return YES; }]; MPElementEntity *activeElement = [self activeElementForThread]; inf(@"Selected: %@", activeElement.name); if (![[MPiOSConfig get].typeTipShown boolValue]) [UIView animateWithDuration:0.5f animations:^{ self.typeTipContainer.alpha = 1; } completion:^(BOOL finished) { if (finished) { [MPiOSConfig get].typeTipShown = PearlBool(YES); dispatch_after( dispatch_time( DISPATCH_TIME_NOW, (int64_t)(5.0f * NSEC_PER_SEC) ), dispatch_get_main_queue(), ^{ [UIView animateWithDuration:0.2f animations:^{ self.typeTipContainer.alpha = 0; }]; } ); } }]; [self.searchDisplayController setActive:NO animated:YES]; self.searchDisplayController.searchBar.text = activeElement.name; MPCheckpoint( MPCheckpointUseType, @{ @"type" : activeElement.typeName, @"version" : @(activeElement.version) } ); [self updateAnimated:YES]; } - (BOOL)textFieldShouldReturn:(UITextField *)textField { if (textField == self.contentField) [self.contentField resignFirstResponder]; if (textField == self.loginNameField) [self.loginNameField resignFirstResponder]; return YES; } - (void)textFieldDidEndEditing:(UITextField *)textField { if (textField == self.contentField) { self.contentField.enabled = NO; MPElementEntity *activeElement = [self activeElementForThread]; if (![activeElement isKindOfClass:[MPElementStoredEntity class]]) { // Not of a type whose content can be edited. err(@"Cannot update element content: Element is not stored: %@", activeElement.name); return; } else if ([((MPElementStoredEntity *)activeElement).content isEqual:self.contentField.text]) // Content hasn't changed. return; [self changeActiveElementWithoutWarningDo:^BOOL(MPElementEntity *activeElement_) { ((MPElementStoredEntity *)activeElement_).content = self.contentField.text; return YES; }]; } if (textField == self.loginNameField) { self.loginNameField.enabled = NO; if (![[MPiOSConfig get].loginNameTipShown boolValue]) { [self showLoginNameTip:@"Tap to copy or hold to edit."]; [MPiOSConfig get].loginNameTipShown = @YES; } [self changeActiveElementWithoutWarningDo:^BOOL(MPElementEntity *activeElement) { if ([self.loginNameField.text length]) activeElement.loginName = self.loginNameField.text; else activeElement.loginName = nil; return YES; }]; } } - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { if (navigationType == UIWebViewNavigationTypeLinkClicked) { if ([[[request URL] query] isEqualToString:@"outdated"]) { [self searchOutdatedElements]; return NO; } [[UIApplication sharedApplication] openURL:[request URL]]; return NO; } return YES; } @end