diff --git a/External/LoveLyndir b/External/LoveLyndir
index 97eafd9b..0b858644 160000
--- a/External/LoveLyndir
+++ b/External/LoveLyndir
@@ -1 +1 @@
-Subproject commit 97eafd9b59f84bd9e3fc3cb4313df8b3c034e766
+Subproject commit 0b858644cb58c357e055b41c2747ad9d45fa9ce3
diff --git a/External/Pearl b/External/Pearl
index 575b409c..67321728 160000
--- a/External/Pearl
+++ b/External/Pearl
@@ -1 +1 @@
-Subproject commit 575b409cca36eabfaacf0a963ed259454cb8ec66
+Subproject commit 673217287de2920d21ef84f1d6811d8250f187b4
diff --git a/External/Reveal.framework/Versions/A/Headers/IBARevealLogger.h b/External/Reveal.framework/Versions/A/Headers/IBARevealLogger.h
index 92193c71..a2dd329c 100644
--- a/External/Reveal.framework/Versions/A/Headers/IBARevealLogger.h
+++ b/External/Reveal.framework/Versions/A/Headers/IBARevealLogger.h
@@ -7,7 +7,7 @@ CF_EXTERN_C_BEGIN
/*!
\brief The Reveal Log level bit flags.
- \discussion These flags are addative. Ie, you should bitwise OR them together.
+ \discussion These flags are additive. i.e. you should bitwise OR them together.
\seealso IBARevealLoggerSetLevelMask
\seealso IBARevealLoggerGetLevelMask
diff --git a/External/Reveal.framework/Versions/A/Reveal b/External/Reveal.framework/Versions/A/Reveal
index 0ae41f5e..8ef14875 100644
Binary files a/External/Reveal.framework/Versions/A/Reveal and b/External/Reveal.framework/Versions/A/Reveal differ
diff --git a/MasterPassword.xcworkspace/xcshareddata/MasterPassword.xccheckout b/MasterPassword.xcworkspace/xcshareddata/MasterPassword.xccheckout
index 69d287bd..663349ee 100644
--- a/MasterPassword.xcworkspace/xcshareddata/MasterPassword.xccheckout
+++ b/MasterPassword.xcworkspace/xcshareddata/MasterPassword.xccheckout
@@ -5,13 +5,11 @@
IDESourceControlProjectFavoriteDictionaryKey
IDESourceControlProjectIdentifier
- D4AB9F0C-D746-4319-AABF-B24705099AED
+ 3D68B2F1-988A-48C3-8450-B37D43BDFE92
IDESourceControlProjectName
MasterPassword
IDESourceControlProjectOriginsDictionary
- 2B6DA448-3730-4F84-B2C3-51272E0D42F3
- ssh://github.com/lhunath/DCIntrospect.git
5263993D-5FE8-464F-B66E-B0F7C2DFF410
ssh://github.com/lhunath/UbiquityStoreManager.git
6A449EC2-A2A3-4635-9C5F-A811E011EAC3
@@ -24,8 +22,10 @@
git://github.com/lhunath/uicolor-utilities.git
B0F634DD-AEE1-4F0D-AE35-4FAF51AD1B5A
git://github.com/lhunath/RHStatusItemView.git
- CBA93B91-B799-4CC6-85B6-749792B76DD4
- ssh://github.com/lhunath/InAppSettingsKit.git
+ CDDE92CF-0136-4DE0-8318-80EDB5C8CAF9
+ git://github.com/lhunath/InAppSettingsKit.git
+ D5CE8AB8-2F69-4A08-A2CE-93C70E0F0567
+ https://github.com/lhunath/DCIntrospect.git
E4C8E206-229C-4DA8-A130-0C544DEC7E07
git://github.com/jonmarimba/jrswizzle.git
FF42A9E0-F41C-42FC-88CD-F2CCDE15DBB6
@@ -35,8 +35,6 @@
MasterPassword.xcworkspace
IDESourceControlProjectRelativeInstallPathDictionary
- 2B6DA448-3730-4F84-B2C3-51272E0D42F3
- ../External/DCIntrospect
5263993D-5FE8-464F-B66E-B0F7C2DFF410
../External/UbiquityStoreManager
6A449EC2-A2A3-4635-9C5F-A811E011EAC3
@@ -49,8 +47,10 @@
../External/Pearl/External/uicolor-utilities
B0F634DD-AEE1-4F0D-AE35-4FAF51AD1B5A
../External/RHStatusItemView
- CBA93B91-B799-4CC6-85B6-749792B76DD4
+ CDDE92CF-0136-4DE0-8318-80EDB5C8CAF9
../External/InAppSettingsKit
+ D5CE8AB8-2F69-4A08-A2CE-93C70E0F0567
+ ../External/DCIntrospect
E4C8E206-229C-4DA8-A130-0C544DEC7E07
../External/Pearl/External/jrswizzle
FF42A9E0-F41C-42FC-88CD-F2CCDE15DBB6
@@ -68,7 +68,7 @@
IDESourceControlRepositoryExtensionIdentifierKey
public.vcs.git
IDESourceControlWCCIdentifierKey
- 2B6DA448-3730-4F84-B2C3-51272E0D42F3
+ D5CE8AB8-2F69-4A08-A2CE-93C70E0F0567
IDESourceControlWCCName
DCIntrospect
@@ -84,7 +84,7 @@
IDESourceControlRepositoryExtensionIdentifierKey
public.vcs.git
IDESourceControlWCCIdentifierKey
- CBA93B91-B799-4CC6-85B6-749792B76DD4
+ CDDE92CF-0136-4DE0-8318-80EDB5C8CAF9
IDESourceControlWCCName
InAppSettingsKit
diff --git a/MasterPassword/ObjC/iOS/MPAvatarCell.h b/MasterPassword/ObjC/iOS/MPAvatarCell.h
index a3ff62cb..f1d0ac9e 100644
--- a/MasterPassword/ObjC/iOS/MPAvatarCell.h
+++ b/MasterPassword/ObjC/iOS/MPAvatarCell.h
@@ -26,15 +26,21 @@ extern const long MPAvatarAdd;
typedef NS_ENUM(NSUInteger, MPAvatarMode) {
MPAvatarModeLowered,
MPAvatarModeRaisedButInactive,
- MPAvatarModeRaisedAndActive
+ MPAvatarModeRaisedAndActive,
+ MPAvatarModeRaisedAndHidden,
+ MPAvatarModeRaisedAndMinimized,
};
@interface MPAvatarCell : UICollectionViewCell
@property (copy, nonatomic) NSString *name;
@property (assign, nonatomic) long avatar;
@property (assign, nonatomic) MPAvatarMode mode;
-@property (assign, nonatomic) float visibility;
+@property (assign, nonatomic) CGFloat visibility;
+@property (assign, nonatomic) BOOL spinnerActive;
+ (NSString *)reuseIdentifier;
+- (void)setVisibility:(CGFloat)visibility animated:(BOOL)animated;
+- (void)setMode:(MPAvatarMode)mode animated:(BOOL)animated;
+
@end
diff --git a/MasterPassword/ObjC/iOS/MPAvatarCell.m b/MasterPassword/ObjC/iOS/MPAvatarCell.m
index 523862b6..d1e44f94 100644
--- a/MasterPassword/ObjC/iOS/MPAvatarCell.m
+++ b/MasterPassword/ObjC/iOS/MPAvatarCell.m
@@ -25,7 +25,10 @@ const long MPAvatarAdd = 10000;
@property(strong, nonatomic) IBOutlet UIImageView *avatarImageView;
@property(strong, nonatomic) IBOutlet UILabel *nameLabel;
@property(strong, nonatomic) IBOutlet UIView *nameContainer;
+@property(strong, nonatomic) IBOutlet UIImageView *spinner;
@property(strong, nonatomic) IBOutlet NSLayoutConstraint *nameCenterConstraint;
+@property(strong, nonatomic) IBOutlet NSLayoutConstraint *avatarSizeConstraint;
+@property(strong, nonatomic) IBOutlet NSLayoutConstraint *avatarTopConstraint;
@end
@@ -37,6 +40,8 @@ const long MPAvatarAdd = 10000;
return @"MPAvatarCell";
}
+#pragma mark - Life cycle
+
- (void)awakeFromNib {
[super awakeFromNib];
@@ -45,28 +50,46 @@ const long MPAvatarAdd = 10000;
self.avatarImageView.hidden = NO;
self.avatarImageView.layer.cornerRadius = self.avatarImageView.bounds.size.height / 2;
- self.avatarImageView.layer.shadowColor = [UIColor blackColor].CGColor;
- self.avatarImageView.layer.shadowOpacity = 1;
- self.avatarImageView.layer.shadowRadius = 15;
self.avatarImageView.layer.masksToBounds = NO;
self.avatarImageView.backgroundColor = [UIColor clearColor];
[self observeKeyPath:@"selected" withBlock:^(id from, id to, NSKeyValueChange cause, id _self) {
- [_self onSelectedOrHighlighted];
+ [_self updateAnimated:YES];
}];
[self observeKeyPath:@"highlighted" withBlock:^(id from, id to, NSKeyValueChange cause, id _self) {
- [_self onSelectedOrHighlighted];
+ [_self updateAnimated:YES];
}];
- self.visibility = 0;
- self.mode = MPAvatarModeLowered;
+ CABasicAnimation *toShadowOpacityAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"];
+ toShadowOpacityAnimation.toValue = @0.2f;
+ toShadowOpacityAnimation.duration = 0.5f;
+
+ CABasicAnimation *pulseShadowOpacityAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"];
+ pulseShadowOpacityAnimation.fromValue = @0.2f;
+ pulseShadowOpacityAnimation.toValue = @0.6f;
+ pulseShadowOpacityAnimation.beginTime = 0.5f;
+ pulseShadowOpacityAnimation.duration = 2.0f;
+ pulseShadowOpacityAnimation.autoreverses = YES;
+ pulseShadowOpacityAnimation.repeatCount = MAXFLOAT;
+
+ CAAnimationGroup *group = [CAAnimationGroup new];
+ group.animations = @[ toShadowOpacityAnimation, pulseShadowOpacityAnimation ];
+ group.duration = MAXFLOAT;
+ [self.avatarImageView.layer addAnimation:group forKey:@"targetedShadow"];
+ self.avatarImageView.layer.shadowColor = [UIColor whiteColor].CGColor;
+ self.avatarImageView.layer.shadowOffset = CGSizeZero;
+
+ [self setVisibility:0 animated:NO];
+ [self setMode:MPAvatarModeLowered animated:NO];
}
-- (void)onSelectedOrHighlighted {
+- (void)dealloc {
- self.avatarImageView.backgroundColor = self.selected || self.highlighted? self.avatarImageView.tintColor: [UIColor clearColor];
+ [self removeKeyPathObservers];
}
+#pragma mark - Properties
+
- (void)setAvatar:(long)avatar {
_avatar = avatar;
@@ -87,11 +110,16 @@ const long MPAvatarAdd = 10000;
self.nameLabel.text = name;
}
-- (void)setVisibility:(float)visibility {
+- (void)setVisibility:(CGFloat)visibility {
+
+ [self setVisibility:visibility animated:YES];
+}
+
+- (void)setVisibility:(CGFloat)visibility animated:(BOOL)animated {
_visibility = visibility;
- self.nameContainer.alpha = visibility;
+ [self updateAnimated:animated];
}
- (void)setHighlighted:(BOOL)highlighted {
@@ -105,35 +133,117 @@ const long MPAvatarAdd = 10000;
- (void)setMode:(MPAvatarMode)mode {
+ [self setMode:mode animated:YES];
+}
+
+- (void)setMode:(MPAvatarMode)mode animated:(BOOL)animated {
+
_mode = mode;
- [UIView animateWithDuration:0.2f animations:^{
+ [self updateAnimated:animated];
+}
+
+- (void)setSpinnerActive:(BOOL)spinnerActive {
+
+ [self setSpinnerActive:spinnerActive animated:YES];
+}
+
+- (void)setSpinnerActive:(BOOL)spinnerActive animated:(BOOL)animated {
+
+ _spinnerActive = spinnerActive;
+
+ CABasicAnimation *rotate = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
+ rotate.toValue = [NSNumber numberWithDouble:2 * M_PI];
+ rotate.duration = 5.0;
+
+ if (spinnerActive) {
+ rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
+ rotate.fromValue = @0.0;
+ rotate.repeatCount = MAXFLOAT;
+ }
+ else {
+ rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
+ rotate.repeatCount = 1;
+ }
+
+ [self.spinner.layer removeAnimationForKey:@"rotation"];
+ [self.spinner.layer addAnimation:rotate forKey:@"rotation"];
+
+ [self updateAnimated:animated];
+}
+
+#pragma mark - Private
+
+- (void)updateAnimated:(BOOL)animated {
+
+ [UIView animateWithDuration:animated? 0.2f: 0 animations:^{
self.avatarImageView.transform = CGAffineTransformIdentity;
}];
- [UIView animateWithDuration:0.3f animations:^{
- switch (mode) {
+ [UIView animateWithDuration:animated? 0.3f: 0 animations:^{
+ switch (self.mode) {
case MPAvatarModeLowered: {
+ self.avatarSizeConstraint.constant = self.avatarImageView.image.size.height;
+ self.avatarTopConstraint.priority = UILayoutPriorityDefaultLow;
self.nameCenterConstraint.priority = UILayoutPriorityDefaultLow;
+ self.nameContainer.alpha = self.visibility;
self.nameContainer.backgroundColor = [UIColor clearColor];
- self.avatarImageView.alpha = 1;
+ self.avatarImageView.alpha = self.visibility / 0.7f + 0.3f;
+ self.avatarImageView.layer.shadowRadius = 15 * self.visibility * self.visibility;
break;
}
case MPAvatarModeRaisedButInactive: {
+ self.avatarSizeConstraint.constant = self.avatarImageView.image.size.height;
+ self.avatarTopConstraint.priority = UILayoutPriorityDefaultLow;
self.nameCenterConstraint.priority = UILayoutPriorityDefaultLow;
+ self.nameContainer.alpha = self.visibility;
self.nameContainer.backgroundColor = [UIColor clearColor];
- self.avatarImageView.alpha = 0.3f;
+ self.avatarImageView.alpha = 0;
+ self.avatarImageView.layer.shadowRadius = 15 * self.visibility * self.visibility;
break;
}
case MPAvatarModeRaisedAndActive: {
+ self.avatarSizeConstraint.constant = self.avatarImageView.image.size.height;
+ self.avatarTopConstraint.priority = UILayoutPriorityDefaultLow;
self.nameCenterConstraint.priority = UILayoutPriorityDefaultHigh;
+ self.nameContainer.alpha = self.visibility;
self.nameContainer.backgroundColor = [UIColor blackColor];
self.avatarImageView.alpha = 1;
+ self.avatarImageView.layer.shadowRadius = 15 * self.visibility * self.visibility;
+ break;
+ }
+ case MPAvatarModeRaisedAndHidden: {
+ self.avatarSizeConstraint.constant = self.avatarImageView.image.size.height;
+ self.avatarTopConstraint.priority = UILayoutPriorityDefaultLow;
+ self.nameCenterConstraint.priority = UILayoutPriorityDefaultHigh;
+ self.nameContainer.alpha = 0;
+ self.nameContainer.backgroundColor = [UIColor blackColor];
+ self.avatarImageView.alpha = 0;
+ self.avatarImageView.layer.shadowRadius = 15 * self.visibility * self.visibility;
+ break;
+ }
+ case MPAvatarModeRaisedAndMinimized: {
+ self.avatarSizeConstraint.constant = 36;
+ self.avatarTopConstraint.priority = UILayoutPriorityDefaultHigh;
+ self.nameCenterConstraint.priority = UILayoutPriorityDefaultHigh;
+ self.nameContainer.alpha = 0;
+ self.nameContainer.backgroundColor = [UIColor blackColor];
+ self.avatarImageView.alpha = 1;
+ self.avatarImageView.layer.shadowOpacity = 0;
break;
}
}
-
+ [self.avatarSizeConstraint apply];
+ [self.avatarTopConstraint apply];
[self.nameCenterConstraint apply];
+
+ // Avatar selection and spinner.
+ if (self.mode != MPAvatarModeRaisedAndMinimized && (self.selected || self.highlighted) && !self.spinnerActive)
+ self.avatarImageView.backgroundColor = self.avatarImageView.tintColor;
+ else
+ self.avatarImageView.backgroundColor = [UIColor clearColor];
+ self.avatarImageView.layer.cornerRadius = self.avatarImageView.bounds.size.height / 2;
+ self.spinner.alpha = self.spinnerActive? 1: 0;
}];
}
diff --git a/MasterPassword/ObjC/iOS/MPCombinedViewController.h b/MasterPassword/ObjC/iOS/MPCombinedViewController.h
index 00363762..cec775b7 100644
--- a/MasterPassword/ObjC/iOS/MPCombinedViewController.h
+++ b/MasterPassword/ObjC/iOS/MPCombinedViewController.h
@@ -16,34 +16,18 @@
// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
//
-#import "LLGitTip.h"
-
typedef NS_ENUM(NSUInteger, MPCombinedMode) {
MPCombinedModeUserSelection,
MPCombinedModePasswordSelection,
};
-@interface MPCombinedViewController : UIViewController
+@interface MPCombinedViewController : UIViewController
+
+@property (strong, nonatomic) IBOutlet UIView *usersView;
+@property (strong, nonatomic) IBOutlet UIView *passwordsView;
@property(assign, nonatomic) MPCombinedMode mode;
-#pragma mark - UserSelection
-
-@property(strong, nonatomic) IBOutlet UIView *userSelectionContainer;
-@property(weak, nonatomic) IBOutlet UILabel *hintLabel;
-@property(weak, nonatomic) IBOutlet UIView *gitTipTip;
-@property(weak, nonatomic) IBOutlet LLGitTip *gitTipButton;
-@property(weak, nonatomic) IBOutlet UITextField *entryField;
-@property(weak, nonatomic) IBOutlet UILabel *entryLabel;
-@property(weak, nonatomic) IBOutlet UIView *entryContainer;
-@property(weak, nonatomic) IBOutlet UICollectionView *avatarCollectionView;
-@property (strong, nonatomic) IBOutlet NSLayoutConstraint *avatarCollectionCenterConstraint;
-
-#pragma mark - PasswordSelection
-
-@property(strong, nonatomic) IBOutlet UIView *passwordSelectionContainer;
-@property(strong, nonatomic) IBOutlet UICollectionView *passwordCollectionView;
-
- (IBAction)doSignOut:(UIBarButtonItem *)sender;
@end
diff --git a/MasterPassword/ObjC/iOS/MPCombinedViewController.m b/MasterPassword/ObjC/iOS/MPCombinedViewController.m
index 3b77a746..b365e918 100644
--- a/MasterPassword/ObjC/iOS/MPCombinedViewController.m
+++ b/MasterPassword/ObjC/iOS/MPCombinedViewController.m
@@ -17,45 +17,35 @@
//
#import "MPCombinedViewController.h"
-#import "MPEntities.h"
-#import "MPAvatarCell.h"
#import "MPiOSAppDelegate.h"
#import "MPAppDelegate_Store.h"
#import "MPAppDelegate_Key.h"
-
-typedef NS_ENUM(NSUInteger, MPActiveUserState) {
- MPActiveUserStateNone,
- MPActiveUserStateLogin,
- MPActiveUserStateUserName,
- MPActiveUserStateMasterPasswordChoice,
- MPActiveUserStateMasterPasswordConfirmation,
-};
+#import "MPUsersViewController.h"
+#import "MPPasswordsViewController.h"
@interface MPCombinedViewController()
-@property(nonatomic) MPActiveUserState activeUserState;
-@property(nonatomic, strong) NSArray *userIDs;
+@property(strong, nonatomic) IBOutlet NSLayoutConstraint *passwordsTopConstraint;
+@property(nonatomic, strong) MPUsersViewController *usersVC;
+@property(nonatomic, strong) MPPasswordsViewController *passwordsVC;
@end
@implementation MPCombinedViewController {
- __weak id _storeObserver;
- __weak id _mocObserver;
NSArray *_notificationObservers;
- NSString *_masterPasswordChoice;
}
- (void)viewDidLoad {
[super viewDidLoad];
- self.avatarCollectionView.allowsMultipleSelection = YES;
+ [self setMode:MPCombinedModeUserSelection animated:NO];
+}
- [self observeKeyPath:@"avatarCollectionView.contentOffset" withBlock:
- ^(id from, id to, NSKeyValueChange cause, MPCombinedViewController *_self) {
- [_self updateAvatars];
- }];
+- (void)viewWillAppear:(BOOL)animated {
- self.mode = MPCombinedModeUserSelection;
+ [super viewWillAppear:animated];
+
+ [[self navigationController] setNavigationBarHidden:YES animated:animated];
}
- (void)viewDidAppear:(BOOL)animated {
@@ -63,7 +53,6 @@ typedef NS_ENUM(NSUInteger, MPActiveUserState) {
[super viewDidAppear:animated];
[self registerObservers];
- [self updateMode];
}
- (void)viewWillDisappear:(BOOL)animated {
@@ -71,353 +60,60 @@ typedef NS_ENUM(NSUInteger, MPActiveUserState) {
[super viewWillDisappear:animated];
[self removeObservers];
- [self needStoreObserved:NO];
}
-#pragma mark - UITextFieldDelegate
+- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
-- (void)textFieldDidEndEditing:(UITextField *)textField {
+ if ([segue.identifier isEqualToString:@"users"])
+ self.usersVC = segue.destinationViewController;
+ if ([segue.identifier isEqualToString:@"passwords"])
+ self.passwordsVC = segue.destinationViewController;
}
-- (BOOL)textFieldShouldReturn:(UITextField *)textField {
+- (UIStatusBarStyle)preferredStatusBarStyle {
- if (textField == self.entryField) {
- switch (self.activeUserState) {
- case MPActiveUserStateNone: {
- [textField resignFirstResponder];
+ return UIStatusBarStyleLightContent;
+}
+
+
+#pragma mark - Properties
+
+- (void)setMode:(MPCombinedMode)mode {
+
+ [self setMode:mode animated:YES];
+}
+
+- (void)setMode:(MPCombinedMode)mode animated:(BOOL)animated {
+
+ _mode = mode;
+
+ [self becomeFirstResponder];
+
+ [UIView animateWithDuration:animated? 0.3f: 0 animations:^{
+ switch (self.mode) {
+ case MPCombinedModeUserSelection: {
+ [self.usersVC setActive:YES animated:NO];
+ [self.passwordsVC setActive:NO animated:NO];
+// MPUsersViewController *usersVC = [self.storyboard instantiateViewControllerWithIdentifier:@"MPUsersViewController"];
+// [self setViewControllers:@[ usersVC ] direction:UIPageViewControllerNavigationDirectionReverse
+// animated:animated completion:nil];
break;
}
- case MPActiveUserStateLogin: {
- [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
- BOOL signedIn = NO, isNew = NO;
- MPUserEntity *user = [self selectedUserInContext:context isNew:&isNew];
- if (!isNew && user)
- signedIn = [[MPiOSAppDelegate get] signInAsUser:user saveInContext:context
- usingMasterPassword:self.entryField.text];
-
- [[NSOperationQueue mainQueue] addOperationWithBlock:^{
- if (!signedIn) {
- // Sign in failed.
- // TODO: warn user
- return;
- }
- }];
- }];
- break;
- }
- case MPActiveUserStateUserName: {
- NSString *userName = self.entryField.text;
- if (![userName length]) {
- // No name entered.
- // TODO: warn user
- return NO;
- }
-
- [self selectedAvatar].name = userName;
- self.activeUserState = MPActiveUserStateMasterPasswordChoice;
- break;
- }
- case MPActiveUserStateMasterPasswordChoice: {
- NSString *masterPassword = self.entryField.text;
- if (![masterPassword length]) {
- // No password entered.
- // TODO: warn user
- return NO;
- }
-
- self.activeUserState = MPActiveUserStateMasterPasswordConfirmation;
- break;
- }
- case MPActiveUserStateMasterPasswordConfirmation: {
- NSString *masterPassword = self.entryField.text;
- if (![masterPassword length]) {
- // No password entered.
- // TODO: warn user
- return NO;
- }
-
- if (![masterPassword isEqualToString:_masterPasswordChoice]) {
- // Master password confirmation failed.
- // TODO: warn user
- self.activeUserState = MPActiveUserStateMasterPasswordChoice;
- return NO;
- }
-
- [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
- BOOL isNew = NO;
- MPUserEntity *user = [self selectedUserInContext:context isNew:&isNew];
- if (isNew) {
- user = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass( [MPUserEntity class] )
- inManagedObjectContext:context];
- MPAvatarCell *avatarCell = [self selectedAvatar];
- user.avatar = avatarCell.avatar;
- user.name = avatarCell.name;
- }
-
- BOOL signedIn = [[MPiOSAppDelegate get] signInAsUser:user saveInContext:context usingMasterPassword:masterPassword];
- [[NSOperationQueue mainQueue] addOperationWithBlock:^{
- if (!signedIn) {
- // Sign in failed, shouldn't happen for a new user.
- // TODO: warn user
- self.activeUserState = MPActiveUserStateNone;
- return;
- }
- }];
- }];
-
+ case MPCombinedModePasswordSelection: {
+ [self.usersVC setActive:NO animated:NO];
+ [self.passwordsVC setActive:YES animated:NO];
+// MPPasswordsViewController *passwordsVC = [self.storyboard instantiateViewControllerWithIdentifier:@"MPPasswordsViewController"];
+// [self setViewControllers:@[ passwordsVC ] direction:UIPageViewControllerNavigationDirectionForward
+// animated:animated completion:nil];
break;
}
}
- }
- return NO;
+ [self.passwordsTopConstraint apply];
+ }];
}
-// This isn't really in UITextFieldDelegate. We fake it from UITextFieldTextDidChangeNotification.
-- (void)textFieldDidChange:(UITextField *)textField {
-
- if (textField == self.entryField) {
- switch (self.activeUserState) {
- case MPActiveUserStateNone:
- break;
- case MPActiveUserStateLogin:
- break;
- case MPActiveUserStateUserName: {
- NSString *userName = self.entryField.text;
- [self selectedAvatar].name = [userName length]? userName: strl( @"New User" );
- break;
- }
- case MPActiveUserStateMasterPasswordChoice:
- break;
- case MPActiveUserStateMasterPasswordConfirmation:
- break;
- }
- }
-}
-
-#pragma mark - UICollectionViewDataSource
-
-- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
-
- if (collectionView == self.avatarCollectionView)
- return [self.userIDs count] + 1;
-
- else if (collectionView == self.passwordCollectionView)
- return 0;
-
- Throw(@"unexpected collection view: %@", collectionView);
-}
-
-- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
-
- if (collectionView == self.avatarCollectionView) {
- MPAvatarCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:[MPAvatarCell reuseIdentifier] forIndexPath:indexPath];
- [cell addGestureRecognizer:[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(didLongPress:)]];
- [self updateAvatar:cell atIndexPath:indexPath];
-
- BOOL isNew = NO;
- MPUserEntity *user = [self userForIndexPath:indexPath inContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]
- isNew:&isNew];
- if (isNew) {
- // New User
- cell.avatar = MPAvatarAdd;
- cell.name = strl( @"New User" );
- }
- else {
- // Existing User
- cell.avatar = user.avatar;
- cell.name = user.name;
- }
-
- NSArray *selectedIndexPaths = [self.avatarCollectionView indexPathsForSelectedItems];
- if (![selectedIndexPaths count])
- cell.mode = MPAvatarModeLowered;
- else if ([selectedIndexPaths containsObject:indexPath])
- cell.mode = MPAvatarModeRaisedAndActive;
- else
- cell.mode = MPAvatarModeRaisedButInactive;
-
- return cell;
- }
-
- else if (collectionView == self.passwordCollectionView)
- return nil;
-
- Throw(@"unexpected collection view: %@", collectionView);
-}
-
-- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
-
- if (collectionView == self.avatarCollectionView) {
- [self.avatarCollectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally
- animated:YES];
-
- [UIView animateWithDuration:0.3f animations:^{
- for (NSUInteger otherItem = 0; otherItem < [collectionView numberOfItemsInSection:indexPath.section]; ++otherItem)
- if (otherItem != indexPath.item) {
- NSIndexPath *otherIndexPath = [NSIndexPath indexPathForItem:otherItem inSection:indexPath.section];
- [collectionView deselectItemAtIndexPath:otherIndexPath animated:YES];
-
- MPAvatarCell *otherCell = (MPAvatarCell *)[self.avatarCollectionView cellForItemAtIndexPath:otherIndexPath];
- otherCell.mode = MPAvatarModeRaisedButInactive;
- }
-
- MPAvatarCell *cell = (MPAvatarCell *)[self.avatarCollectionView cellForItemAtIndexPath:indexPath];
- cell.mode = MPAvatarModeRaisedAndActive;
- }];
-
- BOOL isNew = NO;
- MPUserEntity *user = [self userForIndexPath:indexPath inContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]
- isNew:&isNew];
- if (isNew)
- self.activeUserState = MPActiveUserStateUserName;
- else if (!user.keyID)
- self.activeUserState = MPActiveUserStateMasterPasswordChoice;
- else
- self.activeUserState = MPActiveUserStateLogin;
- }
-}
-
-- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath {
-
- if (collectionView == self.avatarCollectionView) {
- self.activeUserState = MPActiveUserStateNone;
- }
-}
-
-#pragma mark - UILongPressGestureRecognizer
-
-- (void)didLongPress:(UILongPressGestureRecognizer *)recognizer {
-
- if ([recognizer.view isKindOfClass:[MPAvatarCell class]]) {
- if (recognizer.state != UIGestureRecognizerStateBegan)
- // Don't show the action menu unless the state is Began.
- return;
-
- MPAvatarCell *avatarCell = (MPAvatarCell *)recognizer.view;
- NSManagedObjectContext *mainContext = [MPiOSAppDelegate managedObjectContextForMainThreadIfReady];
-
- BOOL isNew = NO;
- MPUserEntity *user = [self userForAvatar:avatarCell inContext:mainContext isNew:&isNew];
- NSManagedObjectID *userID = user.objectID;
- if (isNew || !user)
- return;
-
- [PearlSheet showSheetWithTitle:user.name
- viewStyle:UIActionSheetStyleBlackTranslucent
- initSheet:nil tappedButtonBlock:^(UIActionSheet *sheet, NSInteger buttonIndex) {
- if (buttonIndex == [sheet cancelButtonIndex])
- return;
-
- if (buttonIndex == [sheet destructiveButtonIndex]) {
- // Delete User
- [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
- NSManagedObject *user_ = [context existingObjectWithID:userID error:NULL];
- if (user_) {
- [context deleteObject:user_];
- [context saveToStore];
- }
- }];
- return;
- }
-
- if (buttonIndex == [sheet firstOtherButtonIndex])
- // Reset Password
- [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
- MPUserEntity *user_ = (MPUserEntity *)[context existingObjectWithID:userID error:NULL];
- if (user_)
- [[MPiOSAppDelegate get] changeMasterPasswordFor:user_ saveInContext:context didResetBlock:^{
- dbg(@"changing mp for user: %@, keyID: %@", user_.name, user_.keyID);
- [[NSOperationQueue mainQueue] addOperationWithBlock:^{
- NSIndexPath *avatarIndexPath = [self.avatarCollectionView indexPathForCell:avatarCell];
- [self.avatarCollectionView selectItemAtIndexPath:avatarIndexPath animated:NO
- scrollPosition:UICollectionViewScrollPositionNone];
- [self collectionView:self.avatarCollectionView didSelectItemAtIndexPath:avatarIndexPath];
- }];
- }];
- }];
- } cancelTitle:[PearlStrings get].commonButtonCancel
- destructiveTitle:@"Delete User" otherTitles:@"Reset Password", nil];
- }
-}
-
-#pragma mark - UIScrollViewDelegate
-
-- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity
- targetContentOffset:(inout CGPoint *)targetContentOffset {
-
- if (scrollView == self.avatarCollectionView) {
- CGPoint offsetToCenter = CGPointMake(
- self.avatarCollectionView.bounds.size.width / 2,
- self.avatarCollectionView.bounds.size.height / 2 );
- NSIndexPath *avatarIndexPath = [self.avatarCollectionView indexPathForItemAtPoint:
- CGPointPlusCGPoint( *targetContentOffset, offsetToCenter )];
- CGPoint targetCenter = [self.avatarCollectionView layoutAttributesForItemAtIndexPath:avatarIndexPath].center;
- *targetContentOffset = CGPointMinusCGPoint( targetCenter, offsetToCenter );
- NSAssert([self.avatarCollectionView indexPathForItemAtPoint:targetCenter].item == avatarIndexPath.item, @"should be same item");
- }
-}
-
-- (MPAvatarCell *)selectedAvatar {
-
- NSArray *selectedIndexPaths = self.avatarCollectionView.indexPathsForSelectedItems;
- if (![selectedIndexPaths count]) {
- // No selected user.
- return nil;
- }
-
- return (MPAvatarCell *)[self.avatarCollectionView cellForItemAtIndexPath:selectedIndexPaths.firstObject];
-}
-
-- (MPUserEntity *)selectedUserInContext:(NSManagedObjectContext *)context isNew:(BOOL *)isNew {
-
- MPAvatarCell *selectedAvatar = [self selectedAvatar];
- if (!selectedAvatar) {
- // No selected user.
- *isNew = NO;
- return nil;
- }
-
- return [self userForAvatar:selectedAvatar inContext:context isNew:isNew];
-}
-
-- (MPUserEntity *)userForAvatar:(MPAvatarCell *)cell inContext:(NSManagedObjectContext *)context isNew:(BOOL *)isNew {
-
- return [self userForIndexPath:[self.avatarCollectionView indexPathForCell:cell] inContext:context isNew:isNew];
-}
-
-- (MPUserEntity *)userForIndexPath:(NSIndexPath *)indexPath inContext:(NSManagedObjectContext *)context isNew:(BOOL *)isNew {
-
- if ((*isNew = indexPath.item >= [self.userIDs count]))
- return nil;
-
- NSError *error = nil;
- MPUserEntity *user = (MPUserEntity *)[context existingObjectWithID:self.userIDs[indexPath.item] error:&error];
- if (error)
- wrn(@"Failed to load user into context: %@", error);
-
- return user;
-}
-
-- (void)updateAvatars {
-
- for (NSIndexPath *indexPath in self.avatarCollectionView.indexPathsForVisibleItems)
- [self updateAvatarAtIndexPath:indexPath];
-}
-
-- (void)updateAvatarAtIndexPath:(NSIndexPath *)indexPath {
-
- MPAvatarCell *cell = (MPAvatarCell *)[self.avatarCollectionView cellForItemAtIndexPath:indexPath];
- [self updateAvatar:cell atIndexPath:indexPath];
-}
-
-- (void)updateAvatar:(MPAvatarCell *)cell atIndexPath:(NSIndexPath *)indexPath {
-
- CGFloat current = [self.avatarCollectionView layoutAttributesForItemAtIndexPath:indexPath].center.x -
- self.avatarCollectionView.contentOffset.x;
- CGFloat max = self.avatarCollectionView.bounds.size.width;
- cell.visibility = MAX(0, MIN( 1, 1 - ABS( current / (max / 2) - 1 ) ));
-}
+#pragma mark - Private
- (void)registerObservers {
@@ -426,46 +122,19 @@ typedef NS_ENUM(NSUInteger, MPActiveUserState) {
Weakify(self);
_notificationObservers = @[
- [[NSNotificationCenter defaultCenter]
- addObserverForName:UIApplicationWillResignActiveNotification object:nil
- queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
- Strongify(self);
-
-// [self emergencyCloseAnimated:NO];
- self.userSelectionContainer.alpha = 0;
- self.passwordSelectionContainer.alpha = 0;
- }],
- [[NSNotificationCenter defaultCenter]
- addObserverForName:UIApplicationDidBecomeActiveNotification object:nil
- queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
- Strongify(self);
-
- [self updateMode];
- [UIView animateWithDuration:1 animations:^{
- self.userSelectionContainer.alpha = 1;
- self.passwordSelectionContainer.alpha = 1;
- }];
- }],
[[NSNotificationCenter defaultCenter]
addObserverForName:MPSignedInNotification object:nil
queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
Strongify(self);
- self.mode = MPCombinedModePasswordSelection;
+ [self setMode:MPCombinedModePasswordSelection];
}],
[[NSNotificationCenter defaultCenter]
addObserverForName:MPSignedOutNotification object:nil
queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
Strongify(self);
- self.mode = MPCombinedModeUserSelection;
- }],
- [[NSNotificationCenter defaultCenter]
- addObserverForName:UITextFieldTextDidChangeNotification object:self.entryField
- queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
- Strongify(self);
-
- [self textFieldDidChange:note.object];
+ [self setMode:MPCombinedModeUserSelection animated:[note.userInfo[@"animated"] boolValue]];
}],
];
}
@@ -477,181 +146,8 @@ typedef NS_ENUM(NSUInteger, MPActiveUserState) {
_notificationObservers = nil;
}
-- (void)needStoreObserved:(BOOL)observeStore {
- if (observeStore) {
- Weakify(self);
-
- NSManagedObjectContext *mainContext = [MPiOSAppDelegate managedObjectContextForMainThreadIfReady];
- if (!_mocObserver && mainContext)
- _mocObserver = [[NSNotificationCenter defaultCenter]
- addObserverForName:NSManagedObjectContextObjectsDidChangeNotification object:mainContext
- queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
- Strongify(self);
- [self updateMode];
- }];
- if (!_storeObserver)
- _storeObserver = [[NSNotificationCenter defaultCenter]
- addObserverForName:USMStoreDidChangeNotification object:nil
- queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
- Strongify(self);
- [self updateMode];
- }];
- }
- if (!observeStore) {
- if (_mocObserver)
- [[NSNotificationCenter defaultCenter] removeObserver:_mocObserver];
- if (_storeObserver)
- [[NSNotificationCenter defaultCenter] removeObserver:_storeObserver];
- }
-}
-
-- (void)setUserIDs:(NSArray *)userIDs {
-
- _userIDs = userIDs;
-
- [[NSOperationQueue mainQueue] addOperationWithBlock:^{
- [self.avatarCollectionView reloadData];
- }];
-}
-
-- (void)setMode:(MPCombinedMode)mode {
-
- _mode = mode;
-
- [self updateMode];
-}
-
-- (void)updateMode {
-
- // Ensure we're on the main thread.
- if (![NSThread isMainThread]) {
- [[NSOperationQueue mainQueue] addOperationWithBlock:^{
- [self updateMode];
- }];
- return;
- }
-
- self.userSelectionContainer.hidden = YES;
- self.passwordSelectionContainer.hidden = YES;
-
- [self becomeFirstResponder];
-
- switch (self.mode) {
- case MPCombinedModeUserSelection: {
- [[self navigationController] setNavigationBarHidden:YES animated:YES];
- self.userSelectionContainer.hidden = NO;
- [self needStoreObserved:YES];
-
- [self setActiveUserState:MPActiveUserStateNone animated:NO];
- [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
- NSError *error = nil;
- NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPUserEntity class] )];
- fetchRequest.sortDescriptors = @[ [NSSortDescriptor sortDescriptorWithKey:@"lastUsed" ascending:NO] ];
- NSArray *users = [context executeFetchRequest:fetchRequest error:&error];
- if (!users) {
- err(@"Failed to load users: %@", error);
- self.userIDs = nil;
- }
-
- NSMutableArray *userIDs = [NSMutableArray arrayWithCapacity:[users count]];
- for (MPUserEntity *user in users)
- [userIDs addObject:user.objectID];
- self.userIDs = userIDs;
- }];
-
- break;
- }
- case MPCombinedModePasswordSelection: {
- [[self navigationController] setNavigationBarHidden:NO animated:YES];
- self.passwordSelectionContainer.hidden = NO;
- [self needStoreObserved:NO];
- break;
- }
- }
-}
-
-- (void)setActiveUserState:(MPActiveUserState)activeUserState {
-
- [self setActiveUserState:activeUserState animated:YES];
-}
-
-- (void)setActiveUserState:(MPActiveUserState)activeUserState animated:(BOOL)animated {
-
- _activeUserState = activeUserState;
- _masterPasswordChoice = nil;
-
- [UIView animateWithDuration:animated? 0.3f: 0 animations:^{
- // Set the entry container's contents.
- switch (activeUserState) {
- case MPActiveUserStateNone: {
- for (NSUInteger item = 0; item < [self.avatarCollectionView numberOfItemsInSection:0]; ++item) {
- NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:0];
- [self.avatarCollectionView deselectItemAtIndexPath:indexPath animated:YES];
-
- MPAvatarCell *avatarCell = (MPAvatarCell *)[self.avatarCollectionView cellForItemAtIndexPath:indexPath];
- avatarCell.mode = MPAvatarModeLowered;
- }
- break;
- }
- case MPActiveUserStateLogin: {
- self.entryField.text = strl( @"Enter your master password:" );
- self.entryField.text = nil;
- self.entryField.secureTextEntry = YES;
- break;
- }
- case MPActiveUserStateUserName: {
- self.entryLabel.text = strl( @"Enter your full name:" );
- self.entryField.text = nil;
- self.entryField.secureTextEntry = NO;
- break;
- }
- case MPActiveUserStateMasterPasswordChoice: {
- self.entryLabel.text = strl( @"Choose your master password:" );
- self.entryField.text = nil;
- self.entryField.secureTextEntry = YES;
- break;
- }
- case MPActiveUserStateMasterPasswordConfirmation: {
- _masterPasswordChoice = self.entryField.text;
- self.entryLabel.text = strl( @"Confirm your master password:" );
- self.entryField.text = nil;
- self.entryField.secureTextEntry = YES;
- break;
- }
- }
-
- // Manage the random avatar for the new user if selected.
- MPAvatarCell *selectedAvatar = [self selectedAvatar];
- if (selectedAvatar.avatar == MPAvatarAdd) {
- selectedAvatar.avatar = arc4random() % MPAvatarCount;
- }
- else {
- NSIndexPath *newUserIndexPath = [NSIndexPath indexPathForItem:[_userIDs count] inSection:0];
- MPAvatarCell *newUserAvatar = (MPAvatarCell *)[[self avatarCollectionView] cellForItemAtIndexPath:newUserIndexPath];
- newUserAvatar.avatar = MPAvatarAdd;
- newUserAvatar.name = strl( @"New User" );
- }
-
- // Manage the entry container depending on whether a user is activate or not.
- if (activeUserState == MPActiveUserStateNone) {
- self.avatarCollectionCenterConstraint.priority = UILayoutPriorityDefaultHigh;
- self.entryContainer.alpha = 0;
- }
- else {
- self.avatarCollectionCenterConstraint.priority = UILayoutPriorityDefaultLow;
- self.entryContainer.alpha = 1;
- }
- [self.avatarCollectionCenterConstraint apply];
-
- // Toggle the keyboard.
- if (activeUserState == MPActiveUserStateNone)
- [self.entryField resignFirstResponder];
- } completion:^(BOOL finished) {
- if (activeUserState != MPActiveUserStateNone)
- [self.entryField becomeFirstResponder];
- }];
-}
+#pragma mark - Actions
- (IBAction)doSignOut:(UIBarButtonItem *)sender {
diff --git a/MasterPassword/ObjC/iOS/MPPasswordsViewController.h b/MasterPassword/ObjC/iOS/MPPasswordsViewController.h
new file mode 100644
index 00000000..05fd41b0
--- /dev/null
+++ b/MasterPassword/ObjC/iOS/MPPasswordsViewController.h
@@ -0,0 +1,33 @@
+/**
+ * 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
+ */
+
+//
+// MPCombinedViewController.h
+// MPCombinedViewController
+//
+// Created by lhunath on 2014-03-08.
+// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
+//
+
+#import "LLGitTip.h"
+
+@interface MPPasswordsViewController : UIViewController
+
+@property(strong, nonatomic) IBOutlet UIView *passwordSelectionContainer;
+@property(strong, nonatomic) IBOutlet UICollectionView *passwordCollectionView;
+@property (strong, nonatomic) IBOutlet NSLayoutConstraint *passwordsToBottomConstraint;
+@property (strong, nonatomic) IBOutlet NSLayoutConstraint *navigationBarToTopConstraint;
+@property (strong, nonatomic) IBOutlet NSLayoutConstraint *navigationBarToPasswordsConstraint;
+
+@property(assign, nonatomic) BOOL active;
+
+- (void)setActive:(BOOL)active animated:(BOOL)animated;
+
+@end
diff --git a/MasterPassword/ObjC/iOS/MPPasswordsViewController.m b/MasterPassword/ObjC/iOS/MPPasswordsViewController.m
new file mode 100644
index 00000000..ffe16a47
--- /dev/null
+++ b/MasterPassword/ObjC/iOS/MPPasswordsViewController.m
@@ -0,0 +1,218 @@
+/**
+ * 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
+ */
+
+//
+// MPPasswordsViewController.h
+// MPPasswordsViewController
+//
+// Created by lhunath on 2014-03-08.
+// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
+//
+
+#import "MPPasswordsViewController.h"
+#import "MPiOSAppDelegate.h"
+#import "MPAppDelegate_Store.h"
+
+@interface MPPasswordsViewController()
+@property (strong, nonatomic) IBOutlet UINavigationBar *navigationBar;
+
+@end
+
+@implementation MPPasswordsViewController {
+ __weak id _storeObserver;
+ __weak id _mocObserver;
+ NSArray *_notificationObservers;
+}
+
+- (void)viewDidLoad {
+
+ [super viewDidLoad];
+
+ self.view.backgroundColor = [UIColor clearColor];
+}
+
+- (void)viewWillAppear:(BOOL)animated {
+
+ [super viewWillAppear:animated];
+
+ [self registerObservers];
+ [self observeStore];
+}
+
+- (void)viewWillDisappear:(BOOL)animated {
+
+ [super viewWillDisappear:animated];
+
+ [self removeObservers];
+ [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 {
+}
+
+- (BOOL)textFieldShouldReturn:(UITextField *)textField {
+
+ return NO;
+}
+
+// This isn't really in UITextFieldDelegate. We fake it from UITextFieldTextDidChangeNotification.
+- (void)textFieldDidChange:(UITextField *)textField {
+
+}
+
+#pragma mark - UICollectionViewDataSource
+
+- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
+
+ if (collectionView == self.passwordCollectionView)
+ return 0;
+
+ Throw(@"unexpected collection view: %@", collectionView);
+}
+
+- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
+
+ if (collectionView == self.passwordCollectionView)
+ return nil;
+
+ 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 - Private
+
+- (void)registerObservers {
+
+ if ([_notificationObservers count])
+ return;
+
+ Weakify(self);
+ _notificationObservers = @[
+ [[NSNotificationCenter defaultCenter]
+ addObserverForName:UIApplicationWillResignActiveNotification object:nil
+ queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
+ Strongify(self);
+
+ self.passwordSelectionContainer.alpha = 0;
+ }],
+ [[NSNotificationCenter defaultCenter]
+ addObserverForName:UIApplicationDidBecomeActiveNotification object:nil
+ queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
+ Strongify(self);
+
+// [self updateMode]; TODO: reload passwords list
+ [UIView animateWithDuration:1 animations:^{
+ self.passwordSelectionContainer.alpha = 1;
+ }];
+ }],
+ ];
+}
+
+- (void)removeObservers {
+
+ for (id observer in _notificationObservers)
+ [[NSNotificationCenter defaultCenter] removeObserver:observer];
+ _notificationObservers = nil;
+}
+
+- (void)observeStore {
+
+// Weakify(self);
+
+ 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
+ }];
+}
+
+- (void)stopObservingStore {
+
+ if (_mocObserver)
+ [[NSNotificationCenter defaultCenter] removeObserver:_mocObserver];
+ if (_storeObserver)
+ [[NSNotificationCenter defaultCenter] removeObserver:_storeObserver];
+}
+
+#pragma mark - Properties
+
+- (void)setActive:(BOOL)active {
+
+ [self setActive:active animated:YES];
+}
+
+- (void)setActive:(BOOL)active animated:(BOOL)animated {
+
+ _active = active;
+
+ [UIView animateWithDuration:animated? 0.3f: 0 animations:^{
+ self.navigationBarToPasswordsConstraint.priority = active? UILayoutPriorityDefaultHigh: 1;
+ self.navigationBarToTopConstraint.priority = active? 1: UILayoutPriorityDefaultHigh;
+ self.passwordsToBottomConstraint.priority = active? 1: UILayoutPriorityDefaultHigh;
+
+ [self.navigationBarToPasswordsConstraint apply];
+ [self.navigationBarToTopConstraint apply];
+ [self.passwordsToBottomConstraint apply];
+ }];
+}
+
+#pragma mark - Actions
+
+@end
diff --git a/MasterPassword/ObjC/iOS/MPPreferencesViewController.m b/MasterPassword/ObjC/iOS/MPPreferencesViewController.m
index 39308bca..2b52922c 100644
--- a/MasterPassword/ObjC/iOS/MPPreferencesViewController.m
+++ b/MasterPassword/ObjC/iOS/MPPreferencesViewController.m
@@ -25,12 +25,12 @@
for (NSUInteger a = 0; a < MPAvatarCount; ++a) {
UIButton *avatar = [self.avatarTemplate clone];
- avatar.tag = (NSInteger)a;
+ avatar.tag = a;
avatar.hidden = NO;
avatar.center = CGPointMake(
self.avatarTemplate.center.x * (a + 1) + self.avatarTemplate.bounds.size.width / 2 * a,
self.avatarTemplate.center.y );
- [avatar setBackgroundImage:[UIImage imageNamed:PearlString( @"avatar-%d", a )]
+ [avatar setBackgroundImage:[UIImage imageNamed:PearlString( @"avatar-%ld", (long)a )]
forState:UIControlStateNormal];
[avatar setSelectionInSuperviewCandidate:YES isClearable:NO];
diff --git a/MasterPassword/ObjC/iOS/MPUnlockViewController.m b/MasterPassword/ObjC/iOS/MPUnlockViewController.m
index 733f2269..b6afd4d9 100644
--- a/MasterPassword/ObjC/iOS/MPUnlockViewController.m
+++ b/MasterPassword/ObjC/iOS/MPUnlockViewController.m
@@ -54,7 +54,7 @@
avatar.center = CGPointMake(
(20 + self.avatarTemplate.bounds.size.width / 2) * (a + 1) + self.avatarTemplate.bounds.size.width / 2 * a,
20 + self.avatarTemplate.bounds.size.height / 2 );
- [avatar setBackgroundImage:[UIImage imageNamed:PearlString( @"avatar-%d", a )] forState:UIControlStateNormal];
+ [avatar setBackgroundImage:[UIImage imageNamed:PearlString( @"avatar-%ld", (long)a )] forState:UIControlStateNormal];
[avatar setSelectionInSuperviewCandidate:YES isClearable:NO];
avatar.layer.cornerRadius = avatar.bounds.size.height / 2;
@@ -276,12 +276,6 @@
[super viewWillDisappear:animated];
}
-- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
-
- if ([segue.identifier isEqualToString:@"MP_Settings"])
- [self.navigationController setNavigationBarHidden:NO animated:YES];
-}
-
- (BOOL)prefersStatusBarHidden {
return YES;
@@ -292,6 +286,12 @@
return UIStatusBarAnimationSlide;
}
+- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
+
+ if ([segue.identifier isEqualToString:@"MP_Settings"])
+ [self.navigationController setNavigationBarHidden:NO animated:YES];
+}
+
- (BOOL)canBecomeFirstResponder {
return YES;
diff --git a/MasterPassword/ObjC/iOS/MPUsersViewController.h b/MasterPassword/ObjC/iOS/MPUsersViewController.h
new file mode 100644
index 00000000..651e3ef4
--- /dev/null
+++ b/MasterPassword/ObjC/iOS/MPUsersViewController.h
@@ -0,0 +1,40 @@
+/**
+ * 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
+ */
+
+//
+// MPCombinedViewController.h
+// MPCombinedViewController
+//
+// Created by lhunath on 2014-03-08.
+// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
+//
+
+#import "LLGitTip.h"
+
+@interface MPUsersViewController : UIViewController
+
+@property (strong, nonatomic) IBOutlet UINavigationBar *navigationBar;
+@property(weak, nonatomic) IBOutlet UIView *userSelectionContainer;
+@property(weak, nonatomic) IBOutlet UILabel *hintLabel;
+@property(weak, nonatomic) IBOutlet UIView *gitTipTip;
+@property(weak, nonatomic) IBOutlet LLGitTip *gitTipButton;
+@property(weak, nonatomic) IBOutlet UITextField *entryField;
+@property(weak, nonatomic) IBOutlet UILabel *entryLabel;
+@property(weak, nonatomic) IBOutlet UIView *entryContainer;
+@property(weak, nonatomic) IBOutlet UIView *footerContainer;
+@property(weak, nonatomic) IBOutlet UICollectionView *avatarCollectionView;
+@property(weak, nonatomic) IBOutlet NSLayoutConstraint *avatarCollectionCenterConstraint;
+@property (strong, nonatomic) IBOutlet NSLayoutConstraint *navigationBarToTopConstraint;
+
+@property(assign, nonatomic) BOOL active;
+
+- (void)setActive:(BOOL)active animated:(BOOL)animated;
+
+@end
diff --git a/MasterPassword/ObjC/iOS/MPUsersViewController.m b/MasterPassword/ObjC/iOS/MPUsersViewController.m
new file mode 100644
index 00000000..203393f1
--- /dev/null
+++ b/MasterPassword/ObjC/iOS/MPUsersViewController.m
@@ -0,0 +1,742 @@
+/**
+ * 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
+ */
+
+//
+// MPCombinedViewController.h
+// MPCombinedViewController
+//
+// Created by lhunath on 2014-03-08.
+// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
+//
+
+#import "MPUsersViewController.h"
+#import "MPEntities.h"
+#import "MPAvatarCell.h"
+#import "MPiOSAppDelegate.h"
+#import "MPAppDelegate_Store.h"
+#import "MPAppDelegate_Key.h"
+
+typedef NS_ENUM(NSUInteger, MPActiveUserState) {
+ /** The users are all inactive */
+ MPActiveUserStateNone,
+ /** The selected user is activated and being logged in with */
+ MPActiveUserStateLogin,
+ /** The selected user is activated and its user name is being asked for */
+ MPActiveUserStateUserName,
+ /** The selected user is activated and its new master password is being asked for */
+ MPActiveUserStateMasterPasswordChoice,
+ /** The selected user is activated and the confirmation of the previously entered master password is being asked for */
+ MPActiveUserStateMasterPasswordConfirmation,
+ /** The selected user is activated displayed at the top with the rest of the UI inactive */
+ MPActiveUserStateMinimized,
+};
+
+@interface MPUsersViewController()
+
+@property(nonatomic) MPActiveUserState activeUserState;
+@property(nonatomic, strong) NSArray *userIDs;
+@property(nonatomic, strong) NSTimer *marqueeTipTimer;
+@property(nonatomic, strong) NSArray *marqueeTipTexts;
+@property(nonatomic) NSUInteger marqueeTipTextIndex;
+@end
+
+@implementation MPUsersViewController {
+ __weak id _storeObserver;
+ __weak id _mocObserver;
+ NSArray *_notificationObservers;
+ NSString *_masterPasswordChoice;
+}
+
+- (void)viewDidLoad {
+
+ [super viewDidLoad];
+
+ self.marqueeTipTexts = @[
+ strl(@"Press and hold to change password or delete."),
+ strl(@"Shake for emergency generator."),
+ ];
+
+ self.view.backgroundColor = [UIColor clearColor];
+ self.avatarCollectionView.allowsMultipleSelection = YES;
+ [self.entryField addTarget:self action:@selector(textFieldEditingChanged:) forControlEvents:UIControlEventEditingChanged];
+
+ [self setActive:YES animated:NO];
+}
+
+- (void)viewWillAppear:(BOOL)animated {
+
+ [super viewWillAppear:animated];
+
+ self.userSelectionContainer.alpha = 0;
+
+ [self observeStore];
+ [self registerObservers];
+ [self reloadUsers];
+
+ [self.marqueeTipTimer invalidate];
+ self.marqueeTipTimer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(firedMarqueeTimer:) userInfo:nil repeats:YES];
+}
+
+- (void)viewWillDisappear:(BOOL)animated {
+
+ [super viewWillDisappear:animated];
+
+ [self removeObservers];
+ [self stopObservingStore];
+
+ [self.marqueeTipTimer invalidate];
+}
+
+- (BOOL)canBecomeFirstResponder {
+
+ return YES;
+}
+
+- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event {
+
+// if (motion == UIEventSubtypeMotionShake)
+// [self emergencyOpenAnimated:YES];
+}
+
+#pragma mark - UITextFieldDelegate
+
+- (void)textFieldDidEndEditing:(UITextField *)textField {
+}
+
+- (BOOL)textFieldShouldReturn:(UITextField *)textField {
+
+ if (textField == self.entryField) {
+ switch (self.activeUserState) {
+ case MPActiveUserStateNone: {
+ [textField resignFirstResponder];
+ break;
+ }
+ case MPActiveUserStateLogin: {
+ [self selectedAvatar].spinnerActive = YES;
+ [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
+ BOOL signedIn = NO, isNew = NO;
+ MPUserEntity *user = [self selectedUserInContext:context isNew:&isNew];
+ if (!isNew && user)
+ signedIn = [[MPiOSAppDelegate get] signInAsUser:user saveInContext:context
+ usingMasterPassword:self.entryField.text];
+
+ [[NSOperationQueue mainQueue] addOperationWithBlock:^{
+ [self selectedAvatar].spinnerActive = NO;
+
+ if (!signedIn) {
+ // Sign in failed.
+ // TODO: warn user
+ return;
+ }
+ }];
+ }];
+ break;
+ }
+ case MPActiveUserStateUserName: {
+ NSString *userName = self.entryField.text;
+ if (![userName length]) {
+ // No name entered.
+ // TODO: warn user
+ return NO;
+ }
+
+ [self selectedAvatar].name = userName;
+ self.activeUserState = MPActiveUserStateMasterPasswordChoice;
+ break;
+ }
+ case MPActiveUserStateMasterPasswordChoice: {
+ NSString *masterPassword = self.entryField.text;
+ if (![masterPassword length]) {
+ // No password entered.
+ // TODO: warn user
+ return NO;
+ }
+
+ self.activeUserState = MPActiveUserStateMasterPasswordConfirmation;
+ break;
+ }
+ case MPActiveUserStateMasterPasswordConfirmation: {
+ NSString *masterPassword = self.entryField.text;
+ if (![masterPassword length]) {
+ // No password entered.
+ // TODO: warn user
+ return NO;
+ }
+
+ if (![masterPassword isEqualToString:_masterPasswordChoice]) {
+ // Master password confirmation failed.
+ // TODO: warn user
+ self.activeUserState = MPActiveUserStateMasterPasswordChoice;
+ return NO;
+ }
+
+ [self selectedAvatar].spinnerActive = YES;
+ [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
+ BOOL isNew = NO;
+ MPUserEntity *user = [self selectedUserInContext:context isNew:&isNew];
+ if (isNew) {
+ user = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass( [MPUserEntity class] )
+ inManagedObjectContext:context];
+ MPAvatarCell *avatarCell = [self selectedAvatar];
+ user.avatar = avatarCell.avatar;
+ user.name = avatarCell.name;
+ }
+
+ BOOL signedIn = [[MPiOSAppDelegate get] signInAsUser:user saveInContext:context usingMasterPassword:masterPassword];
+ [[NSOperationQueue mainQueue] addOperationWithBlock:^{
+ [self selectedAvatar].spinnerActive = NO;
+
+ if (!signedIn) {
+ // Sign in failed, shouldn't happen for a new user.
+ // TODO: warn user
+ self.activeUserState = MPActiveUserStateNone;
+ return;
+ }
+ }];
+ }];
+
+ break;
+ }
+ case MPActiveUserStateMinimized: {
+ [textField resignFirstResponder];
+ break;
+ }
+ }
+ }
+
+ return NO;
+}
+
+// This isn't really in UITextFieldDelegate. We fake it from UITextFieldTextDidChangeNotification.
+- (void)textFieldEditingChanged:(UITextField *)textField {
+
+ if (textField == self.entryField) {
+ switch (self.activeUserState) {
+ case MPActiveUserStateNone:
+ break;
+ case MPActiveUserStateLogin:
+ break;
+ case MPActiveUserStateUserName: {
+ NSString *userName = self.entryField.text;
+ [self selectedAvatar].name = [userName length]? userName: strl( @"New User" );
+ break;
+ }
+ case MPActiveUserStateMasterPasswordChoice:
+ break;
+ case MPActiveUserStateMasterPasswordConfirmation:
+ break;
+ case MPActiveUserStateMinimized:
+ break;
+ }
+ }
+}
+
+#pragma mark - UICollectionViewDataSource
+
+- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
+
+ if (collectionView == self.avatarCollectionView)
+ return [self.userIDs count] + 1;
+
+ Throw(@"unexpected collection view: %@", collectionView);
+}
+
+- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
+
+ if (collectionView == self.avatarCollectionView) {
+ MPAvatarCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:[MPAvatarCell reuseIdentifier] forIndexPath:indexPath];
+ [cell addGestureRecognizer:[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(didLongPress:)]];
+ [self updateVisibilityForAvatar:cell atIndexPath:indexPath animated:NO];
+ [self updateModeForAvatar:cell atIndexPath:indexPath animated:NO];
+
+ BOOL isNew = NO;
+ MPUserEntity *user = [self userForIndexPath:indexPath inContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]
+ isNew:&isNew];
+ if (isNew) {
+ // New User
+ cell.avatar = MPAvatarAdd;
+ cell.name = strl( @"New User" );
+ }
+ else {
+ // Existing User
+ cell.avatar = user.avatar;
+ cell.name = user.name;
+ }
+
+ return cell;
+ }
+
+ Throw(@"unexpected collection view: %@", collectionView);
+}
+
+- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
+
+ if (collectionView == self.avatarCollectionView) {
+ [self.avatarCollectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally
+ animated:YES];
+
+ // Deselect all other cells.
+ for (NSUInteger otherItem = 0; otherItem < [collectionView numberOfItemsInSection:indexPath.section]; ++otherItem)
+ if (otherItem != indexPath.item) {
+ NSIndexPath *otherIndexPath = [NSIndexPath indexPathForItem:otherItem inSection:indexPath.section];
+ [collectionView deselectItemAtIndexPath:otherIndexPath animated:YES];
+ }
+
+ BOOL isNew = NO;
+ MPUserEntity *user = [self userForIndexPath:indexPath inContext:[MPiOSAppDelegate managedObjectContextForMainThreadIfReady]
+ isNew:&isNew];
+ if (isNew)
+ self.activeUserState = MPActiveUserStateUserName;
+ else if (!user.keyID)
+ self.activeUserState = MPActiveUserStateMasterPasswordChoice;
+ else
+ self.activeUserState = MPActiveUserStateLogin;
+ }
+}
+
+- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath {
+
+ if (collectionView == self.avatarCollectionView) {
+ self.activeUserState = MPActiveUserStateNone;
+ }
+}
+
+#pragma mark - UILongPressGestureRecognizer
+
+- (void)didLongPress:(UILongPressGestureRecognizer *)recognizer {
+
+ if ([recognizer.view isKindOfClass:[MPAvatarCell class]]) {
+ if (recognizer.state != UIGestureRecognizerStateBegan)
+ // Don't show the action menu unless the state is Began.
+ return;
+
+ MPAvatarCell *avatarCell = (MPAvatarCell *)recognizer.view;
+ NSManagedObjectContext *mainContext = [MPiOSAppDelegate managedObjectContextForMainThreadIfReady];
+
+ BOOL isNew = NO;
+ MPUserEntity *user = [self userForAvatar:avatarCell inContext:mainContext isNew:&isNew];
+ NSManagedObjectID *userID = user.objectID;
+ if (isNew || !user)
+ return;
+
+ [PearlSheet showSheetWithTitle:user.name
+ viewStyle:UIActionSheetStyleBlackTranslucent
+ initSheet:nil tappedButtonBlock:^(UIActionSheet *sheet, NSInteger buttonIndex) {
+ if (buttonIndex == [sheet cancelButtonIndex])
+ return;
+
+ if (buttonIndex == [sheet destructiveButtonIndex]) {
+ // Delete User
+ [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
+ NSManagedObject *user_ = [context existingObjectWithID:userID error:NULL];
+ if (user_) {
+ [context deleteObject:user_];
+ [context saveToStore];
+ }
+ }];
+ return;
+ }
+
+ if (buttonIndex == [sheet firstOtherButtonIndex])
+ // Reset Password
+ [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
+ MPUserEntity *user_ = (MPUserEntity *)[context existingObjectWithID:userID error:NULL];
+ if (user_)
+ [[MPiOSAppDelegate get] changeMasterPasswordFor:user_ saveInContext:context didResetBlock:^{
+ dbg(@"changing mp for user: %@, keyID: %@", user_.name, user_.keyID);
+ [[NSOperationQueue mainQueue] addOperationWithBlock:^{
+ NSIndexPath *avatarIndexPath = [self.avatarCollectionView indexPathForCell:avatarCell];
+ [self.avatarCollectionView selectItemAtIndexPath:avatarIndexPath animated:NO
+ scrollPosition:UICollectionViewScrollPositionNone];
+ [self collectionView:self.avatarCollectionView didSelectItemAtIndexPath:avatarIndexPath];
+ }];
+ }];
+ }];
+ } cancelTitle:[PearlStrings get].commonButtonCancel
+ destructiveTitle:@"Delete User" otherTitles:@"Reset Password", nil];
+ }
+}
+
+#pragma mark - UIScrollViewDelegate
+
+- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity
+ targetContentOffset:(inout CGPoint *)targetContentOffset {
+
+ if (scrollView == self.avatarCollectionView) {
+ CGPoint offsetToCenter = CGPointMake(
+ self.avatarCollectionView.bounds.size.width / 2,
+ self.avatarCollectionView.bounds.size.height / 2 );
+ NSIndexPath *avatarIndexPath = [self.avatarCollectionView indexPathForItemAtPoint:
+ CGPointPlusCGPoint( *targetContentOffset, offsetToCenter )];
+ CGPoint targetCenter = [self.avatarCollectionView layoutAttributesForItemAtIndexPath:avatarIndexPath].center;
+ *targetContentOffset = CGPointMinusCGPoint( targetCenter, offsetToCenter );
+ NSAssert([self.avatarCollectionView indexPathForItemAtPoint:targetCenter].item == avatarIndexPath.item, @"should be same item");
+ }
+}
+
+#pragma mark - Private
+
+- (void)firedMarqueeTimer:(NSTimer *)timer {
+
+ [UIView animateWithDuration:0.5 animations:^{
+ self.hintLabel.alpha = 0;
+ } completion:^(BOOL finished) {
+ if (!finished)
+ return;
+
+ self.hintLabel.text = self.marqueeTipTexts[++self.marqueeTipTextIndex % [self.marqueeTipTexts count]];
+ [UIView animateWithDuration:0.5 animations:^{
+ self.hintLabel.alpha = 1;
+ }];
+ }];
+}
+
+- (MPAvatarCell *)selectedAvatar {
+
+ NSArray *selectedIndexPaths = self.avatarCollectionView.indexPathsForSelectedItems;
+ if (![selectedIndexPaths count]) {
+ // No selected user.
+ return nil;
+ }
+
+ return (MPAvatarCell *)[self.avatarCollectionView cellForItemAtIndexPath:selectedIndexPaths.firstObject];
+}
+
+- (MPUserEntity *)selectedUserInContext:(NSManagedObjectContext *)context isNew:(BOOL *)isNew {
+
+ MPAvatarCell *selectedAvatar = [self selectedAvatar];
+ if (!selectedAvatar) {
+ // No selected user.
+ *isNew = NO;
+ return nil;
+ }
+
+ return [self userForAvatar:selectedAvatar inContext:context isNew:isNew];
+}
+
+- (MPUserEntity *)userForAvatar:(MPAvatarCell *)cell inContext:(NSManagedObjectContext *)context isNew:(BOOL *)isNew {
+
+ return [self userForIndexPath:[self.avatarCollectionView indexPathForCell:cell] inContext:context isNew:isNew];
+}
+
+- (MPUserEntity *)userForIndexPath:(NSIndexPath *)indexPath inContext:(NSManagedObjectContext *)context isNew:(BOOL *)isNew {
+
+ if ((*isNew = indexPath.item >= [self.userIDs count]))
+ return nil;
+
+ NSError *error = nil;
+ MPUserEntity *user = (MPUserEntity *)[context existingObjectWithID:self.userIDs[indexPath.item] error:&error];
+ if (error)
+ wrn(@"Failed to load user into context: %@", error);
+
+ return user;
+}
+
+- (void)updateAvatars {
+
+ for (NSIndexPath *indexPath in self.avatarCollectionView.indexPathsForVisibleItems)
+ [self updateAvatarAtIndexPath:indexPath];
+}
+
+- (void)updateAvatarAtIndexPath:(NSIndexPath *)indexPath {
+
+ MPAvatarCell *cell = (MPAvatarCell *)[self.avatarCollectionView cellForItemAtIndexPath:indexPath];
+ [self updateVisibilityForAvatar:cell atIndexPath:indexPath animated:NO];
+ [self updateModeForAvatar:cell atIndexPath:indexPath animated:NO];
+}
+
+- (void)updateVisibilityForAvatar:(MPAvatarCell *)cell atIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated {
+
+ CGFloat current = [self.avatarCollectionView layoutAttributesForItemAtIndexPath:indexPath].center.x -
+ self.avatarCollectionView.contentOffset.x;
+ CGFloat max = self.avatarCollectionView.bounds.size.width;
+
+ [cell setVisibility:MAX(0, MIN( 1, 1 - ABS( current / (max / 2) - 1 ) )) animated:animated];
+}
+
+- (void)updateModeForAvatar:(MPAvatarCell *)avatarCell atIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated {
+
+ switch (self.activeUserState) {
+ case MPActiveUserStateNone: {
+ [self.avatarCollectionView deselectItemAtIndexPath:indexPath animated:YES];
+ [avatarCell setMode:MPAvatarModeLowered animated:animated];
+ break;
+ }
+ case MPActiveUserStateLogin:
+ case MPActiveUserStateUserName:
+ case MPActiveUserStateMasterPasswordChoice:
+ case MPActiveUserStateMasterPasswordConfirmation: {
+ if ([self.avatarCollectionView.indexPathsForSelectedItems containsObject:indexPath])
+ [avatarCell setMode:MPAvatarModeRaisedAndActive animated:animated];
+ else
+ [avatarCell setMode:MPAvatarModeRaisedButInactive animated:animated];
+ break;
+ }
+ case MPActiveUserStateMinimized: {
+ if ([self.avatarCollectionView.indexPathsForSelectedItems containsObject:indexPath])
+ [avatarCell setMode:MPAvatarModeRaisedAndMinimized animated:animated];
+ else
+ [avatarCell setMode:MPAvatarModeRaisedAndHidden animated:animated];
+ break;
+ }
+ }
+}
+
+- (void)registerObservers {
+
+ if ([_notificationObservers count])
+ return;
+
+ Weakify(self);
+ _notificationObservers = @[
+ [[NSNotificationCenter defaultCenter]
+ addObserverForName:UIApplicationWillResignActiveNotification object:nil
+ queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
+ Strongify(self);
+
+// [self emergencyCloseAnimated:NO];
+ self.userSelectionContainer.alpha = 0;
+ }],
+ [[NSNotificationCenter defaultCenter]
+ addObserverForName:UIApplicationDidBecomeActiveNotification object:nil
+ queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
+ Strongify(self);
+
+ [self reloadUsers];
+
+ [UIView animateWithDuration:1 animations:^{
+ self.userSelectionContainer.alpha = 1;
+ }];
+ }],
+ ];
+
+ [self observeKeyPath:@"avatarCollectionView.contentOffset" withBlock:
+ ^(id from, id to, NSKeyValueChange cause, MPUsersViewController *_self) {
+ [_self updateAvatars];
+ }];
+}
+
+- (void)removeObservers {
+
+ for (id observer in _notificationObservers)
+ [[NSNotificationCenter defaultCenter] removeObserver:observer];
+ _notificationObservers = nil;
+
+ [self removeKeyPathObservers];
+}
+
+- (void)observeStore {
+
+ Weakify(self);
+
+ NSManagedObjectContext *mainContext = [MPiOSAppDelegate managedObjectContextForMainThreadIfReady];
+ if (!_mocObserver && mainContext)
+ _mocObserver = [[NSNotificationCenter defaultCenter]
+ addObserverForName:NSManagedObjectContextObjectsDidChangeNotification object:mainContext
+ queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
+ Strongify(self);
+ [self reloadUsers];
+ }];
+ if (!_storeObserver)
+ _storeObserver = [[NSNotificationCenter defaultCenter]
+ addObserverForName:USMStoreDidChangeNotification object:nil
+ queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
+ Strongify(self);
+ [self reloadUsers];
+ }];
+}
+
+- (void)stopObservingStore {
+
+ if (_mocObserver)
+ [[NSNotificationCenter defaultCenter] removeObserver:_mocObserver];
+ if (_storeObserver)
+ [[NSNotificationCenter defaultCenter] removeObserver:_storeObserver];
+}
+
+- (void)reloadUsers {
+
+ [MPiOSAppDelegate managedObjectContextPerformBlock:^(NSManagedObjectContext *context) {
+ NSError *error = nil;
+ NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass( [MPUserEntity class] )];
+ fetchRequest.sortDescriptors = @[ [NSSortDescriptor sortDescriptorWithKey:@"lastUsed" ascending:NO] ];
+ NSArray *users = [context executeFetchRequest:fetchRequest error:&error];
+ if (!users) {
+ err(@"Failed to load users: %@", error);
+ self.userIDs = nil;
+ }
+
+ NSMutableArray *userIDs = [NSMutableArray arrayWithCapacity:[users count]];
+ for (MPUserEntity *user in users)
+ [userIDs addObject:user.objectID];
+ self.userIDs = userIDs;
+ }];
+}
+
+#pragma mark - Properties
+
+- (void)setActive:(BOOL)active {
+
+ [self setActive:active animated:YES];
+}
+
+- (void)setActive:(BOOL)active animated:(BOOL)animated {
+
+ _active = active;
+ dbg(@"active -> %d", active);
+
+ if (active)
+ [self setActiveUserState:MPActiveUserStateNone animated:animated];
+ else
+ [self setActiveUserState:MPActiveUserStateMinimized animated:animated];
+}
+
+- (void)setUserIDs:(NSArray *)userIDs {
+
+ _userIDs = userIDs;
+ dbg(@"userIDs -> %lu", (unsigned long)[userIDs count]);
+
+ [[NSOperationQueue mainQueue] addOperationWithBlock:^{
+ [self.avatarCollectionView reloadData];
+
+ [UIView animateWithDuration:0.3f animations:^{
+ self.userSelectionContainer.alpha = 1;
+ }];
+ }];
+}
+
+- (void)setActiveUserState:(MPActiveUserState)activeUserState {
+
+ [self setActiveUserState:activeUserState animated:YES];
+}
+
+- (void)setActiveUserState:(MPActiveUserState)activeUserState animated:(BOOL)animated {
+
+ _activeUserState = activeUserState;
+ _masterPasswordChoice = nil;
+
+ if (activeUserState != MPActiveUserStateMinimized && [MPiOSAppDelegate get].key) {
+ [[MPiOSAppDelegate get] signOutAnimated:YES];
+ return;
+ }
+
+ [UIView animateWithDuration:animated? 0.3f: 0 animations:^{
+ MPAvatarCell *selectedAvatar = [self selectedAvatar];
+
+ // Set avatar modes.
+ for (NSUInteger item = 0; item < [self.avatarCollectionView numberOfItemsInSection:0]; ++item) {
+ NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:0];
+ MPAvatarCell *avatarCell = (MPAvatarCell *)[self.avatarCollectionView cellForItemAtIndexPath:indexPath];
+ [self updateModeForAvatar:avatarCell atIndexPath:indexPath animated:NO];
+
+ if (selectedAvatar && avatarCell == selectedAvatar)
+ [self.avatarCollectionView scrollToItemAtIndexPath:indexPath
+ atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO];
+ }
+
+ // Set the entry container's contents.
+ switch (activeUserState) {
+ case MPActiveUserStateNone:
+ dbg(@"activeUserState -> none");
+ break;
+ case MPActiveUserStateLogin: {
+ dbg(@"activeUserState -> login");
+ self.entryLabel.text = strl( @"Enter your master password:" );
+ self.entryField.text = nil;
+ self.entryField.secureTextEntry = YES;
+ break;
+ }
+ case MPActiveUserStateUserName: {
+ dbg(@"activeUserState -> userName");
+ self.entryLabel.text = strl( @"Enter your full name:" );
+ self.entryField.text = nil;
+ self.entryField.secureTextEntry = NO;
+ break;
+ }
+ case MPActiveUserStateMasterPasswordChoice: {
+ dbg(@"activeUserState -> masterPasswordChoice");
+ self.entryLabel.text = strl( @"Choose your master password:" );
+ self.entryField.text = nil;
+ self.entryField.secureTextEntry = YES;
+ break;
+ }
+ case MPActiveUserStateMasterPasswordConfirmation: {
+ dbg(@"activeUserState -> masterPasswordConfirmation");
+ _masterPasswordChoice = self.entryField.text;
+ self.entryLabel.text = strl( @"Confirm your master password:" );
+ self.entryField.text = nil;
+ self.entryField.secureTextEntry = YES;
+ break;
+ }
+ case MPActiveUserStateMinimized:
+ dbg(@"activeUserState -> minimized");
+ break;
+ }
+
+ // Manage the random avatar for the new user if selected.
+ if (selectedAvatar.avatar == MPAvatarAdd)
+ selectedAvatar.avatar = arc4random() % MPAvatarCount;
+ else {
+ NSIndexPath *newUserIndexPath = [NSIndexPath indexPathForItem:[_userIDs count] inSection:0];
+ MPAvatarCell *newUserAvatar = (MPAvatarCell *)[[self avatarCollectionView] cellForItemAtIndexPath:newUserIndexPath];
+ newUserAvatar.avatar = MPAvatarAdd;
+ newUserAvatar.name = strl( @"New User" );
+ }
+
+ // Manage the entry container depending on whether a user is activate or not.
+ switch (activeUserState) {
+ case MPActiveUserStateNone: {
+ self.navigationBarToTopConstraint.priority = UILayoutPriorityDefaultHigh;
+ self.avatarCollectionCenterConstraint.priority = UILayoutPriorityDefaultHigh;
+ self.avatarCollectionView.scrollEnabled = YES;
+ self.entryContainer.alpha = 0;
+ self.footerContainer.alpha = 1;
+ break;
+ }
+ case MPActiveUserStateLogin:
+ case MPActiveUserStateUserName:
+ case MPActiveUserStateMasterPasswordChoice:
+ case MPActiveUserStateMasterPasswordConfirmation: {
+ self.navigationBarToTopConstraint.priority = UILayoutPriorityDefaultHigh;
+ self.avatarCollectionCenterConstraint.priority = UILayoutPriorityDefaultLow;
+ self.avatarCollectionView.scrollEnabled = NO;
+ self.entryContainer.alpha = 1;
+ self.footerContainer.alpha = 1;
+ break;
+ }
+ case MPActiveUserStateMinimized: {
+ self.navigationBarToTopConstraint.priority = 1;
+ self.avatarCollectionCenterConstraint.priority = UILayoutPriorityDefaultLow;
+ self.avatarCollectionView.scrollEnabled = NO;
+ self.entryContainer.alpha = 0;
+ self.footerContainer.alpha = 0;
+ break;
+ }
+ }
+ [self.navigationBarToTopConstraint apply];
+ [self.avatarCollectionCenterConstraint apply];
+
+ // Toggle the keyboard.
+ if (!self.entryContainer.alpha)
+ [self.entryField resignFirstResponder];
+ } completion:^(BOOL finished) {
+ if (finished && self.entryContainer.alpha)
+ [self.entryField becomeFirstResponder];
+ }];
+}
+
+#pragma mark - Actions
+
+- (IBAction)doSignOut:(UIBarButtonItem *)sender {
+
+ [[MPiOSAppDelegate get] signOutAnimated:YES];
+}
+
+@end
diff --git a/MasterPassword/ObjC/iOS/MainStoryboard_iPhone.storyboard b/MasterPassword/ObjC/iOS/MainStoryboard_iPhone.storyboard
index 2ebbf993..9a26cdea 100644
--- a/MasterPassword/ObjC/iOS/MainStoryboard_iPhone.storyboard
+++ b/MasterPassword/ObjC/iOS/MainStoryboard_iPhone.storyboard
@@ -1,8 +1,8 @@
-
+
-
+
@@ -1198,6 +1198,7 @@ L4m3P4sSw0rD
+
@@ -2546,6 +2547,7 @@ Only site names and custom passwords are sent to iCloud. Passwords are encrypte
+
@@ -3173,4 +3175,4 @@ However, it means that anyone who finds your device unlocked can do the same.
-
\ 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 f6171fbc..083cddf7 100644
--- a/MasterPassword/ObjC/iOS/MasterPassword-iOS.xcodeproj/project.pbxproj
+++ b/MasterPassword/ObjC/iOS/MasterPassword-iOS.xcodeproj/project.pbxproj
@@ -12,7 +12,10 @@
93D39262A8A97DB748213309 /* PearlEMail.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D393BB973253D4BAAC84AA /* PearlEMail.m */; };
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 */; };
+ 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 */; };
93D395F08A087F8A24689347 /* NSArray+Indexing.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D39067C0AFDC581794E2B8 /* NSArray+Indexing.m */; };
93D396AA30690B256F30378A /* PearlNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D3956915634581E737B38C /* PearlNavigationController.m */; };
93D396BA1C74C4A06FD86437 /* PearlOverlay.h in Headers */ = {isa = PBXBuildFile; fileRef = 93D3942A356B639724157982 /* PearlOverlay.h */; };
@@ -21,6 +24,7 @@
93D399433EA75E50656040CB /* Twitter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 93D394077F8FAB8167647187 /* Twitter.framework */; };
93D399BBC0A7EC746CB1B19B /* MPLogsViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 93D391943675426839501BB8 /* MPLogsViewController.h */; };
93D39B842AB9A5D072810D76 /* NSError+PearlFullDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = 93D398C95847261903D781D3 /* NSError+PearlFullDescription.h */; };
+ 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 */; };
93D39D596A2E376D6F6F5DA1 /* MPCombinedViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 93D393310223DDB35218467A /* MPCombinedViewController.m */; };
@@ -496,22 +500,29 @@
/* Begin PBXFileReference section */
93D39067C0AFDC581794E2B8 /* NSArray+Indexing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSArray+Indexing.m"; sourceTree = ""; };
+ 93D39083C93D90C4B94541AD /* PearlUIView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PearlUIView.h; sourceTree = ""; };
93D390A66F69AB1CDB0BFF93 /* LLModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LLModel.h; sourceTree = ""; };
+ 93D390EEC85E94D9C888643F /* PearlUINavigationBar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PearlUINavigationBar.h; sourceTree = ""; };
93D390FADEB325D8D54A957D /* PearlOverlay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PearlOverlay.m; sourceTree = ""; };
+ 93D3914D7597F9A28DB9D85E /* MPPasswordsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPPasswordsViewController.h; sourceTree = ""; };
93D391943675426839501BB8 /* MPLogsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MPLogsViewController.h; sourceTree = ""; };
93D3923B42DA2DA18F287092 /* LLModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LLModel.m; sourceTree = ""; };
+ 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 = ""; };
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 = ""; };
93D3956915634581E737B38C /* PearlNavigationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PearlNavigationController.m; 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 = ""; };
93D3979190DACEBD1F6AE9F4 /* MPLogsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPLogsViewController.m; sourceTree = ""; };
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 = ""; };
+ 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 = ""; };
93D39AA1EE2E1E7B81372240 /* NSDictionary+Indexing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+Indexing.m"; sourceTree = ""; };
@@ -521,6 +532,7 @@
93D39C8E26B06F01566785B7 /* LLToggleViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LLToggleViewController.m; 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 = ""; };
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; };
@@ -1666,6 +1678,10 @@
DAFC5657172C573B00CB5CC5 /* InAppSettingsKit */,
DA5BFA47147E415C00F98B1E /* Frameworks */,
DA5BFA45147E415C00F98B1E /* Products */,
+ 93D399E571F61E50A9BF8FAF /* MPUsersViewController.m */,
+ 93D3971FE104BB4052484151 /* MPUsersViewController.h */,
+ 93D393676C32D23A47E27957 /* PearlUIView.m */,
+ 93D39083C93D90C4B94541AD /* PearlUIView.h */,
);
sourceTree = "";
};
@@ -2514,11 +2530,15 @@
93D39730673227EFF6DEFF19 /* MPSetupViewController.h */,
93D3979190DACEBD1F6AE9F4 /* MPLogsViewController.m */,
93D391943675426839501BB8 /* MPLogsViewController.h */,
+ 93D3924EE15017F8A12CB436 /* MPPasswordsViewController.m */,
+ 93D3914D7597F9A28DB9D85E /* MPPasswordsViewController.h */,
93D393310223DDB35218467A /* MPCombinedViewController.m */,
93D39CF8ADF4542CDC4CD385 /* MPCombinedViewController.h */,
DA38D6A218CCB5BF009AEB3E /* Storyboard.storyboard */,
93D39B381350802A194BF332 /* MPAvatarCell.m */,
93D39DA27D768B53C8B1330C /* MPAvatarCell.h */,
+ 93D39DDDAC305E8ABB4220C7 /* PearlUINavigationBar.m */,
+ 93D390EEC85E94D9C888643F /* PearlUINavigationBar.h */,
);
path = iOS;
sourceTree = "";
@@ -3819,6 +3839,10 @@
DA095E75172F4CD8001C948B /* MPLogsViewController.m in Sources */,
93D39D596A2E376D6F6F5DA1 /* MPCombinedViewController.m in Sources */,
93D3957237D303DE2D38C267 /* MPAvatarCell.m in Sources */,
+ 93D39B8F90F58A5D158DDBA3 /* MPPasswordsViewController.m in Sources */,
+ 93D3954FCE045A3CC7E804B7 /* MPUsersViewController.m in Sources */,
+ 93D3959643EACF286D0152BA /* PearlUINavigationBar.m in Sources */,
+ 93D393543ACC701C018C74DA /* PearlUIView.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/MasterPassword/ObjC/iOS/PearlUINavigationBar.h b/MasterPassword/ObjC/iOS/PearlUINavigationBar.h
new file mode 100644
index 00000000..3bb15eb4
--- /dev/null
+++ b/MasterPassword/ObjC/iOS/PearlUINavigationBar.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
+ */
+
+//
+// PearlUINavigationBar.h
+// PearlUINavigationBar
+//
+// Created by lhunath on 2014-03-17.
+// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
+//
+
+#import
+
+@interface PearlUINavigationBar : UINavigationBar
+
+@property (assign, nonatomic) BOOL ignoreTouches;
+@property (assign, nonatomic) BOOL invisible;
+
+@end
diff --git a/MasterPassword/ObjC/iOS/PearlUINavigationBar.m b/MasterPassword/ObjC/iOS/PearlUINavigationBar.m
new file mode 100644
index 00000000..7edcab3c
--- /dev/null
+++ b/MasterPassword/ObjC/iOS/PearlUINavigationBar.m
@@ -0,0 +1,43 @@
+/**
+ * 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
+ */
+
+//
+// PearlUINavigationBar.h
+// PearlUINavigationBar
+//
+// Created by lhunath on 2014-03-17.
+// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
+//
+
+#import "PearlUINavigationBar.h"
+
+@implementation PearlUINavigationBar
+
+- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
+
+ UIView *hitView = [super hitTest:point withEvent:event];
+ if (self.ignoreTouches && hitView == self)
+ return nil;
+
+ return hitView;
+}
+
+- (void)setInvisible:(BOOL)invisible {
+
+ _invisible = invisible;
+
+ if (invisible) {
+ self.translucent = YES;
+ self.shadowImage = [UIImage new];
+ [self setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault];
+ }
+}
+
+@end
diff --git a/MasterPassword/ObjC/iOS/PearlUIView.h b/MasterPassword/ObjC/iOS/PearlUIView.h
new file mode 100644
index 00000000..96db6438
--- /dev/null
+++ b/MasterPassword/ObjC/iOS/PearlUIView.h
@@ -0,0 +1,25 @@
+/**
+ * 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
+ */
+
+//
+// PearlUIView.h
+// PearlUIView
+//
+// Created by lhunath on 2014-03-17.
+// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
+//
+
+#import
+
+@interface PearlUIView : UIView
+
+@property(assign, nonatomic) BOOL ignoreTouches;
+
+@end
diff --git a/MasterPassword/ObjC/iOS/PearlUIView.m b/MasterPassword/ObjC/iOS/PearlUIView.m
new file mode 100644
index 00000000..7e759804
--- /dev/null
+++ b/MasterPassword/ObjC/iOS/PearlUIView.m
@@ -0,0 +1,32 @@
+/**
+ * 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
+ */
+
+//
+// PearlUIView.h
+// PearlUIView
+//
+// Created by lhunath on 2014-03-17.
+// Copyright, lhunath (Maarten Billemont) 2014. All rights reserved.
+//
+
+#import "PearlUIView.h"
+
+@implementation PearlUIView
+
+- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
+
+ UIView *hitView = [super hitTest:point withEvent:event];
+ if (self.ignoreTouches && hitView == self)
+ return nil;
+
+ return hitView;
+}
+
+@end
diff --git a/MasterPassword/ObjC/iOS/Storyboard.storyboard b/MasterPassword/ObjC/iOS/Storyboard.storyboard
index 3c786956..a8475caf 100644
--- a/MasterPassword/ObjC/iOS/Storyboard.storyboard
+++ b/MasterPassword/ObjC/iOS/Storyboard.storyboard
@@ -4,651 +4,33 @@
+
-
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Title
- Title
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
@@ -670,13 +52,21 @@
-
+
+
+
+
+
+
+
+
+
-
+
+
+
-
-
+
+
+
+
+
-
+
+
+
+
@@ -768,107 +166,106 @@
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
+
+
-
-
-
-
+
+
+
-
+
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
+
+
-
-
+
+
@@ -877,14 +274,13 @@
-
-
+
@@ -892,16 +288,771 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Title
+ Title
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -912,6 +1063,7 @@
+