diff --git a/MasterPassword/ObjC/MPAppDelegate_Shared.h b/MasterPassword/ObjC/MPAppDelegate_Shared.h index d961130d..8c1cce86 100644 --- a/MasterPassword/ObjC/MPAppDelegate_Shared.h +++ b/MasterPassword/ObjC/MPAppDelegate_Shared.h @@ -16,6 +16,7 @@ #endif @property(strong, nonatomic) MPKey *key; +@property(strong, nonatomic) NSManagedObjectID *activeUserOID; + (instancetype)get; diff --git a/MasterPassword/ObjC/MPAppDelegate_Shared.m b/MasterPassword/ObjC/MPAppDelegate_Shared.m index ad495b2a..5b14c946 100644 --- a/MasterPassword/ObjC/MPAppDelegate_Shared.m +++ b/MasterPassword/ObjC/MPAppDelegate_Shared.m @@ -10,9 +10,7 @@ #import "MPAppDelegate_Store.h" #import "MPAppDelegate_Key.h" -@implementation MPAppDelegate_Shared { - NSManagedObjectID *_activeUserOID; -} +@implementation MPAppDelegate_Shared + (MPAppDelegate_Shared *)get { @@ -32,11 +30,11 @@ - (MPUserEntity *)activeUserInContext:(NSManagedObjectContext *)moc { - if (!_activeUserOID || !moc) + if (!self.activeUserOID || !moc) return nil; NSError *error; - MPUserEntity *activeUser = (MPUserEntity *)[moc existingObjectWithID:_activeUserOID error:&error]; + MPUserEntity *activeUser = (MPUserEntity *)[moc existingObjectWithID:self.activeUserOID error:&error]; if (!activeUser) { [self signOutAnimated:YES]; err(@"Failed to retrieve active user: %@", error); @@ -51,7 +49,7 @@ if (activeUser.objectID.isTemporaryID && ![activeUser.managedObjectContext obtainPermanentIDsForObjects:@[ activeUser ] error:&error]) err(@"Failed to obtain a permanent object ID after setting active user: %@", error); - _activeUserOID = activeUser.objectID; + self.activeUserOID = activeUser.objectID; } @end diff --git a/MasterPassword/ObjC/MPAppDelegate_Store.h b/MasterPassword/ObjC/MPAppDelegate_Store.h index b0dacec0..b2ba5269 100644 --- a/MasterPassword/ObjC/MPAppDelegate_Store.h +++ b/MasterPassword/ObjC/MPAppDelegate_Store.h @@ -21,6 +21,8 @@ typedef enum { @interface MPAppDelegate_Shared(Store) + (NSManagedObjectContext *)managedObjectContextForMainThreadIfReady; ++ (BOOL)managedObjectContextForMainThreadPerformBlock:(void (^)(NSManagedObjectContext *mainContext))mocBlock; ++ (BOOL)managedObjectContextForMainThreadPerformBlockAndWait:(void (^)(NSManagedObjectContext *mainContext))mocBlock; + (BOOL)managedObjectContextPerformBlock:(void (^)(NSManagedObjectContext *context))mocBlock; + (BOOL)managedObjectContextPerformBlockAndWait:(void (^)(NSManagedObjectContext *context))mocBlock; diff --git a/MasterPassword/ObjC/MPAppDelegate_Store.m b/MasterPassword/ObjC/MPAppDelegate_Store.m index a381f758..cf4703f2 100644 --- a/MasterPassword/ObjC/MPAppDelegate_Store.m +++ b/MasterPassword/ObjC/MPAppDelegate_Store.m @@ -48,6 +48,32 @@ PearlAssociatedObjectProperty(NSManagedObjectContext*, MainManagedObjectContext, return mainManagedObjectContext; } ++ (BOOL)managedObjectContextForMainThreadPerformBlock:(void (^)(NSManagedObjectContext *mainContext))mocBlock { + + NSManagedObjectContext *mainManagedObjectContext = [self managedObjectContextForMainThreadIfReady]; + if (!mainManagedObjectContext) + return NO; + + [mainManagedObjectContext performBlock:^{ + mocBlock( mainManagedObjectContext ); + }]; + + return YES; +} + ++ (BOOL)managedObjectContextForMainThreadPerformBlockAndWait:(void (^)(NSManagedObjectContext *mainContext))mocBlock { + + NSManagedObjectContext *mainManagedObjectContext = [self managedObjectContextForMainThreadIfReady]; + if (!mainManagedObjectContext) + return NO; + + [mainManagedObjectContext performBlockAndWait:^{ + mocBlock( mainManagedObjectContext ); + }]; + + return YES; +} + + (BOOL)managedObjectContextPerformBlock:(void (^)(NSManagedObjectContext *context))mocBlock { NSManagedObjectContext *mainManagedObjectContext = [[self get] mainManagedObjectContextIfReady]; diff --git a/MasterPassword/ObjC/MPEntities.h b/MasterPassword/ObjC/MPEntities.h index b91737dc..dc6d1351 100644 --- a/MasterPassword/ObjC/MPEntities.h +++ b/MasterPassword/ObjC/MPEntities.h @@ -35,6 +35,8 @@ - (NSUInteger)use; - (BOOL)migrateExplicitly:(BOOL)explicit; +- (NSString *)resolveContentUsingKey:(MPKey *)key; +- (void)resolveContentUsingKey:(MPKey *)key result:(void (^)(NSString *))result; @end diff --git a/MasterPassword/ObjC/MPEntities.m b/MasterPassword/ObjC/MPEntities.m index 5b5fbc32..e46f60b5 100644 --- a/MasterPassword/ObjC/MPEntities.m +++ b/MasterPassword/ObjC/MPEntities.m @@ -145,6 +145,16 @@ return YES; } +- (NSString *)resolveContentUsingKey:(MPKey *)key { + + return [self.algorithm resolveContentForElement:self usingKey:key]; +} + +- (void)resolveContentUsingKey:(MPKey *)key result:(void (^)(NSString *))result { + + [self.algorithm resolveContentForElement:self usingKey:key result:result]; +} + @end @implementation MPElementGeneratedEntity(MP) diff --git a/MasterPassword/ObjC/iOS/MPAvatarCell.m b/MasterPassword/ObjC/iOS/MPAvatarCell.m index d1e44f94..391fcee8 100644 --- a/MasterPassword/ObjC/iOS/MPAvatarCell.m +++ b/MasterPassword/ObjC/iOS/MPAvatarCell.m @@ -37,7 +37,7 @@ const long MPAvatarAdd = 10000; + (NSString *)reuseIdentifier { - return @"MPAvatarCell"; + return NSStringFromClass( self ); } #pragma mark - Life cycle diff --git a/MasterPassword/ObjC/iOS/MPPasswordCell.h b/MasterPassword/ObjC/iOS/MPPasswordCell.h new file mode 100644 index 00000000..8d1116c4 --- /dev/null +++ b/MasterPassword/ObjC/iOS/MPPasswordCell.h @@ -0,0 +1,36 @@ +/** + * Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com) + * + * See the enclosed file LICENSE for license information (LGPLv3). If you did + * not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt + * + * @author Maarten Billemont + * @license http://www.gnu.org/licenses/lgpl-3.0.txt + */ + +// +// MPAvatarCell.h +// MPAvatarCell +// +// Created by lhunath on 2014-03-11. +// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved. +// + +#import +#import "MPEntities.h" + +@interface MPPasswordCell : UICollectionViewCell + +@property(nonatomic, copy) NSString *transientSite; +@property(strong, nonatomic) IBOutlet UITextField *contentField; + ++ (NSString *)reuseIdentifier; + +- (MPElementEntity *)elementInContext:(NSManagedObjectContext *)context; ++ (NSString *)reuseIdentifierForElement:(MPElementEntity *)entity; +- (void)setElement:(MPElementEntity *)element; + +- (void)updateAnimated:(BOOL)animated; +- (void)populateWithElement:(MPElementEntity *)element; + +@end diff --git a/MasterPassword/ObjC/iOS/MPPasswordCell.m b/MasterPassword/ObjC/iOS/MPPasswordCell.m new file mode 100644 index 00000000..35e694a5 --- /dev/null +++ b/MasterPassword/ObjC/iOS/MPPasswordCell.m @@ -0,0 +1,186 @@ +/** + * Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com) + * + * See the enclosed file LICENSE for license information (LGPLv3). If you did + * not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt + * + * @author Maarten Billemont + * @license http://www.gnu.org/licenses/lgpl-3.0.txt + */ + +// +// MPAvatarCell.h +// MPAvatarCell +// +// Created by lhunath on 2014-03-11. +// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved. +// + +#import "MPPasswordCell.h" +#import "MPiOSAppDelegate.h" +#import "MPAppDelegate_Store.h" +#import "MPPasswordGeneratedCell.h" +#import "MPPasswordStoredCell.h" + +@interface MPPasswordCell() + +@property(strong, nonatomic) IBOutlet UILabel *nameLabel; +@property(strong, nonatomic) IBOutlet UIButton *loginButton; + +@property(nonatomic, strong) NSManagedObjectID *elementOID; +@end + +@implementation MPPasswordCell + ++ (NSString *)reuseIdentifier { + + return NSStringFromClass( self ); +} + ++ (NSString *)reuseIdentifierForElement:(MPElementEntity *)element { + + if ([element isKindOfClass:[MPElementGeneratedEntity class]]) + return [MPPasswordGeneratedCell reuseIdentifier]; + + if ([element isKindOfClass:[MPElementStoredEntity class]]) + return [MPPasswordStoredCell reuseIdentifier]; + + return [self reuseIdentifier]; +} + +#pragma mark - UITextFieldDelegate + +- (BOOL)textFieldShouldReturn:(UITextField *)textField { + + if (textField == self.contentField && [self.contentField.text length]) { + [self.contentField resignFirstResponder]; + return YES; + } + + return NO; +} + +#pragma mark - Life cycle + +- (void)awakeFromNib { + + [super awakeFromNib]; + + self.layer.cornerRadius = 5; +} + +- (void)dealloc { + + [self removeKeyPathObservers]; +} + +#pragma mark - Actions + +- (IBAction)doUser:(id)sender { +} + +#pragma mark - Properties + +- (void)setTransientSite:(NSString *)name { + + _transientSite = name; + _elementOID = nil; + + [self updateAnimated:YES]; +} + +- (void)setElement:(MPElementEntity *)element { + + self.elementOID = [element objectID]; +} + +- (void)setElementOID:(NSManagedObjectID *)elementOID { + + _transientSite = nil; + _elementOID = elementOID; + + [self updateAnimated:YES]; +} + +- (MPElementEntity *)elementInContext:(NSManagedObjectContext *)context { + + NSError *error = nil; + MPElementEntity *element = _elementOID? (MPElementEntity *)[context existingObjectWithID:_elementOID error:&error]: nil; + if (_elementOID && !element) + err(@"Failed to load element: %@", error); + + return element; +} + +#pragma mark - Private + +- (void)updateAnimated:(BOOL)animated { + + Weakify(self); + + if (self.transientSite) { + self.alpha = 1; + self.nameLabel.text = self.transientSite; + self.contentField.text = nil; + + [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) { + MPKey *key = [MPiOSAppDelegate get].key; + if (!key) { + self.alpha = 0; + return; + } + + MPElementType type = [[MPiOSAppDelegate get] activeUserInContext:context].defaultType; + if (!type) + type = MPElementTypeGeneratedLong; + NSString *newContent = [MPAlgorithmDefault generateContentNamed:self.transientSite ofType:type withCounter:1 usingKey:key]; + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ + self.contentField.text = newContent; + }]; + }]; + } + else if (self.elementOID) { + NSManagedObjectContext *mainContext = [MPiOSAppDelegate managedObjectContextForMainThreadIfReady]; + [mainContext performBlock:^{ + [UIView animateWithDuration:animated? 0.3f: 0 animations:^{ + Strongify(self); + NSError *error = nil; + MPKey *key = [MPiOSAppDelegate get].key; + if (!key) { + self.alpha = 0; + return; + } + + MPElementEntity *element = (MPElementEntity *)[mainContext existingObjectWithID:_elementOID error:&error]; + if (!element) { + err(@"Failed to load element: %@", error); + self.alpha = 0; + return; + } + + self.alpha = 1; + [self populateWithElement:element]; + + [element resolveContentUsingKey:key result:^(NSString *result) { + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ + Strongify(self); + self.contentField.text = result; + }]; + }]; + }]; + }]; + } + else { + [UIView animateWithDuration:animated? 0.3f: 0 animations:^{ + self.alpha = 0; + }]; + } +} + +- (void)populateWithElement:(MPElementEntity *)element { + + self.nameLabel.text = element.name; + self.contentField.text = nil; +} + +@end diff --git a/MasterPassword/ObjC/iOS/MPPasswordGeneratedCell.h b/MasterPassword/ObjC/iOS/MPPasswordGeneratedCell.h new file mode 100644 index 00000000..9e123d45 --- /dev/null +++ b/MasterPassword/ObjC/iOS/MPPasswordGeneratedCell.h @@ -0,0 +1,26 @@ +/** + * Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com) + * + * See the enclosed file LICENSE for license information (LGPLv3). If you did + * not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt + * + * @author Maarten Billemont + * @license http://www.gnu.org/licenses/lgpl-3.0.txt + */ + +// +// MPPasswordGeneratedCell.h +// MPPasswordGeneratedCell +// +// Created by lhunath on 2014-03-19. +// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved. +// + +#import +#import "MPPasswordCell.h" + +@interface MPPasswordGeneratedCell : MPPasswordCell + +- (MPElementGeneratedEntity *)elementInContext:(NSManagedObjectContext *)context; + +@end diff --git a/MasterPassword/ObjC/iOS/MPPasswordGeneratedCell.m b/MasterPassword/ObjC/iOS/MPPasswordGeneratedCell.m new file mode 100644 index 00000000..e6735824 --- /dev/null +++ b/MasterPassword/ObjC/iOS/MPPasswordGeneratedCell.m @@ -0,0 +1,87 @@ +/** + * Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com) + * + * See the enclosed file LICENSE for license information (LGPLv3). If you did + * not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt + * + * @author Maarten Billemont + * @license http://www.gnu.org/licenses/lgpl-3.0.txt + */ + +// +// MPPasswordGeneratedCell.h +// MPPasswordGeneratedCell +// +// Created by lhunath on 2014-03-19. +// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved. +// + +#import "MPPasswordGeneratedCell.h" +#import "MPiOSAppDelegate.h" +#import "MPAppDelegate_Store.h" + +@interface MPPasswordGeneratedCell() + +@property(strong, nonatomic) IBOutlet UILabel *counterLabel; +@property(strong, nonatomic) IBOutlet UIButton *counterButton; +@property(strong, nonatomic) IBOutlet UIButton *upgradeButton; + +@end + +@implementation MPPasswordGeneratedCell + +- (void)populateWithElement:(MPElementEntity *)element { + + [super populateWithElement:element]; + + MPElementGeneratedEntity *generatedElement = [self generatedElement:element]; + self.counterLabel.text = strf(@"%lu", (unsigned long)generatedElement.counter); + + if (element.requiresExplicitMigration) { + self.upgradeButton.alpha = 1; + self.counterButton.alpha = 0; + } else { + self.upgradeButton.alpha = 0; + self.counterButton.alpha = 1; + } +} + +#pragma mark - Actions + +- (IBAction)doUpgrade:(UIButton *)sender { + + [MPiOSAppDelegate managedObjectContextForMainThreadPerformBlock:^(NSManagedObjectContext *mainContext) { + [[self elementInContext:mainContext] migrateExplicitly:YES]; + [mainContext saveToStore]; + + [self updateAnimated:YES]; + }]; +} + +- (IBAction)doIncrementCounter:(UIButton *)sender { + + [MPiOSAppDelegate managedObjectContextForMainThreadPerformBlock:^(NSManagedObjectContext *mainContext) { + ++[self elementInContext:mainContext].counter; + [mainContext saveToStore]; + + [self updateAnimated:YES]; + }]; +} + +#pragma mark - Properties + +- (MPElementGeneratedEntity *)elementInContext:(NSManagedObjectContext *)context { + + return [self generatedElement:[super elementInContext:context]]; +} + +- (MPElementGeneratedEntity *)generatedElement:(MPElementEntity *)element { + + NSAssert([element isKindOfClass:[MPElementGeneratedEntity class]], @"Element is not of generated type: %@", element.name); + if (![element isKindOfClass:[MPElementGeneratedEntity class]]) + return nil; + + return (MPElementGeneratedEntity *)element; +} + +@end diff --git a/MasterPassword/ObjC/iOS/MPPasswordStoredCell.h b/MasterPassword/ObjC/iOS/MPPasswordStoredCell.h new file mode 100644 index 00000000..9f07f78c --- /dev/null +++ b/MasterPassword/ObjC/iOS/MPPasswordStoredCell.h @@ -0,0 +1,26 @@ +/** + * Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com) + * + * See the enclosed file LICENSE for license information (LGPLv3). If you did + * not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt + * + * @author Maarten Billemont + * @license http://www.gnu.org/licenses/lgpl-3.0.txt + */ + +// +// MPPasswordStoredCell.h +// MPPasswordStoredCell +// +// Created by lhunath on 2014-03-19. +// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved. +// + +#import +#import "MPPasswordCell.h" + +@interface MPPasswordStoredCell : MPPasswordCell + +- (MPElementStoredEntity *)elementInContext:(NSManagedObjectContext *)context; + +@end diff --git a/MasterPassword/ObjC/iOS/MPPasswordStoredCell.m b/MasterPassword/ObjC/iOS/MPPasswordStoredCell.m new file mode 100644 index 00000000..3c0d141f --- /dev/null +++ b/MasterPassword/ObjC/iOS/MPPasswordStoredCell.m @@ -0,0 +1,73 @@ +/** + * Copyright Maarten Billemont (http://www.lhunath.com, lhunath@lyndir.com) + * + * See the enclosed file LICENSE for license information (LGPLv3). If you did + * not receive this file, see http://www.gnu.org/licenses/lgpl-3.0.txt + * + * @author Maarten Billemont + * @license http://www.gnu.org/licenses/lgpl-3.0.txt + */ + +// +// MPPasswordGeneratedCell.h +// MPPasswordGeneratedCell +// +// Created by lhunath on 2014-03-19. +// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved. +// + +#import "MPPasswordStoredCell.h" +#import "MPiOSAppDelegate.h" +#import "MPAppDelegate_Store.h" + +@interface MPPasswordStoredCell() + +@property(strong, nonatomic) IBOutlet UIButton *editButton; + +@end + +@implementation MPPasswordStoredCell + +#pragma mark - Actions + +- (IBAction)doEditContent:(UIButton *)sender { + + self.contentField.enabled = YES; + [self.contentField becomeFirstResponder]; +} + +#pragma mark - UITextFieldDelegate + +- (void)textFieldDidEndEditing:(UITextField *)textField { + + [super textFieldDidEndEditing:textField]; + + if (textField == self.contentField) { + [MPiOSAppDelegate managedObjectContextForMainThreadPerformBlock:^(NSManagedObjectContext *mainContext) { + if (mainContext) { + [[self elementInContext:mainContext] setContentObject:self.contentField.text]; + [mainContext saveToStore]; + } + + [self updateAnimated:YES]; + }]; + } +} + +#pragma mark - Properties + +- (MPElementStoredEntity *)elementInContext:(NSManagedObjectContext *)context { + + return [self storedElement:[super elementInContext:context]]; +} + +- (MPElementStoredEntity *)storedElement:(MPElementEntity *)element { + + NSAssert([element isKindOfClass:[MPElementStoredEntity class]], @"Element is not of generated type: %@", element.name); + if (![element isKindOfClass:[MPElementStoredEntity class]]) + return nil; + + return (MPElementStoredEntity *)element; +} + +@end diff --git a/MasterPassword/ObjC/iOS/MPPasswordsViewController.h b/MasterPassword/ObjC/iOS/MPPasswordsViewController.h index 05fd41b0..b37ee6d0 100644 --- a/MasterPassword/ObjC/iOS/MPPasswordsViewController.h +++ b/MasterPassword/ObjC/iOS/MPPasswordsViewController.h @@ -18,10 +18,11 @@ #import "LLGitTip.h" -@interface MPPasswordsViewController : UIViewController +@interface MPPasswordsViewController : UIViewController @property(strong, nonatomic) IBOutlet UIView *passwordSelectionContainer; @property(strong, nonatomic) IBOutlet UICollectionView *passwordCollectionView; +@property (strong, nonatomic) IBOutlet UISearchBar *passwordsSearchBar; @property (strong, nonatomic) IBOutlet NSLayoutConstraint *passwordsToBottomConstraint; @property (strong, nonatomic) IBOutlet NSLayoutConstraint *navigationBarToTopConstraint; @property (strong, nonatomic) IBOutlet NSLayoutConstraint *navigationBarToPasswordsConstraint; diff --git a/MasterPassword/ObjC/iOS/MPPasswordsViewController.m b/MasterPassword/ObjC/iOS/MPPasswordsViewController.m index ffe16a47..85018248 100644 --- a/MasterPassword/ObjC/iOS/MPPasswordsViewController.m +++ b/MasterPassword/ObjC/iOS/MPPasswordsViewController.m @@ -19,10 +19,14 @@ #import "MPPasswordsViewController.h" #import "MPiOSAppDelegate.h" #import "MPAppDelegate_Store.h" +#import "MPPasswordCell.h" -@interface MPPasswordsViewController() -@property (strong, nonatomic) IBOutlet UINavigationBar *navigationBar; +@interface MPPasswordsViewController() +@property(strong, nonatomic) IBOutlet UINavigationBar *navigationBar; + +@property(nonatomic, readonly) NSString *query; +@property(nonatomic, strong) NSFetchedResultsController *fetchedResultsController; @end @implementation MPPasswordsViewController { @@ -54,28 +58,6 @@ [self stopObservingStore]; } -#pragma mark - UITableViewDataSource - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - - if (tableView == self.searchDisplayController.searchResultsTableView) - return 0; - - NSAssert(NO, @"Unexpected table view: %@", tableView); - return 0; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { - - if (tableView == self.searchDisplayController.searchResultsTableView) { - - } - - NSAssert(NO, @"Unexpected table view: %@", tableView); - return nil; -} - - #pragma mark - UITextFieldDelegate - (void)textFieldDidEndEditing:(UITextField *)textField { @@ -88,43 +70,99 @@ // This isn't really in UITextFieldDelegate. We fake it from UITextFieldTextDidChangeNotification. - (void)textFieldDidChange:(UITextField *)textField { - } #pragma mark - UICollectionViewDataSource +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { + + return [self.fetchedResultsController.sections count] + 1; +} + - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { - if (collectionView == self.passwordCollectionView) - return 0; + if (collectionView == self.passwordCollectionView) { + if (section < [self.fetchedResultsController.sections count]) + return ((id)self.fetchedResultsController.sections[section]).numberOfObjects; + + // New Site. + return [self.query length]? 1: 0; + } Throw(@"unexpected collection view: %@", collectionView); } - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { - if (collectionView == self.passwordCollectionView) - return nil; + if (collectionView == self.passwordCollectionView) { + MPPasswordCell *cell; + if (indexPath.section < [self.fetchedResultsController.sections count]) { + MPElementEntity *element = [self.fetchedResultsController objectAtIndexPath:indexPath]; + if (indexPath.item < 2) + cell = [collectionView dequeueReusableCellWithReuseIdentifier:[MPPasswordCell reuseIdentifierForElement:element] forIndexPath:indexPath]; + else + cell = [collectionView dequeueReusableCellWithReuseIdentifier:[MPPasswordCell reuseIdentifierForElement:element] forIndexPath:indexPath]; + + [cell setElement:element]; + } else { + // New Site. + cell = [collectionView dequeueReusableCellWithReuseIdentifier:[MPPasswordCell reuseIdentifier] forIndexPath:indexPath]; + cell.transientSite = self.query; + } + return cell; + } Throw(@"unexpected collection view: %@", collectionView); } - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { - } - (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath { - } #pragma mark - UILongPressGestureRecognizer - (void)didLongPress:(UILongPressGestureRecognizer *)recognizer { - } #pragma mark - UIScrollViewDelegate +#pragma mark - UISearchBarDelegate + +- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar { + + if (searchBar == self.passwordsSearchBar) { + self.passwordsSearchBar.showsCancelButton = YES; + + [UIView animateWithDuration:0.3f animations:^{ + self.passwordCollectionView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.3f]; + }]; + } +} + +- (void)searchBarTextDidEndEditing:(UISearchBar *)searchBar { + + if (searchBar == self.passwordsSearchBar) { + self.passwordsSearchBar.showsCancelButton = NO; + + [UIView animateWithDuration:0.3f animations:^{ + self.passwordCollectionView.backgroundColor = [UIColor clearColor]; + }]; + } +} + +- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar { + + [searchBar resignFirstResponder]; +} + +- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { + + if (searchBar == self.passwordsSearchBar) + [self updatePasswords]; +} + #pragma mark - Private @@ -164,23 +202,24 @@ - (void)observeStore { -// Weakify(self); + Weakify(self); - NSManagedObjectContext *mainContext = [MPiOSAppDelegate managedObjectContextForMainThreadIfReady]; - if (!_mocObserver && mainContext) - _mocObserver = [[NSNotificationCenter defaultCenter] - addObserverForName:NSManagedObjectContextObjectsDidChangeNotification object:mainContext - queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { + NSManagedObjectContext *mainContext = [MPiOSAppDelegate managedObjectContextForMainThreadIfReady]; + if (!_mocObserver && mainContext) + _mocObserver = [[NSNotificationCenter defaultCenter] + addObserverForName:NSManagedObjectContextObjectsDidChangeNotification object:mainContext + queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { // Strongify(self); // [self updateMode]; TODO: reload passwords list - }]; - if (!_storeObserver) - _storeObserver = [[NSNotificationCenter defaultCenter] - addObserverForName:USMStoreDidChangeNotification object:nil - queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { -// Strongify(self); -// [self updateMode]; TODO: reload passwords list - }]; + }]; + if (!_storeObserver) + _storeObserver = [[NSNotificationCenter defaultCenter] + addObserverForName:USMStoreDidChangeNotification object:nil + queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { + Strongify(self); + self.fetchedResultsController = nil; + [self updatePasswords]; + }]; } - (void)stopObservingStore { @@ -191,8 +230,49 @@ [[NSNotificationCenter defaultCenter] removeObserver:_storeObserver]; } +- (void)updatePasswords { + + [self.fetchedResultsController.managedObjectContext performBlock:^{ + NSManagedObjectID *activeUserOID = [MPiOSAppDelegate get].activeUserOID; + if (!activeUserOID) + return; + + NSError *error = nil; + self.fetchedResultsController.fetchRequest.predicate = [NSPredicate predicateWithFormat: + @"user == %@ AND name BEGINSWITH[cd] %@", activeUserOID, self.query]; + if (![self.fetchedResultsController performFetch:&error]) + err(@"Couldn't fetch elements: %@", error); + + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ + [self.passwordCollectionView reloadData]; + }]; + }]; +} + #pragma mark - Properties +- (NSString *)query { + + return [self.passwordsSearchBar.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; +} + +- (NSFetchedResultsController *)fetchedResultsController { + + if (!_fetchedResultsController) + [MPiOSAppDelegate managedObjectContextPerformBlockAndWait:^(NSManagedObjectContext *context) { + NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPElementEntity class] )]; + fetchRequest.sortDescriptors = @[ + [[NSSortDescriptor alloc] initWithKey:NSStringFromSelector( @selector(lastUsed) ) ascending:NO] + ]; + fetchRequest.fetchBatchSize = 10; + _fetchedResultsController = [[NSFetchedResultsController alloc] + initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:nil cacheName:nil]; + _fetchedResultsController.delegate = self; + }]; + + return _fetchedResultsController; +} + - (void)setActive:(BOOL)active { [self setActive:active animated:YES]; diff --git a/MasterPassword/ObjC/iOS/MasterPassword-iOS.xcodeproj/project.pbxproj b/MasterPassword/ObjC/iOS/MasterPassword-iOS.xcodeproj/project.pbxproj index 083cddf7..aef87614 100644 --- a/MasterPassword/ObjC/iOS/MasterPassword-iOS.xcodeproj/project.pbxproj +++ b/MasterPassword/ObjC/iOS/MasterPassword-iOS.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 93D392EC39DA43C46C692C12 /* NSDictionary+Indexing.h in Headers */ = {isa = PBXBuildFile; fileRef = 93D393B97158D7BE9332EA53 /* NSDictionary+Indexing.h */; }; 93D3932889B6B4206E66A6D6 /* PearlEMail.h in Headers */ = {isa = PBXBuildFile; fileRef = 93D39F7C9F47BF6387FBC5C3 /* PearlEMail.h */; }; 93D393543ACC701C018C74DA /* PearlUIView.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D393676C32D23A47E27957 /* PearlUIView.m */; }; + 93D394F6D3F6E2553AA0D684 /* MPPasswordStoredCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D3947F6BB69CA9A9124A5D /* MPPasswordStoredCell.m */; }; 93D3954FCE045A3CC7E804B7 /* MPUsersViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D399E571F61E50A9BF8FAF /* MPUsersViewController.m */; }; 93D3957237D303DE2D38C267 /* MPAvatarCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39B381350802A194BF332 /* MPAvatarCell.m */; }; 93D3959643EACF286D0152BA /* PearlUINavigationBar.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39DDDAC305E8ABB4220C7 /* PearlUINavigationBar.m */; }; @@ -27,9 +28,11 @@ 93D39B8F90F58A5D158DDBA3 /* MPPasswordsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D3924EE15017F8A12CB436 /* MPPasswordsViewController.m */; }; 93D39C34FE35830EF5BE1D2A /* NSArray+Indexing.h in Headers */ = {isa = PBXBuildFile; fileRef = 93D396D04E57792A54D437AC /* NSArray+Indexing.h */; }; 93D39C8AD8EAB747856B3A8C /* LLModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D3923B42DA2DA18F287092 /* LLModel.m */; }; + 93D39CB5E2EC1078E898F46A /* MPPasswordCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D3937863061C3916AF7AD2 /* MPPasswordCell.m */; }; 93D39D596A2E376D6F6F5DA1 /* MPCombinedViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D393310223DDB35218467A /* MPCombinedViewController.m */; }; 93D39E281E3658B30550CB55 /* NSDictionary+Indexing.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39AA1EE2E1E7B81372240 /* NSDictionary+Indexing.m */; }; 93D39F8A9254177891F38705 /* MPSetupViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39A28369954D147E239BA /* MPSetupViewController.m */; }; + 93D39FA97F4C3F69A75D5A03 /* MPPasswordGeneratedCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D3993422E207BF0B21D089 /* MPPasswordGeneratedCell.m */; }; DA04E33E14B1E70400ECA4F3 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA04E33D14B1E70400ECA4F3 /* MobileCoreServices.framework */; }; DA095E75172F4CD8001C948B /* MPLogsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D3979190DACEBD1F6AE9F4 /* MPLogsViewController.m */; }; DA2CA4DD18D28859007798F8 /* NSArray+Pearl.m in Sources */ = {isa = PBXBuildFile; fileRef = DA2CA4D918D28859007798F8 /* NSArray+Pearl.m */; }; @@ -510,11 +513,14 @@ 93D3924EE15017F8A12CB436 /* MPPasswordsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPPasswordsViewController.m; sourceTree = ""; }; 93D393310223DDB35218467A /* MPCombinedViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPCombinedViewController.m; sourceTree = ""; }; 93D393676C32D23A47E27957 /* PearlUIView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PearlUIView.m; sourceTree = ""; }; + 93D3937863061C3916AF7AD2 /* MPPasswordCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPPasswordCell.m; sourceTree = ""; }; 93D393B97158D7BE9332EA53 /* NSDictionary+Indexing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+Indexing.h"; sourceTree = ""; }; 93D393BB973253D4BAAC84AA /* PearlEMail.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PearlEMail.m; sourceTree = ""; }; 93D394077F8FAB8167647187 /* Twitter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Twitter.framework; path = System/Library/Frameworks/Twitter.framework; sourceTree = SDKROOT; }; 93D3942A356B639724157982 /* PearlOverlay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PearlOverlay.h; sourceTree = ""; }; + 93D3947F6BB69CA9A9124A5D /* MPPasswordStoredCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPPasswordStoredCell.m; sourceTree = ""; }; 93D3956915634581E737B38C /* PearlNavigationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PearlNavigationController.m; sourceTree = ""; }; + 93D395BA6B2CFF5F49A4D25F /* MPPasswordStoredCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPPasswordStoredCell.h; sourceTree = ""; }; 93D396D04E57792A54D437AC /* NSArray+Indexing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+Indexing.h"; sourceTree = ""; }; 93D3971FE104BB4052484151 /* MPUsersViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPUsersViewController.h; sourceTree = ""; }; 93D39730673227EFF6DEFF19 /* MPSetupViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPSetupViewController.h; sourceTree = ""; }; @@ -522,6 +528,7 @@ 93D3983278751A530262F64E /* LLConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LLConfig.h; sourceTree = ""; }; 93D398567FD02DB2647B8CF3 /* PearlNavigationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PearlNavigationController.h; sourceTree = ""; }; 93D398C95847261903D781D3 /* NSError+PearlFullDescription.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSError+PearlFullDescription.h"; sourceTree = ""; }; + 93D3993422E207BF0B21D089 /* MPPasswordGeneratedCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPPasswordGeneratedCell.m; sourceTree = ""; }; 93D399E571F61E50A9BF8FAF /* MPUsersViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPUsersViewController.m; sourceTree = ""; }; 93D39A28369954D147E239BA /* MPSetupViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPSetupViewController.m; sourceTree = ""; }; 93D39A3CC4D8330831FC8CB4 /* LLToggleViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LLToggleViewController.h; sourceTree = ""; }; @@ -530,9 +537,11 @@ 93D39BA6C5CB452973918B7D /* LLButtonView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LLButtonView.h; sourceTree = ""; }; 93D39BF6BCBDFFE844E7D34C /* LLButtonView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LLButtonView.m; sourceTree = ""; }; 93D39C8E26B06F01566785B7 /* LLToggleViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LLToggleViewController.m; sourceTree = ""; }; + 93D39CE1138FDA4D3D1B847A /* MPPasswordGeneratedCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPPasswordGeneratedCell.h; sourceTree = ""; }; 93D39CF8ADF4542CDC4CD385 /* MPCombinedViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPCombinedViewController.h; sourceTree = ""; }; 93D39DA27D768B53C8B1330C /* MPAvatarCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPAvatarCell.h; sourceTree = ""; }; 93D39DDDAC305E8ABB4220C7 /* PearlUINavigationBar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PearlUINavigationBar.m; sourceTree = ""; }; + 93D39E02F69CACAB61C056F8 /* MPPasswordCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPPasswordCell.h; sourceTree = ""; }; 93D39F7C9F47BF6387FBC5C3 /* PearlEMail.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PearlEMail.h; sourceTree = ""; }; 93D39F9106F2CCFB94283188 /* NSError+PearlFullDescription.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSError+PearlFullDescription.m"; sourceTree = ""; }; DA04E33D14B1E70400ECA4F3 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; @@ -1682,6 +1691,10 @@ 93D3971FE104BB4052484151 /* MPUsersViewController.h */, 93D393676C32D23A47E27957 /* PearlUIView.m */, 93D39083C93D90C4B94541AD /* PearlUIView.h */, + 93D39E02F69CACAB61C056F8 /* MPPasswordCell.h */, + 93D3937863061C3916AF7AD2 /* MPPasswordCell.m */, + 93D395BA6B2CFF5F49A4D25F /* MPPasswordStoredCell.h */, + 93D3947F6BB69CA9A9124A5D /* MPPasswordStoredCell.m */, ); sourceTree = ""; }; @@ -2539,6 +2552,8 @@ 93D39DA27D768B53C8B1330C /* MPAvatarCell.h */, 93D39DDDAC305E8ABB4220C7 /* PearlUINavigationBar.m */, 93D390EEC85E94D9C888643F /* PearlUINavigationBar.h */, + 93D3993422E207BF0B21D089 /* MPPasswordGeneratedCell.m */, + 93D39CE1138FDA4D3D1B847A /* MPPasswordGeneratedCell.h */, ); path = iOS; sourceTree = ""; @@ -3843,6 +3858,9 @@ 93D3954FCE045A3CC7E804B7 /* MPUsersViewController.m in Sources */, 93D3959643EACF286D0152BA /* PearlUINavigationBar.m in Sources */, 93D393543ACC701C018C74DA /* PearlUIView.m in Sources */, + 93D39CB5E2EC1078E898F46A /* MPPasswordCell.m in Sources */, + 93D39FA97F4C3F69A75D5A03 /* MPPasswordGeneratedCell.m in Sources */, + 93D394F6D3F6E2553AA0D684 /* MPPasswordStoredCell.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MasterPassword/ObjC/iOS/Storyboard.storyboard b/MasterPassword/ObjC/iOS/Storyboard.storyboard index a8475caf..0be711be 100644 --- a/MasterPassword/ObjC/iOS/Storyboard.storyboard +++ b/MasterPassword/ObjC/iOS/Storyboard.storyboard @@ -19,7 +19,7 @@ - + @@ -391,15 +391,10 @@ - + - - - - Title - Title - + @@ -409,28 +404,31 @@ - + - - + + - + - - + + + + + @@ -526,23 +533,34 @@ + + + + + + + + - - + + - + - - + + + + + @@ -605,6 +629,12 @@ + + + + + + @@ -613,12 +643,15 @@ - + + + + - + - + + + + @@ -745,6 +784,11 @@ + + + + + @@ -753,12 +797,15 @@ - + + + + + + + + + + + + + + @@ -1036,20 +1102,11 @@ + - - - - - - - - - -