diff --git a/External/Pearl b/External/Pearl index 99c73fd3..0d306934 160000 --- a/External/Pearl +++ b/External/Pearl @@ -1 +1 @@ -Subproject commit 99c73fd3b37b4d2621548f5ae366c163231a346d +Subproject commit 0d30693440fd520715f22789d33d5551b3b681c4 diff --git a/MasterPassword/ObjC/iOS/MPElementListAllViewController.h b/MasterPassword/ObjC/iOS/MPElementListAllViewController.h index 984cf97b..ec1211cf 100644 --- a/MasterPassword/ObjC/iOS/MPElementListAllViewController.h +++ b/MasterPassword/ObjC/iOS/MPElementListAllViewController.h @@ -20,6 +20,7 @@ #import "MPElementListController.h" @interface MPElementListAllViewController : MPElementListController +@property (weak, nonatomic) IBOutlet UINavigationBar *navigationBar; - (IBAction)close:(id)sender; - (IBAction)add:(id)sender; diff --git a/MasterPassword/ObjC/iOS/MPElementListAllViewController.m b/MasterPassword/ObjC/iOS/MPElementListAllViewController.m index 2a06b214..ea313a25 100644 --- a/MasterPassword/ObjC/iOS/MPElementListAllViewController.m +++ b/MasterPassword/ObjC/iOS/MPElementListAllViewController.m @@ -16,9 +16,32 @@ // #import "MPElementListAllViewController.h" +#import "MPAppDelegate.h" +#import "MPAppDelegate_Store.h" + +#define MPElementUpgradeOldContentKey @"MPElementUpgradeOldContentKey" +#define MPElementUpgradeNewContentKey @"MPElementUpgradeNewContentKey" @implementation MPElementListAllViewController +- (void)viewWillAppear:(BOOL)animated { + + [super viewWillAppear:animated]; + + if ([self.filter isEqualToString:MPElementListFilterNone]) { + self.navigationBar.topItem.title = @"All Sites"; + self.navigationBar.topItem.rightBarButtonItem = [[UIBarButtonItem alloc] + initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(add:)]; + } + else if ([self.filter isEqualToString:MPElementListFilterOutdated]) { + self.navigationBar.topItem.title = @"Outdated"; + self.navigationBar.topItem.rightBarButtonItem = [[UIBarButtonItem alloc] + initWithTitle:@"Upgrade All" style:UIBarButtonItemStyleBordered target:self action:@selector(upgradeAll:)]; + } + + [self updateData]; +} + - (IBAction)close:(id)sender { [self dismissViewControllerAnimated:YES completion:nil]; @@ -40,11 +63,106 @@ cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:[PearlStrings get].commonButtonOkay, nil]; } -- (void)viewWillAppear:(BOOL)animated { +- (IBAction)upgradeAll:(id)sender { - [super viewWillAppear:animated]; + [PearlAlert showAlertWithTitle:@"Upgrading All Sites" + message:@"You are about to upgrade all outdated sites. This will cause passwords to change. " + @"Afterwards, you can get an overview of the changes." + viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock: + ^(UIAlertView *alert, NSInteger buttonIndex) { + if (buttonIndex == [alert cancelButtonIndex]) + return; - [self updateData]; + PearlAlert *activity = [PearlAlert showActivityWithTitle:@"Upgrading Sites"]; + [self performUpgradeAllWithCompletion:^(BOOL success, NSDictionary *changes) { + dispatch_async( dispatch_get_main_queue(), ^{ + [self showUpgradeChanges:changes]; + [activity cancelAlert]; + } ); + }]; + } cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:[PearlStrings get].commonButtonContinue, nil]; +} + +- (void)performUpgradeAllWithCompletion:(void(^)(BOOL success, NSDictionary *changes))completion { + + [MPAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *moc) { + NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPElementEntity class] )]; + fetchRequest.fetchBatchSize = 20; + + NSError *error = nil; + NSArray *elements = [moc executeFetchRequest:fetchRequest error:&error]; + if (!elements) { + err(@"Failed to fetch outdated sites for upgrade: %@", error); + completion(NO, nil); + return; + } + + NSMutableDictionary *elementChanges = [NSMutableDictionary dictionaryWithCapacity:[elements count]]; + for (MPElementEntity *element in elements) { + id oldContent = [element content]; + [element migrateExplicitly:YES]; + id newContent = [element content]; + + if (!(element.type & MPElementFeatureDevicePrivate) && (!oldContent || ![oldContent isEqual:newContent])) + [elementChanges setObject:@{ + MPElementUpgradeOldContentKey : oldContent, + MPElementUpgradeNewContentKey : newContent, + } forKey:element.name]; + } + + completion(YES, elementChanges); + }]; +} + +- (void)showUpgradeChanges:(NSDictionary *)changes { + + if (![changes count]) + return; + + NSMutableString *formattedChanges = [NSMutableString new]; + for (NSString *changedElementName in changes) { + NSDictionary *elementChanges = [changes objectForKey:changedElementName]; + id oldContent = [elementChanges objectForKey:MPElementUpgradeOldContentKey]; + id newContent = [elementChanges objectForKey:MPElementUpgradeNewContentKey]; + [formattedChanges appendFormat:@"%@: %@ -> %@\n", changedElementName, oldContent, newContent]; + } + + [PearlAlert showAlertWithTitle:@"Sites Upgraded" + message:PearlString( @"This upgrade has caused %d passwords to change.\n" + @"To make updating the actual passwords of these accounts easier, " + @"you can email a summary of these changes to yourself.", [changes count]) + viewStyle:UIAlertViewStyleDefault initAlert:nil + tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) { + if (buttonIndex == [alert cancelButtonIndex]) + return; + + [PearlEMail sendEMailTo:nil subject:@"[Master Password] Upgrade Changes" body:formattedChanges]; + } cancelTitle:@"Don't Email" otherTitles:@"Send Email", nil]; +} + +- (NSFetchedResultsController *)fetchedResultsControllerByUses { + + return nil; +} + +- (void)configureFetchRequest:(NSFetchRequest *)fetchRequest { + + fetchRequest.fetchBatchSize = 10; +} + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + + return 1; +} + +- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView { + + return nil; +} + +- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { + + return nil; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { diff --git a/MasterPassword/ObjC/iOS/MPElementListController.h b/MasterPassword/ObjC/iOS/MPElementListController.h index 53c1a0be..41f172fb 100644 --- a/MasterPassword/ObjC/iOS/MPElementListController.h +++ b/MasterPassword/ObjC/iOS/MPElementListController.h @@ -1,28 +1,22 @@ -// -// Created by lhunath on 2013-02-09. -// -// To change the template use AppCode | Preferences | File Templates. -// - - #import #import "MPElementListDelegate.h" -typedef enum { - MPSearchScopeAll, - MPSearchScopeOutdated, -} MPSearchScope; +#define MPElementListFilterNone @"MPElementListFilterNone" +#define MPElementListFilterOutdated @"MPElementListFilterOutdated" @interface MPElementListController : UITableViewController @property (weak, nonatomic) IBOutlet id delegate; -@property (readonly) NSFetchedResultsController *fetchedResultsController; +@property (strong, nonatomic) NSString *filter; + +@property (readonly) NSFetchedResultsController *fetchedResultsControllerByUses; +@property (readonly) NSFetchedResultsController *fetchedResultsControllerByLastUsed; @property (readonly) NSDateFormatter *dateFormatter; - (void)updateData; - (void)addElementNamed:(NSString *)siteName completion:(void (^)(BOOL success))completion; -- (void)configureCell:(UITableViewCell *)cell inTableView:(UITableView *)tableView atIndexPath:(NSIndexPath *)indexPath; +- (void)configureCell:(UITableViewCell *)cell inTableView:(UITableView *)tableView atTableIndexPath:(NSIndexPath *)indexPath; - (void)customTableViewUpdates; @end diff --git a/MasterPassword/ObjC/iOS/MPElementListController.m b/MasterPassword/ObjC/iOS/MPElementListController.m index c34c8648..f4111c84 100644 --- a/MasterPassword/ObjC/iOS/MPElementListController.m +++ b/MasterPassword/ObjC/iOS/MPElementListController.m @@ -1,10 +1,3 @@ -// -// Created by lhunath on 2013-02-09. -// -// To change the template use AppCode | Preferences | File Templates. -// - - #import "MPElementListController.h" #import "MPAppDelegate_Store.h" @@ -15,7 +8,8 @@ @implementation MPElementListController { - NSFetchedResultsController *_fetchedResultsController; + NSFetchedResultsController *_fetchedResultsControllerByUses; + NSFetchedResultsController *_fetchedResultsControllerByLastUsed; NSDateFormatter *_dateFormatter; } @@ -57,23 +51,47 @@ }]; } -- (NSFetchedResultsController *)fetchedResultsController { +- (NSFetchedResultsController *)fetchedResultsControllerByLastUsed { - if (!_fetchedResultsController) { - NSAssert([[NSThread currentThread] isMainThread], @"The fetchedResultsController must run on the main thread."); + if (!_fetchedResultsControllerByLastUsed) { + NSAssert([[NSThread currentThread] isMainThread], @"The fetchedResultsController must be accessed from the main thread."); NSManagedObjectContext *moc = [MPAppDelegate managedObjectContextForThreadIfReady]; if (!moc) return nil; NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])]; - fetchRequest.sortDescriptors = @[[[NSSortDescriptor alloc] initWithKey:@"uses_" ascending:NO]]; - fetchRequest.fetchBatchSize = 20; - _fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:moc + fetchRequest.sortDescriptors = @[[[NSSortDescriptor alloc] initWithKey:NSStringFromSelector( @selector(lastUsed) ) ascending:NO]]; + [self configureFetchRequest:fetchRequest]; + _fetchedResultsControllerByLastUsed = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:moc sectionNameKeyPath:nil cacheName:nil]; - _fetchedResultsController.delegate = self; + _fetchedResultsControllerByLastUsed.delegate = self; } - return _fetchedResultsController; + return _fetchedResultsControllerByLastUsed; +} + +- (NSFetchedResultsController *)fetchedResultsControllerByUses { + + if (!_fetchedResultsControllerByUses) { + NSAssert([[NSThread currentThread] isMainThread], @"The fetchedResultsController must be accessed from the main thread."); + NSManagedObjectContext *moc = [MPAppDelegate managedObjectContextForThreadIfReady]; + if (!moc) + return nil; + + NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPElementEntity class] )]; + fetchRequest.sortDescriptors = @[ [[NSSortDescriptor alloc] initWithKey:NSStringFromSelector( @selector(uses_) ) ascending:NO] ]; + [self configureFetchRequest:fetchRequest]; + _fetchedResultsControllerByUses = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:moc + sectionNameKeyPath:nil cacheName:nil]; + _fetchedResultsControllerByUses.delegate = self; + } + + return _fetchedResultsControllerByUses; +} + +- (void)configureFetchRequest:(NSFetchRequest *)fetchRequest { + + fetchRequest.fetchLimit = 5; } - (NSDateFormatter *)dateFormatter { @@ -92,34 +110,33 @@ // Build predicate. NSPredicate *predicate = [NSPredicate predicateWithFormat:@"user == %@", activeUser]; + + // Add query predicate. UISearchBar *searchBar = self.searchDisplayController.searchBar; if (searchBar) { NSString *query = [searchBar.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; if (!query) return; - // Add query predicate. predicate = [NSCompoundPredicate andPredicateWithSubpredicates: @[predicate, [NSPredicate predicateWithFormat:@"name BEGINSWITH[cd] %@", query]]]; - - // Add scope predicate. - switch ((MPSearchScope) searchBar.selectedScopeButtonIndex) { - - case MPSearchScopeAll: - break; - case MPSearchScopeOutdated: - predicate = [NSCompoundPredicate andPredicateWithSubpredicates: - @[[NSPredicate predicateWithFormat:@"requiresExplicitMigration_ == YES"], predicate]]; - break; - } } - self.fetchedResultsController.fetchRequest.predicate = predicate; + + // Add filter predicate. + if ([self.filter isEqualToString:MPElementListFilterOutdated]) + predicate = [NSCompoundPredicate andPredicateWithSubpredicates: + @[[NSPredicate predicateWithFormat:@"requiresExplicitMigration_ == YES"], predicate]]; + // Fetch NSError *error; - if (![self.fetchedResultsController performFetch:&error]) + self.fetchedResultsControllerByLastUsed.fetchRequest.predicate = predicate; + self.fetchedResultsControllerByUses.fetchRequest.predicate = predicate; + if (![self.fetchedResultsControllerByLastUsed performFetch:&error]) err(@"Couldn't fetch elements: %@", error); - else - [self.tableView reloadData]; + if (![self.fetchedResultsControllerByUses performFetch:&error]) + err(@"Couldn't fetch elements: %@", error); + + [self.tableView reloadData]; } - (void)customTableViewUpdates { @@ -140,52 +157,32 @@ case NSFetchedResultsChangeInsert: dbg(@"%@ -- NSFetchedResultsChangeInsert:%@", NSStringFromSelector(_cmd), anObject); - [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; + [self.tableView insertRowsAtIndexPaths:@[ [self tableIndexPathForFetchController:controller indexPath:newIndexPath] ] + withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeDelete: dbg(@"%@ -- NSFetchedResultsChangeDelete:%@", NSStringFromSelector(_cmd), anObject); - [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; + [self.tableView deleteRowsAtIndexPaths:@[ [self tableIndexPathForFetchController:controller indexPath:indexPath] ] + withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeUpdate: dbg(@"%@ -- NSFetchedResultsChangeUpdate:%@", NSStringFromSelector(_cmd), anObject); - [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; + [self.tableView reloadRowsAtIndexPaths:@[ [self tableIndexPathForFetchController:controller indexPath:indexPath] ] + withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeMove: dbg(@"%@ -- NSFetchedResultsChangeMove:%@", NSStringFromSelector(_cmd), anObject); - [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] + [self.tableView deleteRowsAtIndexPaths:@[ [self tableIndexPathForFetchController:controller indexPath:indexPath] ] withRowAnimation:UITableViewRowAnimationAutomatic]; - [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] + [self.tableView insertRowsAtIndexPaths:@[ [self tableIndexPathForFetchController:controller indexPath:newIndexPath] ] withRowAnimation:UITableViewRowAnimationAutomatic]; break; } } -- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id )sectionInfo - atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type { - - switch (type) { - - case NSFetchedResultsChangeInsert: - dbg(@"%@ -- NSFetchedResultsChangeInsert:%d", NSStringFromSelector(_cmd), sectionIndex); - [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] - withRowAnimation:UITableViewRowAnimationAutomatic]; - break; - - case NSFetchedResultsChangeDelete: - dbg(@"%@ -- NSFetchedResultsChangeDelete:%d", NSStringFromSelector(_cmd), sectionIndex); - [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] - withRowAnimation:UITableViewRowAnimationAutomatic]; - break; - - case NSFetchedResultsChangeMove: - case NSFetchedResultsChangeUpdate: - Throw(@"Invalid change type for section changes: %d", type); - } -} - - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller { dbg(@"%@ on %@", NSStringFromSelector(_cmd), [NSThread currentThread].name); @@ -195,16 +192,18 @@ - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { - NSInteger integer = (NSInteger)[[self.fetchedResultsController sections] count]; - dbg(@"%@ = %d", NSStringFromSelector(_cmd), integer); - return integer; + return 2; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - NSInteger integer = (NSInteger)[[[self.fetchedResultsController sections] objectAtIndex:(unsigned)section] numberOfObjects]; - dbg(@"%@%d = %d", NSStringFromSelector(_cmd), section, integer); - return integer; + if (section == 0) + return (NSInteger)[[[self.fetchedResultsControllerByLastUsed sections] lastObject] numberOfObjects]; + + if (section == 1) + return (NSInteger)[[[self.fetchedResultsControllerByUses sections] lastObject] numberOfObjects]; + + Throw(@"Unsupported section: %d", section); } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { @@ -213,50 +212,82 @@ if (!cell) cell = (UITableViewCell *) [[UIViewController alloc] initWithNibName:@"MPElementListCellView" bundle:nil].view; - [self configureCell:cell inTableView:tableView atIndexPath:indexPath]; + [self configureCell:cell inTableView:tableView atTableIndexPath:indexPath]; return cell; } -- (void)configureCell:(UITableViewCell *)cell inTableView:(UITableView *)tableView atIndexPath:(NSIndexPath *)indexPath { +- (void)configureCell:(UITableViewCell *)cell inTableView:(UITableView *)tableView atTableIndexPath:(NSIndexPath *)indexPath { - MPElementEntity *element = [self.fetchedResultsController objectAtIndexPath:indexPath]; + MPElementEntity *element = [self elementForTableIndexPath:indexPath]; cell.textLabel.text = element.name; cell.detailTextLabel.text = PearlString(@"%d views, last on %@: %@", element.uses, [self.dateFormatter stringFromDate:element.lastUsed], [element.algorithm shortNameOfType:element.type]); } +- (NSIndexPath *)tableIndexPathForFetchController:(NSFetchedResultsController *)fetchedResultsController indexPath:(NSIndexPath *)indexPath { + + if (fetchedResultsController == self.fetchedResultsControllerByLastUsed) + return [NSIndexPath indexPathForRow:indexPath.row inSection:0]; + if (fetchedResultsController == self.fetchedResultsControllerByUses) + return [NSIndexPath indexPathForRow:indexPath.row inSection:1]; + + Throw(@"Unknown fetched results controller: %@, for index path: %@", fetchedResultsController, indexPath); +} + +- (NSIndexPath *)fetchedIndexPathForTableIndexPath:(NSIndexPath *)indexPath { + + return [NSIndexPath indexPathForRow:indexPath.row inSection:0]; +} + +- (MPElementEntity *)elementForTableIndexPath:(NSIndexPath *)indexPath { + + if (indexPath.section == 0) + return [self.fetchedResultsControllerByLastUsed objectAtIndexPath:[self fetchedIndexPathForTableIndexPath:indexPath]]; + + if (indexPath.section == 1) + return [self.fetchedResultsControllerByUses objectAtIndexPath:[self fetchedIndexPathForTableIndexPath:indexPath]]; + + Throw(@"Unsupported section: %d", indexPath.section); +} + - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { - [self.delegate didSelectElement:[self.fetchedResultsController objectAtIndexPath:indexPath]]; + [self.delegate didSelectElement:[self elementForTableIndexPath:indexPath]]; } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { - return [[[self.fetchedResultsController sections] objectAtIndex:(unsigned)section] name]; + if (section == 0) + return @"Most Recently Used"; + + if (section == 1) + return @"Most Commonly Used"; + + Throw(@"Unsupported section: %d", section); } - (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView { - return [self.fetchedResultsController sectionIndexTitles]; + return @[@"recency", @"uses"]; } - (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index { - return [self.fetchedResultsController sectionForSectionIndexTitle:title atIndex:index]; + return index; } - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { - NSManagedObjectContext *moc = self.fetchedResultsController.managedObjectContext; - [moc performBlock:^{ - MPElementEntity *element = [self.fetchedResultsController objectAtIndexPath:indexPath]; + MPElementEntity *element = [self elementForTableIndexPath:indexPath]; + [element.managedObjectContext performBlockAndWait:^{ inf(@"Deleting element: %@", element.name); - [moc deleteObject:element]; + [element.managedObjectContext deleteObject:element]; + [element.managedObjectContext saveToStore]; #ifdef TESTFLIGHT_SDK_VERSION [TestFlight passCheckpoint:MPCheckpointDeleteElement]; diff --git a/MasterPassword/ObjC/iOS/MPElementListSearchController.m b/MasterPassword/ObjC/iOS/MPElementListSearchController.m index f90ae2c2..95512178 100644 --- a/MasterPassword/ObjC/iOS/MPElementListSearchController.m +++ b/MasterPassword/ObjC/iOS/MPElementListSearchController.m @@ -10,7 +10,6 @@ #import "MPMainViewController.h" #import "MPAppDelegate.h" - @interface MPElementListSearchController () @property (nonatomic) BOOL newSiteSectionWasNeeded; @@ -47,7 +46,7 @@ - (void)searchBarBookmarkButtonClicked:(UISearchBar *)searchBar { - [((MPMainViewController *)self.delegate) performSegueWithIdentifier:@"MP_AllSites" sender:self]; + [((MPMainViewController *)self.delegate) performSegueWithIdentifier:@"MP_AllSites" sender:MPElementListFilterNone]; } - (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar { @@ -70,12 +69,7 @@ - (void)searchDisplayControllerWillBeginSearch:(UISearchDisplayController *)controller { - controller.searchBar.showsScopeBar = controller.searchBar.selectedScopeButtonIndex != MPSearchScopeAll; controller.searchBar.text = @""; - if (controller.searchBar.showsScopeBar) - controller.searchBar.scopeButtonTitles = @[@"All", @"Outdated"]; - else - controller.searchBar.scopeButtonTitles = nil; [UIView animateWithDuration:0.2f animations:^{ self.searchTipContainer.alpha = 0; @@ -91,8 +85,6 @@ controller.searchBar.prompt = nil; controller.searchBar.searchResultsButtonSelected = NO; - controller.searchBar.selectedScopeButtonIndex = MPSearchScopeAll; - controller.searchBar.showsScopeBar = NO; } - (void)searchDisplayController:(UISearchDisplayController *)controller didLoadSearchResultsTableView:(UITableView *)tableView { @@ -110,13 +102,6 @@ return NO; } -- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchScope:(NSInteger)searchOption { - - [self updateData]; - return NO; -} - - - (void)updateData { [super updateData]; @@ -146,37 +131,39 @@ return NO; __block BOOL hasExactQueryMatch = NO; - [[self.fetchedResultsController sections] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { - id sectionInfo = obj; - [[sectionInfo objects] enumerateObjectsUsingBlock:^(id obj_, NSUInteger idx_, BOOL *stop_) { - if ([[obj_ name] isEqualToString:query]) { - hasExactQueryMatch = YES; - *stop_ = YES; - } - }]; - if (hasExactQueryMatch) - *stop = YES; + id sectionInfo = [[self.fetchedResultsControllerByUses sections] lastObject]; + [[sectionInfo objects] enumerateObjectsUsingBlock:^(id obj_, NSUInteger idx_, BOOL *stop_) { + if ([[obj_ name] isEqualToString:query]) { + hasExactQueryMatch = YES; + *stop_ = YES; + } }]; + if (hasExactQueryMatch) + return NO; - return !hasExactQueryMatch; + sectionInfo = [[self.fetchedResultsControllerByLastUsed sections] lastObject]; + [[sectionInfo objects] enumerateObjectsUsingBlock:^(id obj_, NSUInteger idx_, BOOL *stop_) { + if ([[obj_ name] isEqualToString:query]) { + hasExactQueryMatch = YES; + *stop_ = YES; + } + }]; + if (hasExactQueryMatch) + return NO; + + return YES; } - (void)customTableViewUpdates { BOOL newSiteSectionIsNeeded = [self newSiteSectionNeeded]; - dbg(@"isNeeded:%d, wasNeeded:%d", newSiteSectionIsNeeded, self.newSiteSectionWasNeeded); - if (newSiteSectionIsNeeded && !self.newSiteSectionWasNeeded) { - dbg(@"%@ -- insertSection:%d", NSStringFromSelector(_cmd), [[self.fetchedResultsController sections] count]); - [self.tableView insertSections:[NSIndexSet indexSetWithIndex:[[self.fetchedResultsController sections] count]] + if (newSiteSectionIsNeeded && !self.newSiteSectionWasNeeded) + [self.tableView insertSections:[NSIndexSet indexSetWithIndex:2] withRowAnimation:UITableViewRowAnimationAutomatic]; - } - else if (!newSiteSectionIsNeeded && self.newSiteSectionWasNeeded) { - dbg(@"%@ -- deleteSection:%d", NSStringFromSelector(_cmd), [[self.fetchedResultsController sections] count]); - [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:[[self.fetchedResultsController sections] count]] + else if (!newSiteSectionIsNeeded && self.newSiteSectionWasNeeded) + [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:2] withRowAnimation:UITableViewRowAnimationAutomatic]; - } self.newSiteSectionWasNeeded = newSiteSectionIsNeeded; - dbg(@"wasNeeded->%d", self.newSiteSectionWasNeeded); } - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { @@ -185,39 +172,37 @@ if ([self newSiteSectionNeeded]) ++sectionCount; - dbg(@"%@ (actually) = %d", NSStringFromSelector(_cmd), sectionCount); return sectionCount; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - NSUInteger fetchSections = [[self.fetchedResultsController sections] count]; - if (section < (NSInteger)fetchSections) + if (section < [super numberOfSectionsInTableView:tableView]) + // Section is one of super's sections. return [super tableView:tableView numberOfRowsInSection:section]; - dbg(@"%@%d = %d", NSStringFromSelector(_cmd), section, 1); return 1; } -- (void)configureCell:(UITableViewCell *)cell inTableView:(UITableView *)tableView atIndexPath:(NSIndexPath *)indexPath { +- (void)configureCell:(UITableViewCell *)cell inTableView:(UITableView *)tableView atTableIndexPath:(NSIndexPath *)indexPath { - NSUInteger fetchSections = [[self.fetchedResultsController sections] count]; - if (indexPath.section < (NSInteger)fetchSections) - [super configureCell:cell inTableView:tableView atIndexPath:indexPath]; - - else { - // "New" section - NSString *query = [self.searchDisplayController.searchBar.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; - cell.textLabel.text = query; - cell.detailTextLabel.text = PearlString(@"Add new site: %@", - [MPAlgorithmDefault shortNameOfType:[[MPAppDelegate get].activeUser defaultType]]); + if (indexPath.section < [super numberOfSectionsInTableView:tableView]) { + // Section is one of super's sections. + [super configureCell:cell inTableView:tableView atTableIndexPath:indexPath]; + return; } + + // "New" section + NSString *query = [self.searchDisplayController.searchBar.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + cell.textLabel.text = query; + cell.detailTextLabel.text = PearlString(@"New site: %@", + [MPAlgorithmDefault shortNameOfType:[[MPAppDelegate get].activeUser defaultType]]); } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { - NSUInteger fetchSections = [[self.fetchedResultsController sections] count]; - if (indexPath.section < (NSInteger)fetchSections) { + if (indexPath.section < [super numberOfSectionsInTableView:tableView]) { + // Section is one of super's sections. [super tableView:tableView didSelectRowAtIndexPath:indexPath]; return; } @@ -240,18 +225,18 @@ - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { - NSUInteger fetchSections = [[self.fetchedResultsController sections] count]; - if (section < (NSInteger)fetchSections) + if (section < [super numberOfSectionsInTableView:tableView]) + // Section is one of super's sections. return [super tableView:tableView titleForHeaderInSection:section]; - return @""; + return @"Create"; } - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { - NSUInteger fetchSections = [[self.fetchedResultsController sections] count]; - if (indexPath.section < (NSInteger)fetchSections) + if (indexPath.section < [super numberOfSectionsInTableView:tableView]) + // Section is one of super's sections. [super tableView:tableView commitEditingStyle:editingStyle forRowAtIndexPath:indexPath]; } diff --git a/MasterPassword/ObjC/iOS/MPMainViewController.m b/MasterPassword/ObjC/iOS/MPMainViewController.m index aa9949a5..11bfb20c 100644 --- a/MasterPassword/ObjC/iOS/MPMainViewController.m +++ b/MasterPassword/ObjC/iOS/MPMainViewController.m @@ -8,10 +8,8 @@ #import "MPMainViewController.h" #import "MPAppDelegate.h" -#import "MPAppDelegate_Key.h" #import "MPAppDelegate_Store.h" #import "MPElementListAllViewController.h" -#import "MPElementListSearchController.h" @interface MPMainViewController() @@ -50,8 +48,10 @@ if ([[segue identifier] isEqualToString:@"MP_ChooseType"]) ((MPTypeViewController *)[segue destinationViewController]).delegate = self; - if ([[segue identifier] isEqualToString:@"MP_AllSites"]) - ((MPElementListAllViewController *)[((UINavigationController *)[segue destinationViewController]) topViewController]).delegate = self; + if ([[segue identifier] isEqualToString:@"MP_AllSites"]) { + ((MPElementListAllViewController *)[segue destinationViewController]).delegate = self; + ((MPElementListAllViewController *)[segue destinationViewController]).filter = sender; + } } - (void)viewDidLoad { @@ -262,10 +262,10 @@ 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); + 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.frame = CGRectSetY(self.helpContainer.frame, 246); + [self.helpContainer setFrameFromCurrentSizeAndParentPaddingTop:CGFLOAT_MAX right:0 bottom:0 left:0]; } } @@ -690,9 +690,7 @@ - (IBAction)searchOutdatedElements { - self.searchDisplayController.searchBar.selectedScopeButtonIndex = MPSearchScopeOutdated; - self.searchDisplayController.searchBar.searchResultsButtonSelected = YES; - [self.searchDisplayController.searchBar becomeFirstResponder]; + [self performSegueWithIdentifier:@"MP_AllSites" sender:MPElementListFilterOutdated]; } - (IBAction)closeAlert { diff --git a/MasterPassword/ObjC/iOS/MPSetupViewController.h b/MasterPassword/ObjC/iOS/MPSetupViewController.h index 19160c17..950f2875 100644 --- a/MasterPassword/ObjC/iOS/MPSetupViewController.h +++ b/MasterPassword/ObjC/iOS/MPSetupViewController.h @@ -21,6 +21,7 @@ @interface MPSetupViewController : UIViewController @property (weak, nonatomic) IBOutlet UISwitch *cloudSwitch; +@property (weak, nonatomic) IBOutlet UISwitch *rememberLoginSwitch; - (IBAction)close:(UIBarButtonItem *)sender; diff --git a/MasterPassword/ObjC/iOS/MPSetupViewController.m b/MasterPassword/ObjC/iOS/MPSetupViewController.m index 2c85fc34..3977804f 100644 --- a/MasterPassword/ObjC/iOS/MPSetupViewController.m +++ b/MasterPassword/ObjC/iOS/MPSetupViewController.m @@ -30,6 +30,8 @@ if (self.cloudSwitch && [[MPiOSConfig get].iCloudDecided boolValue]) self.cloudSwitch.on = [MPAppDelegate get].storeManager.cloudEnabled; + if (self.rememberLoginSwitch) + self.rememberLoginSwitch.on = [[MPiOSConfig get].rememberLogin boolValue]; } - (void)viewWillDisappear:(BOOL)animated { @@ -40,6 +42,8 @@ [MPiOSConfig get].iCloudDecided = @YES; [MPAppDelegate get].storeManager.cloudEnabled = self.cloudSwitch.on; } + if (self.rememberLoginSwitch) + [MPiOSConfig get].rememberLogin = @(self.rememberLoginSwitch.on); } - (IBAction)close:(UIBarButtonItem *)sender { diff --git a/MasterPassword/ObjC/iOS/MPUnlockViewController.m b/MasterPassword/ObjC/iOS/MPUnlockViewController.m index e6325c6b..243d966f 100644 --- a/MasterPassword/ObjC/iOS/MPUnlockViewController.m +++ b/MasterPassword/ObjC/iOS/MPUnlockViewController.m @@ -212,7 +212,7 @@ if (!moc) return; - self.tip.text = @"Tap and hold to delete or reset."; + self.tip.text = @"Tap and hold to delete or reset user."; __block NSArray *users = nil; [moc performBlockAndWait:^{ @@ -266,15 +266,13 @@ avatar.backgroundColor = [UIColor clearColor]; } options:0]; [avatar onSelect:^(BOOL selected) { - if (selected) { - if ((self.selectedUser = user)) - [self didToggleUserSelection]; - else - [self didSelectNewUserAvatar:avatar]; - } else { + if (!selected) { self.selectedUser = nil; [self didToggleUserSelection]; - } + } else if ((self.selectedUser = user)) + [self didToggleUserSelection]; + else + [self didSelectNewUserAvatar:avatar]; } options:0]; [self.avatarToUserOID setObject:NilToNSNull([user objectID]) forKey:[NSValue valueWithNonretainedObject:avatar]]; diff --git a/MasterPassword/ObjC/iOS/MainStoryboard_iPhone.storyboard b/MasterPassword/ObjC/iOS/MainStoryboard_iPhone.storyboard index f8fee226..14e9bab1 100644 --- a/MasterPassword/ObjC/iOS/MainStoryboard_iPhone.storyboard +++ b/MasterPassword/ObjC/iOS/MainStoryboard_iPhone.storyboard @@ -705,10 +705,6 @@ Your passwords will be AES-encrypted with your master password. - - All - Outdated - @@ -1006,7 +1002,7 @@ L4m3P4sSw0rD - + @@ -1101,7 +1097,7 @@ L4m3P4sSw0rD - + @@ -1254,11 +1250,11 @@ L4m3P4sSw0rD - - + - - The passwords created by this app are not stored anywhere but created on the spot. - -It is vital that you + - + + + + + + + + + . + +You don't need to remember any password generated by this app, but + + + + + + + - + - + + - + @@ -2122,28 +2126,29 @@ It is vital that you - -Cgo - + and that it is secure. + +Don't reuse an old password! +Don't give out your master password. +A small nonsense sentence is a great password. - - Don't reuse an old password! - + + - Don't give out your master password, no matter how close you think you are with the other person. + -You can make passwords for all sorts of things, like email addresses, sites or real-world objects like your bike lock: if you can name it, you can get a password for it. +You can make passwords for anything, like email addresses, sites or real-world things like a bike lock: if you can name it, you can get a password for it. @@ -2160,13 +2165,13 @@ You can make passwords for all sorts of things, like email addresses, sites or r - + - + @@ -2175,7 +2180,7 @@ You can make passwords for all sorts of things, like email addresses, sites or r - + @@ -2527,7 +2532,66 @@ You can make passwords for all sorts of things, like email addresses, sites or r - + + + + + + + + + + + + + + + + + + + + + + + + The right balance between security and convenience is often very personal. + +To make getting to your passwords faster, you can remain logged in after you close Master Password. This allows you to skip having to log in the next time. + +However, it means that anyone who finds your device unlocked can do the same. + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2567,6 +2631,7 @@ You can make passwords for all sorts of things, like email addresses, sites or r + @@ -2588,6 +2653,7 @@ You can make passwords for all sorts of things, like email addresses, sites or r + @@ -2700,6 +2766,7 @@ You can make passwords for all sorts of things, like email addresses, sites or r + @@ -2742,7 +2809,7 @@ You can make passwords for all sorts of things, like email addresses, sites or r + - \ No newline at end of file diff --git a/MasterPassword/ObjC/iOS/MasterPassword-iOS.xcodeproj/project.pbxproj b/MasterPassword/ObjC/iOS/MasterPassword-iOS.xcodeproj/project.pbxproj index 7a652394..af57d1de 100644 --- a/MasterPassword/ObjC/iOS/MasterPassword-iOS.xcodeproj/project.pbxproj +++ b/MasterPassword/ObjC/iOS/MasterPassword-iOS.xcodeproj/project.pbxproj @@ -38,6 +38,8 @@ DA4DA1DA1564471F00F6F596 /* libuicolor-utilities.a in Frameworks */ = {isa = PBXBuildFile; fileRef = DAC6325D1486805C0075AEA5 /* libuicolor-utilities.a */; }; DA5A09DF171A70E4005284AB /* play.png in Resources */ = {isa = PBXBuildFile; fileRef = DA5A09DD171A70E4005284AB /* play.png */; }; DA5A09E0171A70E4005284AB /* play@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = DA5A09DE171A70E4005284AB /* play@2x.png */; }; + DA5A09EA171BB0F7005284AB /* unlocked.png in Resources */ = {isa = PBXBuildFile; fileRef = DA5A09E8171BB0F7005284AB /* unlocked.png */; }; + DA5A09EB171BB0F7005284AB /* unlocked@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = DA5A09E9171BB0F7005284AB /* unlocked@2x.png */; }; DA5BFA49147E415C00F98B1E /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA5BFA48147E415C00F98B1E /* UIKit.framework */; }; DA5BFA4B147E415C00F98B1E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA5BFA4A147E415C00F98B1E /* Foundation.framework */; }; DA5BFA4D147E415C00F98B1E /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA5BFA4C147E415C00F98B1E /* CoreGraphics.framework */; }; @@ -1036,6 +1038,8 @@ DA497B9715E8C90E00B52167 /* libGoogle+.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libGoogle+.a"; sourceTree = BUILT_PRODUCTS_DIR; }; DA5A09DD171A70E4005284AB /* play.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = play.png; sourceTree = ""; }; DA5A09DE171A70E4005284AB /* play@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "play@2x.png"; sourceTree = ""; }; + DA5A09E8171BB0F7005284AB /* unlocked.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = unlocked.png; sourceTree = ""; }; + DA5A09E9171BB0F7005284AB /* unlocked@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "unlocked@2x.png"; sourceTree = ""; }; DA5BFA44147E415C00F98B1E /* MasterPassword.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MasterPassword.app; sourceTree = BUILT_PRODUCTS_DIR; }; DA5BFA48147E415C00F98B1E /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; DA5BFA4A147E415C00F98B1E /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; @@ -2206,6 +2210,8 @@ DABD360D1711E29400CF925C /* Media */ = { isa = PBXGroup; children = ( + DA5A09E8171BB0F7005284AB /* unlocked.png */, + DA5A09E9171BB0F7005284AB /* unlocked@2x.png */, DA5A09DD171A70E4005284AB /* play.png */, DA5A09DE171A70E4005284AB /* play@2x.png */, DABD360E1711E29400CF925C /* Automaton */, @@ -4596,6 +4602,8 @@ DABD3FCF1714F45C00CF925C /* identity@2x.png in Resources */, DA5A09DF171A70E4005284AB /* play.png in Resources */, DA5A09E0171A70E4005284AB /* play@2x.png in Resources */, + DA5A09EA171BB0F7005284AB /* unlocked.png in Resources */, + DA5A09EB171BB0F7005284AB /* unlocked@2x.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MasterPassword/Resources/Media/unlocked.png b/MasterPassword/Resources/Media/unlocked.png new file mode 100644 index 00000000..58b62962 Binary files /dev/null and b/MasterPassword/Resources/Media/unlocked.png differ diff --git a/MasterPassword/Resources/Media/unlocked@2x.png b/MasterPassword/Resources/Media/unlocked@2x.png new file mode 100644 index 00000000..6d494506 Binary files /dev/null and b/MasterPassword/Resources/Media/unlocked@2x.png differ diff --git a/MasterPassword/Resources/Raw/Large Icons.vdesigner/QuickLook/Preview.pdf b/MasterPassword/Resources/Raw/Large Icons.vdesigner/QuickLook/Preview.pdf index 14fb9293..87739826 100644 Binary files a/MasterPassword/Resources/Raw/Large Icons.vdesigner/QuickLook/Preview.pdf and b/MasterPassword/Resources/Raw/Large Icons.vdesigner/QuickLook/Preview.pdf differ diff --git a/MasterPassword/Resources/Raw/Large Icons.vdesigner/QuickLook/Thumbnail.jpg b/MasterPassword/Resources/Raw/Large Icons.vdesigner/QuickLook/Thumbnail.jpg index e029158a..53a6fc2f 100644 Binary files a/MasterPassword/Resources/Raw/Large Icons.vdesigner/QuickLook/Thumbnail.jpg and b/MasterPassword/Resources/Raw/Large Icons.vdesigner/QuickLook/Thumbnail.jpg differ diff --git a/MasterPassword/Resources/Raw/Large Icons.vdesigner/VectorDesigner b/MasterPassword/Resources/Raw/Large Icons.vdesigner/VectorDesigner index a4b9441f..7b750ff5 100644 Binary files a/MasterPassword/Resources/Raw/Large Icons.vdesigner/VectorDesigner and b/MasterPassword/Resources/Raw/Large Icons.vdesigner/VectorDesigner differ