[RENAMED] MPAppDelegate -> MPiOSAppDelegate for iOS to avoid class name confusion in workspace.
915 lines
35 KiB
Objective-C
915 lines
35 KiB
Objective-C
//
|
||
// 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
|