User support.
[ADDED] Support for multiple users, each their own master password.
This commit is contained in:
@@ -10,12 +10,13 @@
|
||||
|
||||
@interface MPAppDelegate_Shared (Key)
|
||||
|
||||
- (void)loadStoredKey;
|
||||
- (void)loadSavedKey;
|
||||
- (IBAction)signOut:(id)sender;
|
||||
|
||||
- (BOOL)tryMasterPassword:(NSString *)tryPassword;
|
||||
- (void)updateKey:(NSData *)key;
|
||||
- (void)forgetKey;
|
||||
- (BOOL)tryMasterPassword:(NSString *)tryPassword forUser:(MPUserEntity *)user;
|
||||
- (void)storeSavedKey;
|
||||
- (void)forgetSavedKey;
|
||||
- (void)unsetKey;
|
||||
|
||||
- (NSData *)keyWithLength:(NSUInteger)keyLength;
|
||||
|
||||
|
@@ -8,67 +8,48 @@
|
||||
|
||||
#import "MPConfig.h"
|
||||
#import "MPAppDelegate_Key.h"
|
||||
#import "MPElementEntity.h"
|
||||
|
||||
@implementation MPAppDelegate_Shared (Key)
|
||||
|
||||
static NSDictionary *keyQuery() {
|
||||
static NSDictionary *keyQuery(MPUserEntity *user) {
|
||||
|
||||
static NSDictionary *MPKeyQuery = nil;
|
||||
if (!MPKeyQuery)
|
||||
MPKeyQuery = [PearlKeyChain createQueryForClass:kSecClassGenericPassword
|
||||
attributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
@"Saved Master Password", (__bridge id)kSecAttrService,
|
||||
@"default", (__bridge id)kSecAttrAccount,
|
||||
nil]
|
||||
matches:nil];
|
||||
|
||||
return MPKeyQuery;
|
||||
return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
|
||||
attributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
@"Saved Master Password", (__bridge id)kSecAttrService,
|
||||
user.name, (__bridge id)kSecAttrAccount,
|
||||
nil]
|
||||
matches:nil];
|
||||
}
|
||||
|
||||
static NSDictionary *keyIDQuery() {
|
||||
- (void)forgetSavedKey {
|
||||
|
||||
static NSDictionary *MPKeyIDQuery = nil;
|
||||
if (!MPKeyIDQuery)
|
||||
MPKeyIDQuery = [PearlKeyChain createQueryForClass:kSecClassGenericPassword
|
||||
attributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
@"Master Password Check", (__bridge id)kSecAttrService,
|
||||
@"default", (__bridge id)kSecAttrAccount,
|
||||
nil]
|
||||
matches:nil];
|
||||
|
||||
return MPKeyIDQuery;
|
||||
}
|
||||
|
||||
- (void)forgetKey {
|
||||
|
||||
inf(@"Deleting key and ID from keychain.");
|
||||
if ([PearlKeyChain deleteItemForQuery:keyQuery()] != errSecItemNotFound)
|
||||
if ([PearlKeyChain deleteItemForQuery:keyQuery(self.activeUser)] != errSecItemNotFound) {
|
||||
inf(@"Removed key from keychain.");
|
||||
if ([PearlKeyChain deleteItemForQuery:keyIDQuery()] != errSecItemNotFound)
|
||||
inf(@"Removed key ID from keychain.");
|
||||
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeyForgotten object:self];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeyForgotten object:self];
|
||||
#ifdef TESTFLIGHT_SDK_VERSION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPForgotten];
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPForgotten];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
- (IBAction)signOut:(id)sender {
|
||||
|
||||
[MPConfig get].saveKey = [NSNumber numberWithBool:NO];
|
||||
[self updateKey:nil];
|
||||
[self forgetSavedKey];
|
||||
[self unsetKey];
|
||||
}
|
||||
|
||||
- (void)loadStoredKey {
|
||||
- (void)loadSavedKey {
|
||||
|
||||
if ([[MPConfig get].saveKey boolValue]) {
|
||||
// Key is stored in keychain. Load it.
|
||||
[self updateKey:[PearlKeyChain dataOfItemForQuery:keyQuery()]];
|
||||
if ([self.activeUser.saveKey boolValue]) {
|
||||
// Key should be saved in keychain. Load it.
|
||||
self.key = [PearlKeyChain dataOfItemForQuery:keyQuery(self.activeUser)];
|
||||
inf(@"Looking for key in keychain: %@.", self.key? @"found": @"missing");
|
||||
if (self.key)
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeySet object:self];
|
||||
} else {
|
||||
// Key should not be stored in keychain. Delete it.
|
||||
if ([PearlKeyChain deleteItemForQuery:keyQuery()] != errSecItemNotFound)
|
||||
if ([PearlKeyChain deleteItemForQuery:keyQuery(self.activeUser)] != errSecItemNotFound)
|
||||
inf(@"Removed key from keychain.");
|
||||
#ifdef TESTFLIGHT_SDK_VERSION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPUnstored];
|
||||
@@ -76,20 +57,19 @@ static NSDictionary *keyIDQuery() {
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)tryMasterPassword:(NSString *)tryPassword {
|
||||
- (BOOL)tryMasterPassword:(NSString *)tryPassword forUser:(MPUserEntity *)user {
|
||||
|
||||
if (![tryPassword length])
|
||||
return NO;
|
||||
|
||||
NSData *tryKey = keyForPassword(tryPassword);
|
||||
NSData *tryKeyID = keyIDForKey(tryKey);
|
||||
NSData *keyID = [PearlKeyChain dataOfItemForQuery:keyIDQuery()];
|
||||
inf(@"Key ID known? %@.", keyID? @"YES": @"NO");
|
||||
if (keyID)
|
||||
inf(@"Key ID known? %@.", user.keyID? @"YES": @"NO");
|
||||
if (user.keyID)
|
||||
// A key ID is known -> a password is set.
|
||||
// Make sure the user's entered password matches it.
|
||||
if (![keyID isEqual:tryKeyID]) {
|
||||
wrn(@"Key ID mismatch. Expected: %@, answer: %@.", [keyID encodeHex], [tryKeyID encodeHex]);
|
||||
if (![user.keyID isEqual:tryKeyID]) {
|
||||
wrn(@"Key ID mismatch. Expected: %@, answer: %@.", [user.keyID encodeHex], [tryKeyID encodeHex]);
|
||||
|
||||
#ifdef TESTFLIGHT_SDK_VERSION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPMismatch];
|
||||
@@ -101,55 +81,45 @@ static NSDictionary *keyIDQuery() {
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPEntered];
|
||||
#endif
|
||||
|
||||
[self updateKey:tryKey];
|
||||
if (self.key != tryKey) {
|
||||
self.key = tryKey;
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeySet object:self];
|
||||
}
|
||||
|
||||
self.activeUser = user;
|
||||
|
||||
#ifdef TESTFLIGHT_SDK_VERSION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointSetKey];
|
||||
#endif
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)updateKey:(NSData *)key {
|
||||
- (void)storeSavedKey {
|
||||
|
||||
if (self.key != key) {
|
||||
self.key = key;
|
||||
if ([self.activeUser.saveKey boolValue]) {
|
||||
NSData *existingKey = [PearlKeyChain dataOfItemForQuery:keyQuery(self.activeUser)];
|
||||
|
||||
if (key)
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeySet object:self];
|
||||
else
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeyUnset object:self];
|
||||
}
|
||||
|
||||
if (self.key) {
|
||||
self.keyID = keyIDForKey(self.key);
|
||||
|
||||
NSData *existingKeyID = [PearlKeyChain dataOfItemForQuery:keyIDQuery()];
|
||||
if (![existingKeyID isEqualToData:self.keyID]) {
|
||||
inf(@"Updating key ID in keychain.");
|
||||
[PearlKeyChain addOrUpdateItemForQuery:keyIDQuery()
|
||||
if (![existingKey isEqualToData:self.key]) {
|
||||
inf(@"Updating key in keychain.");
|
||||
[PearlKeyChain addOrUpdateItemForQuery:keyQuery(self.activeUser)
|
||||
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
self.keyID, (__bridge id)kSecValueData,
|
||||
self.key, (__bridge id)kSecValueData,
|
||||
#if TARGET_OS_IPHONE
|
||||
kSecAttrAccessibleWhenUnlocked, (__bridge id)kSecAttrAccessible,
|
||||
kSecAttrAccessibleWhenUnlockedThisDeviceOnly, (__bridge id)kSecAttrAccessible,
|
||||
#endif
|
||||
nil]];
|
||||
}
|
||||
if ([[MPConfig get].saveKey boolValue]) {
|
||||
NSData *existingKey = [PearlKeyChain dataOfItemForQuery:keyQuery()];
|
||||
if (![existingKey isEqualToData:self.key]) {
|
||||
inf(@"Updating key in keychain.");
|
||||
[PearlKeyChain addOrUpdateItemForQuery:keyQuery()
|
||||
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
self.key, (__bridge id)kSecValueData,
|
||||
#if TARGET_OS_IPHONE
|
||||
kSecAttrAccessibleWhenUnlocked, (__bridge id)kSecAttrAccessible,
|
||||
#endif
|
||||
nil]];
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef TESTFLIGHT_SDK_VERSION
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointSetKey];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
- (void)unsetKey {
|
||||
|
||||
self.key = nil;
|
||||
self.activeUser = nil;
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:MPNotificationKeyUnset object:self];
|
||||
}
|
||||
|
||||
- (NSData *)keyWithLength:(NSUInteger)keyLength {
|
||||
|
||||
return [self.key subdataWithRange:NSMakeRange(0, MIN(keyLength, self.key.length))];
|
||||
|
@@ -6,14 +6,16 @@
|
||||
// Copyright (c) 2011 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPEntities.h"
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
@interface MPAppDelegate_Shared : PearlAppDelegate
|
||||
#else
|
||||
@interface MPAppDelegate_Shared : NSObject <PearlConfigDelegate>
|
||||
#endif
|
||||
|
||||
@property (strong, nonatomic) MPUserEntity *activeUser;
|
||||
@property (strong, nonatomic) NSData *key;
|
||||
@property (strong, nonatomic) NSData *keyID;
|
||||
|
||||
+ (MPAppDelegate_Shared *)get;
|
||||
|
||||
|
@@ -11,7 +11,7 @@
|
||||
@implementation MPAppDelegate_Shared
|
||||
|
||||
@synthesize key;
|
||||
@synthesize keyID;
|
||||
@synthesize activeUser;
|
||||
|
||||
+ (MPAppDelegate_Shared *)get {
|
||||
|
||||
|
@@ -27,7 +27,6 @@ typedef enum {
|
||||
|
||||
- (UbiquityStoreManager *)storeManager;
|
||||
- (void)saveContext;
|
||||
- (void)printStore;
|
||||
|
||||
- (MPImportResult)importSites:(NSString *)importedSitesString withPassword:(NSString *)password
|
||||
askConfirmation:(BOOL(^)(NSUInteger importCount, NSUInteger deleteCount))confirmation;
|
||||
|
@@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
#import "MPAppDelegate_Store.h"
|
||||
#import "MPElementEntity.h"
|
||||
#import "MPEntities.h"
|
||||
#import "MPConfig.h"
|
||||
|
||||
@implementation MPAppDelegate_Shared (Store)
|
||||
@@ -57,7 +57,7 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
|
||||
}
|
||||
|
||||
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
|
||||
|
||||
|
||||
// Start loading the store.
|
||||
[self storeManager];
|
||||
|
||||
@@ -69,7 +69,7 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
|
||||
isReady = [self storeManager].isReady;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
assert([self storeManager].isReady);
|
||||
return [self storeManager].persistentStoreCoordinator;
|
||||
}];
|
||||
@@ -134,51 +134,6 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)printStore {
|
||||
|
||||
if (![self managedObjectModel] || ![self managedObjectContext]) {
|
||||
trc(@"Not printing store: store not initialized.");
|
||||
return;
|
||||
}
|
||||
|
||||
[self.managedObjectContext performBlock:^{
|
||||
trc(@"=== All entities ===");
|
||||
for(NSEntityDescription *entity in [[self managedObjectModel] entities]) {
|
||||
NSFetchRequest *request = [NSFetchRequest new];
|
||||
[request setEntity:entity];
|
||||
NSError *error;
|
||||
NSArray *results = [[self managedObjectContext] executeFetchRequest:request error:&error];
|
||||
for(NSManagedObject *o in results) {
|
||||
if ([o isKindOfClass:[MPElementEntity class]]) {
|
||||
MPElementEntity *e = (MPElementEntity *)o;
|
||||
trc(@"For descriptor: %@, found: %@: %@ (%@)", entity.name, [o class], e.name, e.keyID);
|
||||
} else {
|
||||
trc(@"For descriptor: %@, found: %@", entity.name, [o class]);
|
||||
}
|
||||
}
|
||||
}
|
||||
trc(@"---");
|
||||
if ([MPAppDelegate_Shared get].keyID) {
|
||||
trc(@"=== Known sites ===");
|
||||
NSFetchRequest *fetchRequest = [[self managedObjectModel]
|
||||
fetchRequestFromTemplateWithName:@"MPElements"
|
||||
substitutionVariables:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
@"", @"query",
|
||||
[MPAppDelegate_Shared get].keyID, @"keyID",
|
||||
nil]];
|
||||
[fetchRequest setSortDescriptors:
|
||||
[NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]]];
|
||||
|
||||
NSError *error = nil;
|
||||
for (MPElementEntity *e in [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error]) {
|
||||
trc(@"Found site: %@ (%@): %@", e.name, e.keyID, e);
|
||||
}
|
||||
trc(@"---");
|
||||
} else
|
||||
trc(@"Not printing sites: master password not set.");
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - UbiquityStoreManagerDelegate
|
||||
|
||||
- (NSManagedObjectContext *)managedObjectContextForUbiquityStoreManager:(UbiquityStoreManager *)usm {
|
||||
@@ -278,7 +233,8 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
|
||||
if (!headerPattern || !sitePattern)
|
||||
return MPImportResultInternalError;
|
||||
|
||||
NSString *keyIDHex = nil;
|
||||
NSString *keyIDHex = nil, *userName = nil;
|
||||
MPUserEntity *user = nil;
|
||||
BOOL headerStarted = NO, headerEnded = NO;
|
||||
NSArray *importedSiteLines = [importedSitesString componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
|
||||
NSMutableSet *elementsToDelete = [NSMutableSet set];
|
||||
@@ -307,6 +263,13 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
|
||||
NSTextCheckingResult *headerElements = [[headerPattern matchesInString:importedSiteLine options:0 range:NSMakeRange(0, [importedSiteLine length])] lastObject];
|
||||
NSString *key = [importedSiteLine substringWithRange:[headerElements rangeAtIndex:1]];
|
||||
NSString *value = [importedSiteLine substringWithRange:[headerElements rangeAtIndex:2]];
|
||||
if ([key isEqualToString:@"User Name"]) {
|
||||
userName = value;
|
||||
|
||||
NSFetchRequest *userFetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPUserEntity class])];
|
||||
userFetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@", userName];
|
||||
user = [[self.managedObjectContext executeFetchRequest:fetchRequest error:&error] lastObject];
|
||||
}
|
||||
if ([key isEqualToString:@"Key ID"]) {
|
||||
if (![(keyIDHex = value) isEqualToString:[keyIDForPassword(password) encodeHex]])
|
||||
return MPImportResultInvalidPassword;
|
||||
@@ -316,7 +279,7 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
|
||||
}
|
||||
if (!headerEnded)
|
||||
continue;
|
||||
if (!keyIDHex)
|
||||
if (!keyIDHex || ![userName length])
|
||||
return MPImportResultMalformedInput;
|
||||
if (![importedSiteLine length])
|
||||
continue;
|
||||
@@ -334,15 +297,17 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
|
||||
NSString *exportContent = [importedSiteLine substringWithRange:[siteElements rangeAtIndex:5]];
|
||||
|
||||
// Find existing site.
|
||||
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@ AND keyID == %@", name, keyIDHex];
|
||||
NSArray *existingSites = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
|
||||
if (error)
|
||||
err(@"Couldn't search existing sites: %@", error);
|
||||
if (!existingSites)
|
||||
return MPImportResultInternalError;
|
||||
|
||||
[elementsToDelete addObjectsFromArray:existingSites];
|
||||
[importedSiteElements addObject:[NSArray arrayWithObjects:lastUsed, uses, type, name, exportContent, nil]];
|
||||
if (user) {
|
||||
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"name == %@ AND user == %@", name, user];
|
||||
NSArray *existingSites = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
|
||||
if (error)
|
||||
err(@"Couldn't search existing sites: %@", error);
|
||||
if (!existingSites)
|
||||
return MPImportResultInternalError;
|
||||
|
||||
[elementsToDelete addObjectsFromArray:existingSites];
|
||||
[importedSiteElements addObject:[NSArray arrayWithObjects:lastUsed, uses, type, name, exportContent, nil]];
|
||||
}
|
||||
}
|
||||
|
||||
// Ask for confirmation to import these sites.
|
||||
@@ -357,6 +322,11 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
|
||||
[self saveContext];
|
||||
|
||||
// Import new sites.
|
||||
if (!user) {
|
||||
user = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPUserEntity class]) inManagedObjectContext:self.managedObjectContext];
|
||||
user.name = userName;
|
||||
user.keyID = [keyIDHex decodeHex];
|
||||
}
|
||||
for (NSArray *siteElements in importedSiteElements) {
|
||||
NSDate *lastUsed = [rfc3339DateFormatter dateFromString:[siteElements objectAtIndex:0]];
|
||||
NSInteger uses = [[siteElements objectAtIndex:1] integerValue];
|
||||
@@ -365,14 +335,13 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
|
||||
NSString *exportContent = [siteElements objectAtIndex:4];
|
||||
|
||||
// Create new site.
|
||||
inf(@"Importing site: name=%@, lastUsed=%@, uses=%d, type=%u, keyID=%@", name, lastUsed, uses, type, keyIDHex);
|
||||
MPElementEntity *element = [NSEntityDescription insertNewObjectForEntityForName:ClassNameFromMPElementType(type)
|
||||
inManagedObjectContext:self.managedObjectContext];
|
||||
element.name = name;
|
||||
element.keyID = [keyIDHex decodeHex];
|
||||
element.type = type;
|
||||
element.uses = uses;
|
||||
element.lastUsed = [lastUsed timeIntervalSinceReferenceDate];
|
||||
element.user = user;
|
||||
element.type = [NSNumber numberWithUnsignedInteger:type];
|
||||
element.uses = [NSNumber numberWithUnsignedInteger:uses];
|
||||
element.lastUsed = lastUsed;
|
||||
if ([exportContent length])
|
||||
[element importContent:exportContent];
|
||||
}
|
||||
@@ -399,7 +368,8 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
|
||||
[export appendFormat:@"# \n"];
|
||||
[export appendFormat:@"##\n"];
|
||||
[export appendFormat:@"# Version: %@\n", [PearlInfoPlist get].CFBundleVersion];
|
||||
[export appendFormat:@"# Key ID: %@\n", [self.keyID encodeHex]];
|
||||
[export appendFormat:@"# User Name: %@\n", self.activeUser.name];
|
||||
[export appendFormat:@"# Key ID: %@\n", [self.activeUser.keyID encodeHex]];
|
||||
[export appendFormat:@"# Date: %@\n", [rfc3339DateFormatter stringFromDate:[NSDate date]]];
|
||||
if (showPasswords)
|
||||
[export appendFormat:@"# Passwords: VISIBLE\n"];
|
||||
@@ -411,17 +381,9 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
|
||||
[export appendFormat:@"# used used type name\tpassword\n"];
|
||||
|
||||
// Sites.
|
||||
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
|
||||
fetchRequest.sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]];
|
||||
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"keyID == %@", self.keyID];
|
||||
__autoreleasing NSError *error = nil;
|
||||
NSArray *elements = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
|
||||
if (error)
|
||||
err(@"Error fetching sites for export: %@", error);
|
||||
|
||||
for (MPElementEntity *element in elements) {
|
||||
NSTimeInterval lastUsed = element.lastUsed;
|
||||
int16_t uses = element.uses;
|
||||
for (MPElementEntity *element in self.activeUser.elements) {
|
||||
NSDate *lastUsed = element.lastUsed;
|
||||
NSNumber *uses = element.uses;
|
||||
MPElementType type = (unsigned)element.type;
|
||||
NSString *name = element.name;
|
||||
NSString *content = nil;
|
||||
@@ -435,7 +397,7 @@ static NSDateFormatter *rfc3339DateFormatter = nil;
|
||||
}
|
||||
|
||||
[export appendFormat:@"%@ %8d %8d %20s\t%@\n",
|
||||
[rfc3339DateFormatter stringFromDate:[NSDate dateWithTimeIntervalSinceReferenceDate:lastUsed]], uses, type, [name cStringUsingEncoding:NSUTF8StringEncoding], content? content: @""];
|
||||
[rfc3339DateFormatter stringFromDate:lastUsed], uses, type, [name cStringUsingEncoding:NSUTF8StringEncoding], content? content: @""];
|
||||
}
|
||||
|
||||
#ifdef TESTFLIGHT_SDK_VERSION
|
||||
|
@@ -8,8 +8,7 @@
|
||||
|
||||
@interface MPConfig : PearlConfig
|
||||
|
||||
@property (nonatomic, retain) NSNumber *saveKey;
|
||||
@property (nonatomic, retain) NSNumber *rememberKey;
|
||||
@property (nonatomic, retain) NSNumber *rememberLogin;
|
||||
|
||||
@property (nonatomic, retain) NSNumber *iCloud;
|
||||
@property (nonatomic, retain) NSNumber *iCloudDecided;
|
||||
|
@@ -10,7 +10,7 @@
|
||||
#import "MPAppDelegate.h"
|
||||
|
||||
@implementation MPConfig
|
||||
@dynamic saveKey, rememberKey, iCloud, iCloudDecided;
|
||||
@dynamic rememberLogin, iCloud, iCloudDecided;
|
||||
|
||||
- (id)init {
|
||||
|
||||
@@ -20,8 +20,7 @@
|
||||
[self.defaults registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(askForReviews)),
|
||||
|
||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(saveKey)),
|
||||
[NSNumber numberWithBool:YES], NSStringFromSelector(@selector(rememberKey)),
|
||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(rememberLogin)),
|
||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(iCloud)),
|
||||
[NSNumber numberWithBool:NO], NSStringFromSelector(@selector(iCloudDecided)),
|
||||
nil]];
|
||||
|
@@ -1,27 +1,23 @@
|
||||
//
|
||||
// MPElementEntity.h
|
||||
// MasterPassword
|
||||
// MasterPassword-iOS
|
||||
//
|
||||
// Created by Maarten Billemont on 02/01/12.
|
||||
// Created by Maarten Billemont on 04/06/12.
|
||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
|
||||
@class MPUserEntity;
|
||||
|
||||
@interface MPElementEntity : NSManagedObject
|
||||
|
||||
@property (nonatomic, retain) NSString *name;
|
||||
@property (nonatomic, retain) NSData *keyID;
|
||||
@property (nonatomic, assign) int16_t type;
|
||||
@property (nonatomic, assign) int16_t uses;
|
||||
@property (nonatomic, assign) NSTimeInterval lastUsed;
|
||||
|
||||
@property (nonatomic, retain, readonly) id content;
|
||||
|
||||
- (int16_t)use;
|
||||
- (NSString *)exportContent;
|
||||
- (void)importContent:(NSString *)content;
|
||||
@property (nonatomic, retain) id content;
|
||||
@property (nonatomic, retain) NSDate * lastUsed;
|
||||
@property (nonatomic, retain) NSString * name;
|
||||
@property (nonatomic, retain) NSNumber * type;
|
||||
@property (nonatomic, retain) NSNumber * uses;
|
||||
@property (nonatomic, retain) MPUserEntity *user;
|
||||
|
||||
@end
|
||||
|
@@ -1,51 +1,22 @@
|
||||
//
|
||||
// MPElementEntity.m
|
||||
// MasterPassword
|
||||
// MasterPassword-iOS
|
||||
//
|
||||
// Created by Maarten Billemont on 02/01/12.
|
||||
// Created by Maarten Billemont on 04/06/12.
|
||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPElementEntity.h"
|
||||
#import "MPUserEntity.h"
|
||||
|
||||
|
||||
@implementation MPElementEntity
|
||||
|
||||
@dynamic content;
|
||||
@dynamic lastUsed;
|
||||
@dynamic name;
|
||||
@dynamic keyID;
|
||||
@dynamic type;
|
||||
@dynamic uses;
|
||||
@dynamic lastUsed;
|
||||
|
||||
- (int16_t)use {
|
||||
|
||||
self.lastUsed = [[NSDate date] timeIntervalSinceReferenceDate];
|
||||
return ++self.uses;
|
||||
}
|
||||
|
||||
- (id)content {
|
||||
|
||||
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Content implementation missing." userInfo:nil];
|
||||
}
|
||||
|
||||
- (NSString *)exportContent {
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)importContent:(NSString *)content {
|
||||
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
|
||||
return PearlString(@"%@:%@", [self class], [self name]);
|
||||
}
|
||||
|
||||
- (NSString *)debugDescription {
|
||||
|
||||
return PearlString(@"{%@: name=%@, keyID=%@, type=%d, uses=%d, lastUsed=%@}",
|
||||
NSStringFromClass([self class]), self.name, self.keyID, self.type, self.uses, self.lastUsed);
|
||||
}
|
||||
@dynamic user;
|
||||
|
||||
@end
|
||||
|
@@ -1,8 +1,8 @@
|
||||
//
|
||||
// MPElementGeneratedEntity.h
|
||||
// MasterPassword
|
||||
// MasterPassword-iOS
|
||||
//
|
||||
// Created by Maarten Billemont on 16/01/12.
|
||||
// Created by Maarten Billemont on 04/06/12.
|
||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
|
||||
@interface MPElementGeneratedEntity : MPElementEntity
|
||||
|
||||
@property (nonatomic, assign) int32_t counter;
|
||||
@property (nonatomic, retain) NSNumber * counter;
|
||||
|
||||
@end
|
||||
|
@@ -1,31 +1,16 @@
|
||||
//
|
||||
// MPElementGeneratedEntity.m
|
||||
// MasterPassword
|
||||
// MasterPassword-iOS
|
||||
//
|
||||
// Created by Maarten Billemont on 16/01/12.
|
||||
// Created by Maarten Billemont on 04/06/12.
|
||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPElementGeneratedEntity.h"
|
||||
#import "MPAppDelegate.h"
|
||||
#import "MPAppDelegate_Key.h"
|
||||
|
||||
|
||||
@implementation MPElementGeneratedEntity
|
||||
|
||||
@dynamic counter;
|
||||
|
||||
- (id)content {
|
||||
|
||||
if (!(self.type & MPElementTypeClassGenerated)) {
|
||||
err(@"Corrupt element: %@, type: %d is not in MPElementTypeClassGenerated", self.name, self.type);
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![self.name length])
|
||||
return nil;
|
||||
|
||||
return MPCalculateContent((unsigned)self.type, self.name, [MPAppDelegate get].key, self.counter);
|
||||
}
|
||||
|
||||
@end
|
||||
|
@@ -1,8 +1,8 @@
|
||||
//
|
||||
// MPElementStoredEntity.h
|
||||
// MasterPassword
|
||||
// MasterPassword-iOS
|
||||
//
|
||||
// Created by Maarten Billemont on 02/01/12.
|
||||
// Created by Maarten Billemont on 04/06/12.
|
||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
|
||||
@interface MPElementStoredEntity : MPElementEntity
|
||||
|
||||
@property (nonatomic, retain, readwrite) id content;
|
||||
@property (nonatomic, retain) id contentObject;
|
||||
|
||||
@end
|
||||
|
@@ -1,76 +1,16 @@
|
||||
//
|
||||
// MPElementStoredEntity.m
|
||||
// MasterPassword
|
||||
// MasterPassword-iOS
|
||||
//
|
||||
// Created by Maarten Billemont on 02/01/12.
|
||||
// Created by Maarten Billemont on 04/06/12.
|
||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPElementStoredEntity.h"
|
||||
#import "MPAppDelegate.h"
|
||||
#import "MPAppDelegate_Key.h"
|
||||
|
||||
@interface MPElementStoredEntity ()
|
||||
|
||||
@property (nonatomic, retain, readwrite) id contentObject;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MPElementStoredEntity
|
||||
|
||||
@dynamic contentObject;
|
||||
|
||||
+ (NSDictionary *)queryForDevicePrivateElementNamed:(NSString *)name {
|
||||
|
||||
return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
|
||||
attributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
@"DevicePrivate", (__bridge id)kSecAttrService,
|
||||
name, (__bridge id)kSecAttrAccount,
|
||||
nil]
|
||||
matches:nil];
|
||||
}
|
||||
|
||||
- (id)content {
|
||||
|
||||
assert(self.type & MPElementTypeClassStored);
|
||||
|
||||
NSData *encryptedContent;
|
||||
if (self.type & MPElementFeatureDevicePrivate)
|
||||
encryptedContent = [PearlKeyChain dataOfItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]];
|
||||
else
|
||||
encryptedContent = self.contentObject;
|
||||
|
||||
NSData *decryptedContent = [encryptedContent decryptWithSymmetricKey:[[MPAppDelegate get] keyWithLength:PearlCryptKeySize]
|
||||
padding:YES];
|
||||
return [[NSString alloc] initWithBytes:decryptedContent.bytes length:decryptedContent.length encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
- (void)setContent:(id)content {
|
||||
|
||||
NSData *encryptedContent = [[content description] encryptWithSymmetricKey:[[MPAppDelegate get] keyWithLength:PearlCryptKeySize]
|
||||
padding:YES];
|
||||
|
||||
if (self.type & MPElementFeatureDevicePrivate) {
|
||||
[PearlKeyChain addOrUpdateItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]
|
||||
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
encryptedContent, (__bridge id)kSecValueData,
|
||||
#if TARGET_OS_IPHONE
|
||||
kSecAttrAccessibleWhenUnlockedThisDeviceOnly, (__bridge id)kSecAttrAccessible,
|
||||
#endif
|
||||
nil]];
|
||||
self.contentObject = nil;
|
||||
} else
|
||||
self.contentObject = encryptedContent;
|
||||
}
|
||||
|
||||
- (NSString *)exportContent {
|
||||
|
||||
return [self.contentObject encodeBase64];
|
||||
}
|
||||
|
||||
- (void)importContent:(NSString *)content {
|
||||
|
||||
self.contentObject = [content decodeBase64];
|
||||
}
|
||||
|
||||
@end
|
||||
|
21
MasterPassword/MPEntities.h
Normal file
21
MasterPassword/MPEntities.h
Normal file
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// MPElementEntities.h
|
||||
// MasterPassword-iOS
|
||||
//
|
||||
// Created by Maarten Billemont on 31/05/12.
|
||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "MPElementEntity.h"
|
||||
#import "MPElementStoredEntity.h"
|
||||
#import "MPElementGeneratedEntity.h"
|
||||
#import "MPUserEntity.h"
|
||||
|
||||
@interface MPElementEntity (MP)
|
||||
|
||||
- (NSNumber *)use;
|
||||
- (NSString *)exportContent;
|
||||
- (void)importContent:(NSString *)content;
|
||||
|
||||
@end
|
122
MasterPassword/MPEntities.m
Normal file
122
MasterPassword/MPEntities.m
Normal file
@@ -0,0 +1,122 @@
|
||||
//
|
||||
// MPElementEntities.m
|
||||
// MasterPassword-iOS
|
||||
//
|
||||
// Created by Maarten Billemont on 31/05/12.
|
||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPEntities.h"
|
||||
#import "MPAppDelegate.h"
|
||||
#import "MPAppDelegate_Key.h"
|
||||
|
||||
@implementation MPElementEntity (MP)
|
||||
|
||||
- (NSNumber *)use {
|
||||
|
||||
self.lastUsed = [NSDate date];
|
||||
self.uses = [NSNumber numberWithUnsignedInteger:[self.uses unsignedIntegerValue] + 1];
|
||||
|
||||
return self.uses;
|
||||
}
|
||||
|
||||
- (id)content {
|
||||
|
||||
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Content implementation missing." userInfo:nil];
|
||||
}
|
||||
|
||||
- (NSString *)exportContent {
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)importContent:(NSString *)content {
|
||||
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
|
||||
return PearlString(@"%@:%@", [self class], [self name]);
|
||||
}
|
||||
|
||||
- (NSString *)debugDescription {
|
||||
|
||||
return PearlString(@"{%@: name=%@, user=%@, type=%d, uses=%d, lastUsed=%@}",
|
||||
NSStringFromClass([self class]), self.name, self.user.name, self.type, self.uses, self.lastUsed);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MPElementGeneratedEntity (MP)
|
||||
|
||||
- (id)content {
|
||||
|
||||
if (!([self.type unsignedIntegerValue] & MPElementTypeClassGenerated)) {
|
||||
err(@"Corrupt element: %@, type: %d is not in MPElementTypeClassGenerated", self.name, self.type);
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (![self.name length])
|
||||
return nil;
|
||||
|
||||
return MPCalculateContent([self.type unsignedIntegerValue], self.name, [MPAppDelegate get].key, [self.counter unsignedIntegerValue]);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation MPElementStoredEntity (MP)
|
||||
|
||||
+ (NSDictionary *)queryForDevicePrivateElementNamed:(NSString *)name {
|
||||
|
||||
return [PearlKeyChain createQueryForClass:kSecClassGenericPassword
|
||||
attributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
@"DevicePrivate", (__bridge id)kSecAttrService,
|
||||
name, (__bridge id)kSecAttrAccount,
|
||||
nil]
|
||||
matches:nil];
|
||||
}
|
||||
|
||||
- (id)content {
|
||||
|
||||
assert([self.type unsignedIntegerValue] & MPElementTypeClassStored);
|
||||
|
||||
NSData *encryptedContent;
|
||||
if ([self.type unsignedIntegerValue] & MPElementFeatureDevicePrivate)
|
||||
encryptedContent = [PearlKeyChain dataOfItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]];
|
||||
else
|
||||
encryptedContent = self.contentObject;
|
||||
|
||||
NSData *decryptedContent = [encryptedContent decryptWithSymmetricKey:[[MPAppDelegate get] keyWithLength:PearlCryptKeySize]
|
||||
padding:YES];
|
||||
return [[NSString alloc] initWithBytes:decryptedContent.bytes length:decryptedContent.length encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
- (void)setContent:(id)content {
|
||||
|
||||
NSData *encryptedContent = [[content description] encryptWithSymmetricKey:[[MPAppDelegate get] keyWithLength:PearlCryptKeySize]
|
||||
padding:YES];
|
||||
|
||||
if ([self.type unsignedIntegerValue] & MPElementFeatureDevicePrivate) {
|
||||
[PearlKeyChain addOrUpdateItemForQuery:[MPElementStoredEntity queryForDevicePrivateElementNamed:self.name]
|
||||
withAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
encryptedContent, (__bridge id)kSecValueData,
|
||||
#if TARGET_OS_IPHONE
|
||||
kSecAttrAccessibleWhenUnlockedThisDeviceOnly, (__bridge id)kSecAttrAccessible,
|
||||
#endif
|
||||
nil]];
|
||||
self.contentObject = nil;
|
||||
} else
|
||||
self.contentObject = encryptedContent;
|
||||
}
|
||||
|
||||
- (NSString *)exportContent {
|
||||
|
||||
return [self.contentObject encodeBase64];
|
||||
}
|
||||
|
||||
- (void)importContent:(NSString *)content {
|
||||
|
||||
self.contentObject = [content decodeBase64];
|
||||
}
|
||||
|
||||
@end
|
@@ -83,4 +83,4 @@ NSData *keyIDForKey(NSData *key);
|
||||
NSString *NSStringFromMPElementType(MPElementType type);
|
||||
NSString *ClassNameFromMPElementType(MPElementType type);
|
||||
Class ClassFromMPElementType(MPElementType type);
|
||||
NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *key, int32_t counter);
|
||||
NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *key, uint32_t counter);
|
||||
|
@@ -104,7 +104,7 @@ NSString *ClassNameFromMPElementType(MPElementType type) {
|
||||
}
|
||||
|
||||
static NSDictionary *MPTypes_ciphers = nil;
|
||||
NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *key, int32_t counter) {
|
||||
NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *key, uint32_t counter) {
|
||||
|
||||
if (!(type & MPElementTypeClassGenerated)) {
|
||||
err(@"Incorrect type (is not MPElementTypeClassGenerated): %d, for: %@", type, name);
|
||||
@@ -118,7 +118,7 @@ NSString *MPCalculateContent(MPElementType type, NSString *name, NSData *key, in
|
||||
err(@"Key not set.");
|
||||
return nil;
|
||||
}
|
||||
uint32_t salt = (unsigned)counter;
|
||||
uint32_t salt = counter;
|
||||
if (!counter)
|
||||
// Counter unset, go into OTP mode.
|
||||
// Get the UNIX timestamp of the start of the interval of 5 minutes that the current time is in.
|
||||
|
31
MasterPassword/MPUserEntity.h
Normal file
31
MasterPassword/MPUserEntity.h
Normal file
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// MPUserEntity.h
|
||||
// MasterPassword-iOS
|
||||
//
|
||||
// Created by Maarten Billemont on 04/06/12.
|
||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
|
||||
@class MPElementEntity;
|
||||
|
||||
@interface MPUserEntity : NSManagedObject
|
||||
|
||||
@property (nonatomic, retain) NSData * keyID;
|
||||
@property (nonatomic, retain) NSDate * lastUsed;
|
||||
@property (nonatomic, retain) NSString * name;
|
||||
@property (nonatomic, retain) NSNumber * saveKey;
|
||||
@property (nonatomic, retain) NSNumber * avatar;
|
||||
@property (nonatomic, retain) NSSet *elements;
|
||||
@end
|
||||
|
||||
@interface MPUserEntity (CoreDataGeneratedAccessors)
|
||||
|
||||
- (void)addElementsObject:(MPElementEntity *)value;
|
||||
- (void)removeElementsObject:(MPElementEntity *)value;
|
||||
- (void)addElements:(NSSet *)values;
|
||||
- (void)removeElements:(NSSet *)values;
|
||||
|
||||
@end
|
22
MasterPassword/MPUserEntity.m
Normal file
22
MasterPassword/MPUserEntity.m
Normal file
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// MPUserEntity.m
|
||||
// MasterPassword-iOS
|
||||
//
|
||||
// Created by Maarten Billemont on 04/06/12.
|
||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPUserEntity.h"
|
||||
#import "MPElementEntity.h"
|
||||
|
||||
|
||||
@implementation MPUserEntity
|
||||
|
||||
@dynamic keyID;
|
||||
@dynamic lastUsed;
|
||||
@dynamic name;
|
||||
@dynamic saveKey;
|
||||
@dynamic avatar;
|
||||
@dynamic elements;
|
||||
|
||||
@end
|
@@ -115,8 +115,8 @@
|
||||
|
||||
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
|
||||
fetchRequest.sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:@"uses" ascending:NO]];
|
||||
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name BEGINSWITH[cd] %@) AND keyID == %@",
|
||||
query, query, [MPAppDelegate get].keyID];
|
||||
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name BEGINSWITH[cd] %@) AND user == %@",
|
||||
query, query, [MPAppDelegate get].activeUser];
|
||||
|
||||
NSError *error = nil;
|
||||
self.siteResults = [[MPAppDelegate managedObjectContext] executeFetchRequest:fetchRequest error:&error];
|
||||
|
@@ -1,11 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model name="" userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1171" systemVersion="11E53" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
|
||||
<entity name="MPElementEntity" representedClassName="MPElementEntity" isAbstract="YES" syncable="YES">
|
||||
<attribute name="keyID" attributeType="Binary" indexed="YES" syncable="YES" isSyncIdentityProperty="YES"/>
|
||||
<attribute name="content" optional="YES" transient="YES" attributeType="Transformable" syncable="YES"/>
|
||||
<attribute name="lastUsed" attributeType="Date" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" minValueString="1" indexed="YES" syncable="YES" isSyncIdentityProperty="YES"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="16" syncable="YES"/>
|
||||
<attribute name="uses" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
|
||||
<relationship name="user" minCount="1" maxCount="1" deletionRule="Nullify" destinationEntity="MPUserEntity" inverseName="elements" inverseEntity="MPUserEntity" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="MPElementGeneratedEntity" representedClassName="MPElementGeneratedEntity" parentEntity="MPElementEntity" syncable="YES">
|
||||
<attribute name="counter" optional="YES" attributeType="Integer 32" defaultValueString="1" syncable="YES"/>
|
||||
@@ -13,9 +14,18 @@
|
||||
<entity name="MPElementStoredEntity" representedClassName="MPElementStoredEntity" parentEntity="MPElementEntity" syncable="YES">
|
||||
<attribute name="contentObject" optional="YES" attributeType="Transformable" storedInTruthFile="YES" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="MPUserEntity" representedClassName="MPUserEntity" syncable="YES">
|
||||
<attribute name="avatar" optional="YES" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
|
||||
<attribute name="keyID" optional="YES" attributeType="Binary" syncable="YES"/>
|
||||
<attribute name="lastUsed" optional="YES" attributeType="Date" syncable="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="saveKey" optional="YES" attributeType="Boolean" syncable="YES"/>
|
||||
<relationship name="elements" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="MPElementEntity" inverseName="user" inverseEntity="MPElementEntity" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="MPElementEntity" positionX="160" positionY="192" width="128" height="120"/>
|
||||
<element name="MPElementGeneratedEntity" positionX="160" positionY="192" width="128" height="60"/>
|
||||
<element name="MPElementStoredEntity" positionX="160" positionY="192" width="128" height="60"/>
|
||||
<element name="MPUserEntity" positionX="160" positionY="192" width="128" height="120"/>
|
||||
</elements>
|
||||
</model>
|
@@ -62,20 +62,15 @@
|
||||
|
||||
if (!self.key)
|
||||
// Try and load the key from the keychain.
|
||||
[self loadStoredKey];
|
||||
[self loadSavedKey];
|
||||
|
||||
if (!self.key)
|
||||
// Ask the user to set the key through his master password.
|
||||
if ([NSThread isMainThread])
|
||||
PearlMainThread(^{
|
||||
[self.navigationController presentViewController:
|
||||
[self.navigationController.storyboard instantiateViewControllerWithIdentifier:@"MPUnlockViewController"]
|
||||
animated:animated completion:nil];
|
||||
else
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.navigationController presentViewController:
|
||||
[self.navigationController.storyboard instantiateViewControllerWithIdentifier:@"MPUnlockViewController"]
|
||||
animated:animated completion:nil];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
- (void)export {
|
||||
@@ -91,7 +86,7 @@
|
||||
@"making the result safe from falling in the wrong hands.\n\n"
|
||||
@"If all your passwords are shown and somebody else finds the export, "
|
||||
@"they could gain access to all your sites!"
|
||||
viewStyle:UIAlertViewStyleDefault tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||
viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||
if (buttonIndex == [alert firstOtherButtonIndex] + 0)
|
||||
// Safe Export
|
||||
[self exportShowPasswords:NO];
|
||||
@@ -132,12 +127,6 @@
|
||||
|
||||
- (void)checkConfig {
|
||||
|
||||
if ([[MPConfig get].saveKey boolValue]) {
|
||||
if (self.key)
|
||||
[self updateKey:self.key];
|
||||
} else
|
||||
[self loadStoredKey];
|
||||
|
||||
if ([[MPConfig get].iCloud boolValue] != [self.storeManager iCloudEnabled])
|
||||
[self.storeManager useiCloudStore:[[MPConfig get].iCloud boolValue] alertUser:YES];
|
||||
}
|
||||
@@ -315,7 +304,7 @@
|
||||
NSString *importedSitesString = [[NSString alloc] initWithData:importedSitesData encoding:NSUTF8StringEncoding];
|
||||
[PearlAlert showAlertWithTitle:@"Import Password" message:
|
||||
@"Enter the master password for this export:"
|
||||
viewStyle:UIAlertViewStyleSecureTextInput tappedButtonBlock:
|
||||
viewStyle:UIAlertViewStyleSecureTextInput initAlert:nil tappedButtonBlock:
|
||||
^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
MPImportResult result = [self importSites:importedSitesString withPassword:[alert textFieldAtIndex:0].text
|
||||
@@ -328,6 +317,7 @@
|
||||
[PearlAlert showAlertWithTitle:@"Import Sites?"
|
||||
message:PearlLocalize(@"Import %d sites, overwriting %d existing sites?", importCount, deleteCount)
|
||||
viewStyle:UIAlertViewStyleDefault
|
||||
initAlert:nil
|
||||
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||
if (buttonIndex != [alert cancelButtonIndex])
|
||||
confirmation = YES;
|
||||
@@ -409,8 +399,8 @@
|
||||
|
||||
[self saveContext];
|
||||
|
||||
if (![[MPiOSConfig get].rememberKey boolValue]) {
|
||||
[self updateKey:nil];
|
||||
if (![[MPiOSConfig get].rememberLogin boolValue]) {
|
||||
[self unsetKey];
|
||||
[self loadKey:NO];
|
||||
}
|
||||
|
||||
@@ -455,7 +445,7 @@
|
||||
message:
|
||||
@"iCloud is now disabled.\n\n"
|
||||
@"It is highly recommended you enable iCloud."
|
||||
viewStyle:UIAlertViewStyleDefault tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||
viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||
if (buttonIndex == [alert firstOtherButtonIndex] + 0) {
|
||||
[PearlAlert showAlertWithTitle:@"About iCloud"
|
||||
message:
|
||||
@@ -471,6 +461,7 @@
|
||||
@"with your master password.\n\n"
|
||||
@"Apple can never see any of your passwords."
|
||||
viewStyle:UIAlertViewStyleDefault
|
||||
initAlert:nil
|
||||
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||
[self ubiquityStoreManager:manager didSwitchToiCloud:iCloudEnabled];
|
||||
}
|
||||
|
@@ -9,9 +9,8 @@
|
||||
#import "MPTypeViewController.h"
|
||||
#import "MPElementEntity.h"
|
||||
#import "MPSearchDelegate.h"
|
||||
#import "IASKAppSettingsViewController.h"
|
||||
|
||||
@interface MPMainViewController : UIViewController <MPTypeDelegate, UITextFieldDelegate, MPSearchResultsDelegate, UIWebViewDelegate, IASKSettingsDelegate>
|
||||
@interface MPMainViewController : UIViewController <MPTypeDelegate, UITextFieldDelegate, MPSearchResultsDelegate, UIWebViewDelegate>
|
||||
|
||||
@property (strong, nonatomic) MPElementEntity *activeElement;
|
||||
@property (strong, nonatomic) IBOutlet MPSearchDelegate *searchResultsController;
|
||||
|
@@ -10,8 +10,7 @@
|
||||
#import "MPAppDelegate.h"
|
||||
#import "MPAppDelegate_Key.h"
|
||||
#import "MPAppDelegate_Store.h"
|
||||
#import "MPElementGeneratedEntity.h"
|
||||
#import "MPElementStoredEntity.h"
|
||||
#import "MPEntities.h"
|
||||
#import "IASKAppSettingsViewController.h"
|
||||
#import "ATConnect.h"
|
||||
|
||||
@@ -74,7 +73,7 @@
|
||||
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
if (![self.activeElement.keyID isEqualToData:[MPAppDelegate get].keyID])
|
||||
if (self.activeElement.user != [MPAppDelegate get].activeUser)
|
||||
self.activeElement = nil;
|
||||
self.searchDisplayController.searchBar.text = nil;
|
||||
|
||||
@@ -157,9 +156,9 @@
|
||||
[self setHelpChapter:self.activeElement? @"2": @"1"];
|
||||
self.siteName.text = self.activeElement.name;
|
||||
|
||||
self.passwordCounter.alpha = self.activeElement.type & MPElementTypeClassGenerated? 0.5f: 0;
|
||||
self.passwordIncrementer.alpha = self.activeElement.type & MPElementTypeClassGenerated? 0.5f: 0;
|
||||
self.passwordEdit.alpha = self.activeElement.type & MPElementTypeClassStored? 0.5f: 0;
|
||||
self.passwordCounter.alpha = [self.activeElement.type unsignedIntegerValue] & MPElementTypeClassGenerated? 0.5f: 0;
|
||||
self.passwordIncrementer.alpha = [self.activeElement.type unsignedIntegerValue] & MPElementTypeClassGenerated? 0.5f: 0;
|
||||
self.passwordEdit.alpha = [self.activeElement.type unsignedIntegerValue] & MPElementTypeClassStored? 0.5f: 0;
|
||||
|
||||
[self.typeButton setTitle:NSStringFromMPElementType((unsigned)self.activeElement.type)
|
||||
forState:UIControlStateNormal];
|
||||
@@ -300,7 +299,7 @@
|
||||
@"You will then need to update your account's old password to this newly generated password.\n\n"
|
||||
@"You can reset the counter by holding down on this button."
|
||||
do:^{
|
||||
++((MPElementGeneratedEntity *) self.activeElement).counter;
|
||||
PearlUnsignedIntegerOp([((MPElementGeneratedEntity *) self.activeElement) counter], +1);
|
||||
}];
|
||||
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointIncrementPasswordCounter];
|
||||
@@ -314,7 +313,7 @@
|
||||
if (![self.activeElement isKindOfClass:[MPElementGeneratedEntity class]])
|
||||
// Not of a type that supports a password counter.
|
||||
return;
|
||||
if (((MPElementGeneratedEntity *)self.activeElement).counter == 1)
|
||||
if ([((MPElementGeneratedEntity *)self.activeElement).counter unsignedIntegerValue] == 1)
|
||||
// Counter has initial value, no point resetting.
|
||||
return;
|
||||
|
||||
@@ -323,7 +322,7 @@
|
||||
@"If you continue, the site's password will change back to its original value. "
|
||||
@"You will then need to update your account's password back to this original value."
|
||||
do:^{
|
||||
((MPElementGeneratedEntity *) self.activeElement).counter = 1;
|
||||
((MPElementGeneratedEntity *) self.activeElement).counter = PearlUnsignedInteger(1);
|
||||
}];
|
||||
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointResetPasswordCounter];
|
||||
@@ -332,6 +331,7 @@
|
||||
- (void)changeElementWithWarning:(NSString *)warning do:(void (^)(void))task; {
|
||||
|
||||
[PearlAlert showAlertWithTitle:@"Password Change" message:warning viewStyle:UIAlertViewStyleDefault
|
||||
initAlert:nil
|
||||
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||
if (buttonIndex == [alert cancelButtonIndex])
|
||||
return;
|
||||
@@ -361,7 +361,7 @@
|
||||
|
||||
- (IBAction)editPassword {
|
||||
|
||||
if (self.activeElement.type & MPElementTypeClassStored) {
|
||||
if ([self.activeElement.type unsignedIntegerValue] & MPElementTypeClassStored) {
|
||||
self.contentField.enabled = YES;
|
||||
[self.contentField becomeFirstResponder];
|
||||
}
|
||||
@@ -403,9 +403,7 @@
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
IASKAppSettingsViewController *settingsVC = [IASKAppSettingsViewController new];
|
||||
settingsVC.delegate = self;
|
||||
[self.navigationController pushViewController:settingsVC animated:YES];
|
||||
[self performSegueWithIdentifier:@"UserProfile" sender:self];
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
@@ -436,7 +434,7 @@
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointAction];
|
||||
}
|
||||
cancelTitle:[PearlStrings get].commonButtonCancel destructiveTitle:nil otherTitles:
|
||||
[self isHelpVisible]? @"Hide Help": @"Show Help", @"FAQ", @"Tutorial", @"Settings", @"Export", @"Feedback", @"Sign Out", nil];
|
||||
[self isHelpVisible]? @"Hide Help": @"Show Help", @"FAQ", @"Tutorial", @"Preferences", @"Feedback", @"Sign Out", nil];
|
||||
}
|
||||
|
||||
- (MPElementType)selectedType {
|
||||
@@ -458,7 +456,7 @@
|
||||
MPElementEntity *newElement = [NSEntityDescription insertNewObjectForEntityForName:ClassNameFromMPElementType(type)
|
||||
inManagedObjectContext:[MPAppDelegate managedObjectContext]];
|
||||
newElement.name = self.activeElement.name;
|
||||
newElement.keyID = self.activeElement.keyID;
|
||||
newElement.user = self.activeElement.user;
|
||||
newElement.uses = self.activeElement.uses;
|
||||
newElement.lastUsed = self.activeElement.lastUsed;
|
||||
|
||||
@@ -466,7 +464,7 @@
|
||||
self.activeElement = newElement;
|
||||
}];
|
||||
|
||||
self.activeElement.type = type;
|
||||
self.activeElement.type = PearlUnsignedInteger(type);
|
||||
|
||||
[TestFlight passCheckpoint:[NSString stringWithFormat:MPTestFlightCheckpointSelectType, NSStringFromMPElementType(type)]];
|
||||
|
||||
@@ -481,7 +479,7 @@
|
||||
|
||||
if (element) {
|
||||
self.activeElement = element;
|
||||
if ([self.activeElement use] == 1)
|
||||
if ([[self.activeElement use] unsignedIntegerValue] == 1)
|
||||
[self showAlertWithTitle:@"New Site" message:
|
||||
PearlLocalize(@"You've just created a password for %@.\n\n"
|
||||
@"IMPORTANT:\n"
|
||||
@@ -552,10 +550,4 @@
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)settingsViewControllerDidEnd:(IASKAppSettingsViewController *)sender {
|
||||
|
||||
while ([self.navigationController.viewControllers containsObject:sender])
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}
|
||||
|
||||
@end
|
||||
|
16
MasterPassword/iOS/MPPreferencesViewController.h
Normal file
16
MasterPassword/iOS/MPPreferencesViewController.h
Normal file
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// MPPreferencesViewController.h
|
||||
// MasterPassword-iOS
|
||||
//
|
||||
// Created by Maarten Billemont on 04/06/12.
|
||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "IASKAppSettingsViewController.h"
|
||||
|
||||
@interface MPPreferencesViewController : UITableViewController <IASKSettingsDelegate>
|
||||
|
||||
@property (weak, nonatomic) IBOutlet UIScrollView *avatarScrollView;
|
||||
|
||||
@end
|
63
MasterPassword/iOS/MPPreferencesViewController.m
Normal file
63
MasterPassword/iOS/MPPreferencesViewController.m
Normal file
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// MPPreferencesViewController.m
|
||||
// MasterPassword-iOS
|
||||
//
|
||||
// Created by Maarten Billemont on 04/06/12.
|
||||
// Copyright (c) 2012 Lyndir. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MPPreferencesViewController.h"
|
||||
#import "MPAppDelegate.h"
|
||||
|
||||
@interface MPPreferencesViewController ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation MPPreferencesViewController
|
||||
@synthesize avatarScrollView;
|
||||
|
||||
- (void)viewDidLoad {
|
||||
|
||||
__block NSInteger avatarIndex = 0;
|
||||
[self.avatarScrollView enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
|
||||
UIButton *avatar = (UIButton *)subview;
|
||||
avatar.toggleSelectionWhenTouchedInside = YES;
|
||||
avatar.tag = avatarIndex++;
|
||||
|
||||
[avatar onSelect:^(BOOL selected) {
|
||||
[MPAppDelegate get].activeUser.avatar = PearlInteger(avatar.tag);
|
||||
[self.avatarScrollView enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
|
||||
UIButton *avatar = (UIButton *)subview;
|
||||
avatar.selected = ([[MPAppDelegate get].activeUser.avatar integerValue] == avatar.tag);
|
||||
} recurse:NO];
|
||||
} options:0];
|
||||
} recurse:NO];
|
||||
|
||||
[super viewDidLoad];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
|
||||
[PearlUIUtils autoSizeContent:self.avatarScrollView];
|
||||
|
||||
[super viewWillAppear:animated];
|
||||
}
|
||||
|
||||
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
|
||||
|
||||
return (interfaceOrientation == UIInterfaceOrientationPortrait);
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)settingsViewControllerDidEnd:(IASKAppSettingsViewController *)sender {
|
||||
|
||||
while ([self.navigationController.viewControllers containsObject:sender])
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}
|
||||
|
||||
- (void)viewDidUnload {
|
||||
[self setAvatarScrollView:nil];
|
||||
[super viewDidUnload];
|
||||
}
|
||||
@end
|
@@ -131,8 +131,8 @@
|
||||
|
||||
assert(self.query);
|
||||
|
||||
self.fetchedResultsController.fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name BEGINSWITH[cd] %@) AND keyID == %@",
|
||||
self.query, self.query, NilToNull([MPAppDelegate get].keyID)];
|
||||
self.fetchedResultsController.fetchRequest.predicate = [NSPredicate predicateWithFormat:@"(%@ == '' OR name BEGINSWITH[cd] %@) AND user == %@",
|
||||
self.query, self.query, NilToNull([MPAppDelegate get].activeUser)];
|
||||
|
||||
NSError *error;
|
||||
if (![self.fetchedResultsController performFetch:&error])
|
||||
@@ -147,7 +147,7 @@
|
||||
[self.tipView removeFromSuperview];
|
||||
[overlay addSubview:self.tipView];
|
||||
}
|
||||
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
@@ -280,7 +280,7 @@
|
||||
|
||||
cell.textLabel.text = element.name;
|
||||
cell.detailTextLabel.text = [NSString stringWithFormat:@"Used %d times, last on %@",
|
||||
element.uses, [self.dateFormatter stringFromDate:[NSDate dateWithTimeIntervalSinceReferenceDate:element.lastUsed]]];
|
||||
element.uses, [self.dateFormatter stringFromDate:element.lastUsed]];
|
||||
} else {
|
||||
// "New" section
|
||||
cell.textLabel.text = self.query;
|
||||
@@ -299,6 +299,7 @@
|
||||
[PearlAlert showAlertWithTitle:@"New Site"
|
||||
message:PearlLocalize(@"Do you want to create a new site named:\n%@", siteName)
|
||||
viewStyle:UIAlertViewStyleDefault
|
||||
initAlert:nil
|
||||
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
|
||||
@@ -309,10 +310,10 @@
|
||||
MPElementGeneratedEntity *element = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPElementGeneratedEntity class])
|
||||
inManagedObjectContext:self.fetchedResultsController.managedObjectContext];
|
||||
assert([element isKindOfClass:ClassFromMPElementType((unsigned)element.type)]);
|
||||
assert([MPAppDelegate get].keyID);
|
||||
assert([MPAppDelegate get].activeUser.keyID);
|
||||
|
||||
element.name = siteName;
|
||||
element.keyID = [MPAppDelegate get].keyID;
|
||||
element.user = [MPAppDelegate get].activeUser;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.delegate didSelectElement:element];
|
||||
|
@@ -62,17 +62,15 @@
|
||||
|
||||
if ([delegate respondsToSelector:@selector(selectedType)])
|
||||
if ([delegate selectedType] == [self typeAtIndexPath:indexPath])
|
||||
[cell iterateSubviewsContinueAfter:^BOOL(UIView *subview) {
|
||||
[cell enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
|
||||
if ([subview isKindOfClass:[UIImageView class]]) {
|
||||
UIImageView *imageView = ((UIImageView *)subview);
|
||||
if (!imageView.highlightedImage)
|
||||
imageView.highlightedImage = [imageView.image highlightedImage];
|
||||
imageView.highlighted = YES;
|
||||
return NO;
|
||||
*stop = YES;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}];
|
||||
} recurse:NO];
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
@@ -8,14 +8,17 @@
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface MPUnlockViewController : UIViewController <UITextFieldDelegate>
|
||||
@interface MPUnlockViewController : UIViewController <UITextFieldDelegate, UIScrollViewDelegate>
|
||||
|
||||
@property (weak, nonatomic) IBOutlet UIImageView *lock;
|
||||
@property (weak, nonatomic) IBOutlet UIImageView *spinner;
|
||||
@property (weak, nonatomic) IBOutlet UITextField *field;
|
||||
@property (weak, nonatomic) IBOutlet UILabel *messageLabel;
|
||||
@property (weak, nonatomic) IBOutlet UIView *changeMPView;
|
||||
@property (weak, nonatomic) IBOutlet UITextField *passwordField;
|
||||
@property (weak, nonatomic) IBOutlet UIView *passwordView;
|
||||
@property (weak, nonatomic) IBOutlet UIScrollView *usersView;
|
||||
@property (weak, nonatomic) IBOutlet UILabel *usernameLabel;
|
||||
@property (weak, nonatomic) IBOutlet UILabel *oldUsernameLabel;
|
||||
@property (weak, nonatomic) IBOutlet UIButton *userButtonTemplate;
|
||||
@property (weak, nonatomic) IBOutlet UILabel *deleteTip;
|
||||
|
||||
- (IBAction)changeMP;
|
||||
- (IBAction)deleteTargetedUser:(UILongPressGestureRecognizer *)sender;
|
||||
|
||||
@end
|
||||
|
@@ -12,78 +12,29 @@
|
||||
#import "MPAppDelegate.h"
|
||||
#import "MPAppDelegate_Key.h"
|
||||
#import "MPAppDelegate_Store.h"
|
||||
#import "MPElementEntity.h"
|
||||
|
||||
typedef enum {
|
||||
MPLockscreenIdle,
|
||||
MPLockscreenError,
|
||||
MPLockscreenSuccess,
|
||||
MPLockscreenProgress,
|
||||
} MPLockscreen;
|
||||
#import "MPEntities.h"
|
||||
|
||||
@interface MPUnlockViewController ()
|
||||
|
||||
@property (strong, nonatomic) MPUserEntity *selectedUser;
|
||||
@property (strong, nonatomic) NSMutableDictionary *avatarToUser;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MPUnlockViewController
|
||||
@synthesize lock;
|
||||
@synthesize selectedUser;
|
||||
@synthesize avatarToUser;
|
||||
@synthesize spinner;
|
||||
@synthesize field;
|
||||
@synthesize messageLabel;
|
||||
@synthesize changeMPView;
|
||||
@synthesize passwordField;
|
||||
@synthesize passwordView;
|
||||
@synthesize usersView;
|
||||
@synthesize usernameLabel, oldUsernameLabel;
|
||||
@synthesize userButtonTemplate;
|
||||
@synthesize deleteTip;
|
||||
|
||||
- (void)showMessage:(NSString *)message state:(MPLockscreen)state {
|
||||
|
||||
__block void(^showMessageAnimation)(void) = ^{
|
||||
self.lock.alpha = 0.0f;
|
||||
switch (state) {
|
||||
case MPLockscreenIdle:
|
||||
[self.lock setImage:[UIImage imageNamed:@"lock_idle"]];
|
||||
break;
|
||||
case MPLockscreenError:
|
||||
[self.lock setImage:[UIImage imageNamed:@"lock_red"]];
|
||||
break;
|
||||
case MPLockscreenSuccess:
|
||||
[self.lock setImage:[UIImage imageNamed:@"lock_green"]];
|
||||
break;
|
||||
case MPLockscreenProgress:
|
||||
[self.lock setImage:[UIImage imageNamed:@"lock_blue"]];
|
||||
break;
|
||||
}
|
||||
|
||||
self.lock.alpha = 0.0f;
|
||||
[UIView animateWithDuration:1.0f animations:^{
|
||||
self.lock.alpha = 1.0f;
|
||||
} completion:^(BOOL finished) {
|
||||
if (finished)
|
||||
[UIView animateWithDuration:1.0f delay:0 options:UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse animations:^{
|
||||
self.lock.alpha = 0.5f;
|
||||
} completion:nil];
|
||||
}];
|
||||
|
||||
[UIView animateWithDuration:0.5f animations:^{
|
||||
self.messageLabel.alpha = 1.0f;
|
||||
self.messageLabel.text = message;
|
||||
}];
|
||||
};
|
||||
|
||||
if (self.messageLabel.alpha)
|
||||
[UIView animateWithDuration:0.3f animations:^{
|
||||
self.messageLabel.alpha = 0.0f;
|
||||
} completion:^(BOOL finished) {
|
||||
if (finished)
|
||||
showMessageAnimation();
|
||||
}];
|
||||
else
|
||||
showMessageAnimation();
|
||||
}
|
||||
|
||||
- (void)hideMessage {
|
||||
|
||||
[UIView animateWithDuration:0.5f animations:^{
|
||||
self.messageLabel.alpha = 0.0f;
|
||||
}];
|
||||
}
|
||||
// [UIView animateWithDuration:1.0f delay:0 options:UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse animations:^{
|
||||
// self.lock.alpha = 0.5f;
|
||||
// } completion:nil];
|
||||
|
||||
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
|
||||
|
||||
@@ -92,16 +43,16 @@ typedef enum {
|
||||
|
||||
- (void)viewDidLoad {
|
||||
|
||||
self.messageLabel.text = nil;
|
||||
self.messageLabel.alpha = 0;
|
||||
self.changeMPView.alpha = 0;
|
||||
self.spinner.alpha = 0;
|
||||
self.field.text = nil;
|
||||
self.avatarToUser = [NSMutableDictionary dictionaryWithCapacity:3];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserverForName:MPNotificationKeyForgotten
|
||||
object:nil queue:nil usingBlock:^(NSNotification *note) {
|
||||
[self.field becomeFirstResponder];
|
||||
}];
|
||||
self.spinner.alpha = 0;
|
||||
self.passwordField.text = nil;
|
||||
self.usersView.decelerationRate = UIScrollViewDecelerationRateFast;
|
||||
self.usersView.clipsToBounds = NO;
|
||||
self.usernameLabel.layer.cornerRadius = 5;
|
||||
self.userButtonTemplate.hidden = YES;
|
||||
|
||||
[self updateLayoutAnimated:NO allowScroll:YES completion:nil];
|
||||
|
||||
[super viewDidLoad];
|
||||
}
|
||||
@@ -109,20 +60,29 @@ typedef enum {
|
||||
- (void)viewDidUnload {
|
||||
|
||||
[self setSpinner:nil];
|
||||
[self setField:nil];
|
||||
|
||||
[self setMessageLabel:nil];
|
||||
[self setLock:nil];
|
||||
[self setChangeMPView:nil];
|
||||
[self setPasswordField:nil];
|
||||
[self setPasswordView:nil];
|
||||
[self setUsersView:nil];
|
||||
[self setUsernameLabel:nil];
|
||||
[self setUserButtonTemplate:nil];
|
||||
[self setDeleteTip:nil];
|
||||
[super viewDidUnload];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
|
||||
self.selectedUser = nil;
|
||||
[self updateUsers];
|
||||
|
||||
[super viewWillAppear:animated];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
|
||||
[[UIApplication sharedApplication] setStatusBarHidden:YES
|
||||
withAnimation:animated? UIStatusBarAnimationSlide: UIStatusBarAnimationNone];
|
||||
|
||||
[super viewWillAppear:animated];
|
||||
[super viewDidAppear:animated];
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
@@ -133,24 +93,247 @@ typedef enum {
|
||||
[super viewWillDisappear:animated];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
|
||||
[self.field becomeFirstResponder];
|
||||
|
||||
[super viewDidAppear:animated];
|
||||
- (void)updateUsers {
|
||||
|
||||
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPUserEntity class])];
|
||||
fetchRequest.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"lastUsed" ascending:NO]];
|
||||
NSArray *users = [[MPAppDelegate managedObjectContext] executeFetchRequest:fetchRequest error:nil];
|
||||
|
||||
// Clean up avatars.
|
||||
for (UIView *view in [self.usersView subviews])
|
||||
if (view != self.userButtonTemplate)
|
||||
[view removeFromSuperview];
|
||||
[self.avatarToUser removeAllObjects];
|
||||
|
||||
// Create avatars.
|
||||
for (MPUserEntity *user in users)
|
||||
[self setupAvatar:[PearlUIUtils copyOf:self.userButtonTemplate] forUser:user];
|
||||
[self setupAvatar:[PearlUIUtils copyOf:self.userButtonTemplate] forUser:nil];
|
||||
|
||||
// Scroll view's content changed, update its content size.
|
||||
[PearlUIUtils autoSizeContent:self.usersView ignoreHidden:YES ignoreInvisible:YES limitPadding:NO ignoreSubviews:nil];
|
||||
|
||||
[self updateLayoutAnimated:YES allowScroll:YES completion:nil];
|
||||
|
||||
self.deleteTip.alpha = 0;
|
||||
if ([users count] > 1)
|
||||
[UIView animateWithDuration:0.5f animations:^{
|
||||
self.deleteTip.alpha = 1;
|
||||
}];
|
||||
}
|
||||
|
||||
- (UIButton *)setupAvatar:(UIButton *)avatar forUser:(MPUserEntity *)user {
|
||||
|
||||
[avatar onHighlightOrSelect:^(BOOL highlighted, BOOL selected) {
|
||||
if (highlighted || selected)
|
||||
avatar.backgroundColor = self.userButtonTemplate.backgroundColor;
|
||||
else
|
||||
avatar.backgroundColor = [UIColor clearColor];
|
||||
} options:0];
|
||||
[avatar onSelect:^(BOOL selected) {
|
||||
self.selectedUser = selected? user: nil;
|
||||
if (user)
|
||||
[self didToggleUserSelection];
|
||||
else if (selected)
|
||||
[self didSelectNewUserAvatar:avatar];
|
||||
} options:0];
|
||||
avatar.toggleSelectionWhenTouchedInside = YES;
|
||||
avatar.center = CGPointMake(avatar.center.x + [self.avatarToUser count] * 160, avatar.center.y);
|
||||
avatar.hidden = NO;
|
||||
avatar.layer.cornerRadius = 5;
|
||||
avatar.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||
avatar.layer.shadowOpacity = 1;
|
||||
avatar.layer.shadowRadius = 20;
|
||||
avatar.layer.masksToBounds = NO;
|
||||
avatar.backgroundColor = [UIColor clearColor];
|
||||
|
||||
if (user)
|
||||
[self.avatarToUser setObject:user forKey:[NSValue valueWithNonretainedObject:avatar]];
|
||||
|
||||
if (self.selectedUser && user == self.selectedUser)
|
||||
avatar.selected = YES;
|
||||
|
||||
return avatar;
|
||||
}
|
||||
|
||||
- (void)didToggleUserSelection {
|
||||
|
||||
if (!self.selectedUser)
|
||||
[self.passwordField resignFirstResponder];
|
||||
|
||||
[self updateLayoutAnimated:YES allowScroll:YES completion:^(BOOL finished) {
|
||||
if (finished)
|
||||
if (self.selectedUser)
|
||||
[self.passwordField becomeFirstResponder];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)didSelectNewUserAvatar:(UIButton *)newUserAvatar {
|
||||
|
||||
[PearlAlert showAlertWithTitle:@"New User"
|
||||
message:@"Enter your name:" viewStyle:UIAlertViewStylePlainTextInput
|
||||
initAlert:^(UIAlertView *alert, UITextField *firstField) {
|
||||
firstField.autocapitalizationType = UITextAutocapitalizationTypeWords;
|
||||
firstField.autocorrectionType = UITextAutocorrectionTypeYes;
|
||||
firstField.spellCheckingType = UITextSpellCheckingTypeYes;
|
||||
firstField.keyboardType = UIKeyboardTypeAlphabet;
|
||||
}
|
||||
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||
newUserAvatar.selected = NO;
|
||||
|
||||
if (buttonIndex == [alert cancelButtonIndex])
|
||||
return;
|
||||
|
||||
MPUserEntity *newUser = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([MPUserEntity class])
|
||||
inManagedObjectContext:[MPAppDelegate managedObjectContext]];
|
||||
newUser.name = [alert textFieldAtIndex:0].text;
|
||||
self.selectedUser = newUser;
|
||||
|
||||
[self updateUsers];
|
||||
}
|
||||
cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:[PearlStrings get].commonButtonSave, nil];
|
||||
}
|
||||
|
||||
- (void)updateLayoutAnimated:(BOOL)animated allowScroll:(BOOL)allowScroll completion:(void (^)(BOOL finished))completion {
|
||||
|
||||
if (animated) {
|
||||
self.oldUsernameLabel.text = self.usernameLabel.text;
|
||||
self.oldUsernameLabel.alpha = 1;
|
||||
self.usernameLabel.alpha = 0;
|
||||
|
||||
[UIView animateWithDuration:0.5f animations:^{
|
||||
[self updateLayoutAnimated:NO allowScroll:allowScroll completion:nil];
|
||||
|
||||
self.oldUsernameLabel.alpha = 0;
|
||||
self.usernameLabel.alpha = 1;
|
||||
} completion:^(BOOL finished) {
|
||||
if (completion)
|
||||
completion(finished);
|
||||
}];
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.selectedUser && !self.passwordView.alpha) {
|
||||
self.passwordView.alpha = 1;
|
||||
self.usersView.center = CGPointMake(160, 100);
|
||||
self.usersView.scrollEnabled = NO;
|
||||
self.usernameLabel.center = CGPointMake(160, 84);
|
||||
self.usernameLabel.backgroundColor = [UIColor blackColor];
|
||||
self.oldUsernameLabel.center = self.usernameLabel.center;
|
||||
} else if (self.passwordView.alpha == 1) {
|
||||
self.passwordView.alpha = 0;
|
||||
self.usersView.center = CGPointMake(160, 240);
|
||||
self.usersView.scrollEnabled = YES;
|
||||
self.usernameLabel.center = CGPointMake(160, 296);
|
||||
self.usernameLabel.backgroundColor = [UIColor clearColor];
|
||||
self.oldUsernameLabel.center = self.usernameLabel.center;
|
||||
}
|
||||
|
||||
MPUserEntity *targetedUser = self.selectedUser;
|
||||
UIButton *selectedAvatar = [self avatarForUser:self.selectedUser];
|
||||
UIButton *targetedAvatar = selectedAvatar;
|
||||
if (!targetedAvatar) {
|
||||
targetedAvatar = [self findTargetedAvatar];
|
||||
targetedUser = [self userForAvatar:targetedAvatar];
|
||||
}
|
||||
|
||||
[self.usersView enumerateSubviews:^(UIView *subview, BOOL *stop, BOOL *recurse) {
|
||||
const BOOL isTargeted = subview == targetedAvatar;
|
||||
|
||||
subview.userInteractionEnabled = isTargeted;
|
||||
subview.alpha = isTargeted ? 1: self.selectedUser? 0.1: 0.4;
|
||||
if (!isTargeted && [subview.layer animationForKey:@"targetedShadow"]) {
|
||||
CABasicAnimation *toShadowColorAnimation = [CABasicAnimation animationWithKeyPath:@"shadowColor"];
|
||||
toShadowColorAnimation.toValue = (__bridge id)[UIColor blackColor].CGColor;
|
||||
toShadowColorAnimation.duration = 0.5f;
|
||||
|
||||
CABasicAnimation *toShadowOpacityAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"];
|
||||
toShadowOpacityAnimation.toValue = PearlFloat(1);
|
||||
toShadowOpacityAnimation.duration = 0.5f;
|
||||
|
||||
CAAnimationGroup *group = [[CAAnimationGroup alloc] init];
|
||||
group.animations = [NSArray arrayWithObjects:toShadowColorAnimation, toShadowOpacityAnimation, nil];
|
||||
group.duration = 0.5f;
|
||||
|
||||
[subview.layer removeAnimationForKey:@"targetedShadow"];
|
||||
[subview.layer addAnimation:group forKey:@"inactiveShadow"];
|
||||
}
|
||||
} recurse:NO];
|
||||
|
||||
if (![targetedAvatar.layer animationForKey:@"targetedShadow"]) {
|
||||
CABasicAnimation *toShadowColorAnimation = [CABasicAnimation animationWithKeyPath:@"shadowColor"];
|
||||
toShadowColorAnimation.toValue = (__bridge id)[UIColor whiteColor].CGColor;
|
||||
toShadowColorAnimation.beginTime = 0.0f;
|
||||
toShadowColorAnimation.duration = 0.5f;
|
||||
toShadowColorAnimation.fillMode = kCAFillModeForwards;
|
||||
|
||||
CABasicAnimation *toShadowOpacityAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"];
|
||||
toShadowOpacityAnimation.toValue = PearlFloat(0.2);
|
||||
toShadowOpacityAnimation.duration = 0.5f;
|
||||
|
||||
CABasicAnimation *pulseShadowOpacityAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"];
|
||||
pulseShadowOpacityAnimation.fromValue = PearlFloat(0.2);
|
||||
pulseShadowOpacityAnimation.toValue = PearlFloat(0.6);
|
||||
pulseShadowOpacityAnimation.beginTime = 0.5f;
|
||||
pulseShadowOpacityAnimation.duration = 2.0f;
|
||||
pulseShadowOpacityAnimation.autoreverses = YES;
|
||||
pulseShadowOpacityAnimation.repeatCount = NSIntegerMax;
|
||||
|
||||
CAAnimationGroup *group = [[CAAnimationGroup alloc] init];
|
||||
group.animations = [NSArray arrayWithObjects:toShadowColorAnimation, toShadowOpacityAnimation, pulseShadowOpacityAnimation, nil];
|
||||
group.duration = CGFLOAT_MAX;
|
||||
|
||||
[targetedAvatar.layer removeAnimationForKey:@"inactiveShadow"];
|
||||
[targetedAvatar.layer addAnimation:group forKey:@"targetedShadow"];
|
||||
}
|
||||
|
||||
if (allowScroll) {
|
||||
CGPoint targetContentOffset = CGPointMake(targetedAvatar.center.x - self.usersView.bounds.size.width / 2, self.usersView.contentOffset.y);
|
||||
if (!CGPointEqualToPoint(self.usersView.contentOffset, targetContentOffset))
|
||||
[self.usersView setContentOffset:targetContentOffset animated:animated];
|
||||
}
|
||||
|
||||
self.usernameLabel.text = targetedUser? targetedUser.name: @"New User";
|
||||
self.usernameLabel.bounds = CGRectSetHeight(self.usernameLabel.bounds,
|
||||
[self.usernameLabel.text sizeWithFont:self.usernameLabel.font
|
||||
constrainedToSize:CGSizeMake(self.usernameLabel.bounds.size.width - 10, 100)
|
||||
lineBreakMode:self.usernameLabel.lineBreakMode].height);
|
||||
self.oldUsernameLabel.bounds = self.usernameLabel.bounds;
|
||||
if (completion)
|
||||
completion(YES);
|
||||
}
|
||||
|
||||
- (UIButton *)findTargetedAvatar {
|
||||
|
||||
CGFloat xOfMiddle = self.usersView.contentOffset.x + self.usersView.bounds.size.width / 2;
|
||||
return (UIButton *)[PearlUIUtils viewClosestTo:CGPointMake(xOfMiddle, self.usersView.contentOffset.y) ofArray:self.usersView.subviews];
|
||||
}
|
||||
|
||||
- (UIButton *)avatarForUser:(MPUserEntity *)user {
|
||||
|
||||
__block UIButton *avatar = nil;
|
||||
if (user)
|
||||
[self.avatarToUser enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
|
||||
if (obj == user)
|
||||
avatar = [key nonretainedObjectValue];
|
||||
}];
|
||||
|
||||
return avatar;
|
||||
}
|
||||
|
||||
- (MPUserEntity *)userForAvatar:(UIButton *)avatar {
|
||||
|
||||
return NullToNil([self.avatarToUser objectForKey:[NSValue valueWithNonretainedObject:avatar]]);
|
||||
}
|
||||
|
||||
#pragma mark - UITextFieldDelegate
|
||||
|
||||
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
|
||||
|
||||
if ([textField.text length]) {
|
||||
[textField resignFirstResponder];
|
||||
return YES;
|
||||
}
|
||||
if (![textField.text length])
|
||||
return NO;
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)textFieldDidEndEditing:(UITextField *)textField {
|
||||
[textField resignFirstResponder];
|
||||
|
||||
CABasicAnimation *rotate = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
|
||||
rotate.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
|
||||
@@ -166,52 +349,53 @@ typedef enum {
|
||||
self.spinner.alpha = 1.0f;
|
||||
}];
|
||||
|
||||
[self showMessage:@"Checking password..." state:MPLockscreenProgress];
|
||||
// [self showMessage:@"Checking password..." state:MPLockscreenProgress];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
BOOL unlocked = [[MPAppDelegate get] tryMasterPassword:textField.text];
|
||||
BOOL unlocked = [[MPAppDelegate get] tryMasterPassword:textField.text forUser:self.selectedUser];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (unlocked) {
|
||||
[self showMessage:@"Success!" state:MPLockscreenSuccess];
|
||||
|
||||
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:NSStringFromClass([MPElementEntity class])];
|
||||
fetchRequest.predicate = [NSPredicate predicateWithFormat:@"keyID == %@", [MPAppDelegate get].keyID];
|
||||
fetchRequest.fetchLimit = 1;
|
||||
BOOL keyIDHasElements = [[[MPAppDelegate managedObjectContext] executeFetchRequest:fetchRequest error:nil] count] > 0;
|
||||
if (keyIDHasElements)
|
||||
// [self showMessage:@"Success!" state:MPLockscreenSuccess];
|
||||
if ([selectedUser.keyID length])
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (long)(NSEC_PER_SEC * 1.5f)), dispatch_get_main_queue(), ^{
|
||||
[self dismissModalViewControllerAnimated:YES];
|
||||
});
|
||||
else {
|
||||
[PearlAlert showAlertWithTitle:@"New Master Password"
|
||||
message:
|
||||
@"Please confirm the spelling of this new master password."
|
||||
message:@"Please confirm the spelling of this new master password."
|
||||
viewStyle:UIAlertViewStyleSecureTextInput
|
||||
initAlert:nil
|
||||
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||
if (buttonIndex == [alert cancelButtonIndex]) {
|
||||
[[MPAppDelegate get] updateKey:nil];
|
||||
[[MPAppDelegate get] unsetKey];
|
||||
return;
|
||||
}
|
||||
|
||||
if (![[alert textFieldAtIndex:0].text isEqualToString:textField.text]) {
|
||||
[PearlAlert showAlertWithTitle:@"Incorrect Master Password"
|
||||
message:
|
||||
@"The password you entered doesn't match with the master password you tried to use. "
|
||||
@"You've probably mistyped one of them.\n\n"
|
||||
@"Give it another try."
|
||||
viewStyle:UIAlertViewStyleDefault tappedButtonBlock:nil
|
||||
viewStyle:UIAlertViewStyleDefault initAlert:nil tappedButtonBlock:nil
|
||||
cancelTitle:[PearlStrings get].commonButtonOkay otherTitles:nil];
|
||||
return;
|
||||
}
|
||||
|
||||
self.selectedUser.keyID = [MPAppDelegate get].activeUser.keyID;
|
||||
[[MPAppDelegate get] saveContext];
|
||||
|
||||
[self dismissModalViewControllerAnimated:YES];
|
||||
}
|
||||
cancelTitle:[PearlStrings get].commonButtonCancel
|
||||
otherTitles:[PearlStrings get].commonButtonContinue, nil];
|
||||
}
|
||||
} else {
|
||||
[self showMessage:@"Not valid." state:MPLockscreenError];
|
||||
// [self showMessage:@"Not valid." state:MPLockscreenError];
|
||||
|
||||
[UIView animateWithDuration:0.5f animations:^{
|
||||
self.changeMPView.alpha = 1.0f;
|
||||
// self.changeMPView.alpha = 1.0f;
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -224,8 +408,39 @@ typedef enum {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - UIScrollViewDelegate
|
||||
|
||||
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
|
||||
|
||||
CGFloat xOfMiddle = targetContentOffset->x + scrollView.bounds.size.width / 2;
|
||||
UIButton *middleAvatar = (UIButton *)[PearlUIUtils viewClosestTo:CGPointMake(xOfMiddle, targetContentOffset->y) ofArray:scrollView.subviews];
|
||||
*targetContentOffset = CGPointMake(middleAvatar.center.x - scrollView.bounds.size.width / 2, targetContentOffset->y);
|
||||
|
||||
[self updateLayoutAnimated:NO allowScroll:NO completion:nil];
|
||||
// [self scrollToAvatar:middleAvatar animated:YES];
|
||||
}
|
||||
|
||||
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
|
||||
|
||||
[self updateLayoutAnimated:YES allowScroll:YES completion:nil];
|
||||
// [self scrollToAvatar:middleAvatar animated:YES];
|
||||
}
|
||||
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
||||
|
||||
// CGFloat xOfMiddle = scrollView.contentOffset.x + scrollView.bounds.size.width / 2;
|
||||
// UIButton *middleAvatar = (UIButton *)[PearlUIUtils viewClosestTo:CGPointMake(xOfMiddle, scrollView.contentOffset.y) ofArray:scrollView.subviews];
|
||||
//
|
||||
[self updateLayoutAnimated:NO allowScroll:NO completion:nil];
|
||||
// [self scrollToAvatar:middleAvatar animated:NO];
|
||||
}
|
||||
|
||||
#pragma mark - IBActions
|
||||
|
||||
- (IBAction)changeMP {
|
||||
|
||||
[PearlAlert showAlertWithTitle:@"Changing Master Password"
|
||||
@@ -236,11 +451,12 @@ typedef enum {
|
||||
@"You can always change back to your current master password later.\n"
|
||||
@"Your current sites and passwords will then become available again."
|
||||
viewStyle:UIAlertViewStyleDefault
|
||||
initAlert:nil
|
||||
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||
if (buttonIndex == [alert cancelButtonIndex])
|
||||
return;
|
||||
|
||||
[[MPAppDelegate get] forgetKey];
|
||||
[[MPAppDelegate get] forgetSavedKey];
|
||||
[[MPAppDelegate get] loadKey:YES];
|
||||
|
||||
[TestFlight passCheckpoint:MPTestFlightCheckpointMPChanged];
|
||||
@@ -249,4 +465,30 @@ typedef enum {
|
||||
otherTitles:[PearlStrings get].commonButtonContinue, nil];
|
||||
}
|
||||
|
||||
- (IBAction)deleteTargetedUser:(UILongPressGestureRecognizer *)sender {
|
||||
|
||||
if (sender.state != UIGestureRecognizerStateBegan)
|
||||
return;
|
||||
|
||||
if (self.selectedUser)
|
||||
return;
|
||||
|
||||
MPUserEntity *targetedUser = [self userForAvatar:[self findTargetedAvatar]];
|
||||
if (!targetedUser)
|
||||
return;
|
||||
|
||||
[PearlAlert showAlertWithTitle:@"Delete User" message:
|
||||
PearlString(@"Do you want to delete all record of the following user?\n\n%@", targetedUser.name)
|
||||
viewStyle:UIAlertViewStyleDefault
|
||||
initAlert:nil
|
||||
tappedButtonBlock:^(UIAlertView *alert, NSInteger buttonIndex) {
|
||||
if (buttonIndex == [alert cancelButtonIndex])
|
||||
return;
|
||||
|
||||
[[MPAppDelegate get].managedObjectContext deleteObject:targetedUser];
|
||||
[[MPAppDelegate get] saveContext];
|
||||
|
||||
[self updateUsers];
|
||||
} cancelTitle:[PearlStrings get].commonButtonCancel otherTitles:@"Delete", nil];
|
||||
}
|
||||
@end
|
||||
|
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="1.1" toolsVersion="2182" systemVersion="11E53" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" initialViewController="KZF-fe-y9n">
|
||||
<dependencies>
|
||||
<deployment defaultVersion="1296" identifier="iOS"/>
|
||||
<development defaultVersion="4200" identifier="xcode"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="1181"/>
|
||||
</dependencies>
|
||||
@@ -659,6 +658,7 @@ L4m3P4sSw0rD</string>
|
||||
<outlet property="siteName" destination="gSK-aB-wNI" id="IIe-z8-zy8"/>
|
||||
<outlet property="typeButton" destination="Cei-5z-uWE" id="4M1-d7-5Bh"/>
|
||||
<outlet property="typeTipContainer" destination="g55-0m-WjS" id="KZ9-KV-NMh"/>
|
||||
<segue destination="oLN-6u-GLb" kind="push" identifier="UserProfile" id="tKN-Sw-S5J"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<pongPressGestureRecognizer allowableMovement="10" minimumPressDuration="0.5" id="cZr-Fj-eBw">
|
||||
@@ -735,7 +735,7 @@ L4m3P4sSw0rD</string>
|
||||
<scene sceneID="HkH-JR-Fhy">
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="OGA-5j-IcQ" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<viewController storyboardIdentifier="MPUnlockViewController" modalTransitionStyle="flipHorizontal" id="Nbn-Rv-sP1" customClass="MPUnlockViewController" sceneMemberID="viewController">
|
||||
<viewController storyboardIdentifier="MPUnlockViewController" wantsFullScreenLayout="YES" modalTransitionStyle="flipHorizontal" id="Nbn-Rv-sP1" customClass="MPUnlockViewController" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="PHH-XC-9QQ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="480"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
@@ -744,96 +744,111 @@ L4m3P4sSw0rD</string>
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="480"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
</imageView>
|
||||
<view contentMode="scaleToFill" id="OP6-72-eij">
|
||||
<rect key="frame" x="0.0" y="157" width="320" height="130"/>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" alpha="0.5" contentMode="left" text="Tap and hold to delete." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="10" id="DBJ-Qi-ZcF">
|
||||
<rect key="frame" x="32" y="460" width="256" height="20"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" name="Copperplate" family="Copperplate" pointSize="12"/>
|
||||
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<color key="shadowColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" id="7cc-yu-i0m">
|
||||
<rect key="frame" x="20" y="168" width="280" height="88"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="center" image="lock_idle.png" id="tyv-qJ-bKR">
|
||||
<rect key="frame" x="110" y="15" width="100" height="100"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
|
||||
</imageView>
|
||||
<imageView userInteractionEnabled="NO" alpha="0.0" contentMode="center" image="lock_idle.png" id="Lpf-KA-3CV">
|
||||
<rect key="frame" x="110" y="15" width="100" height="100"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
|
||||
</imageView>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" image="ui_spinner.png" id="27q-lX-0vy">
|
||||
<rect key="frame" x="122" y="28" width="75" height="75"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
|
||||
</imageView>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Checking password..." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="10" id="oU9-lf-nnJ">
|
||||
<rect key="frame" x="20" y="115" width="280" height="15"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
<fontDescription key="fontDescription" name="Copperplate-Bold" family="Copperplate" pointSize="13"/>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Enter your master password:" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="10" id="RhX-bA-EhC">
|
||||
<rect key="frame" x="12" y="0.0" width="256" height="20"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" name="Copperplate" family="Copperplate" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" image="ui_textfield.png" id="ivR-Xl-NrT">
|
||||
<rect key="frame" x="0.0" y="28" width="280" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<rect key="contentStretch" x="0.25" y="0.25" width="0.49999999999999961" height="0.49999999999999961"/>
|
||||
</imageView>
|
||||
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" textAlignment="center" clearsOnBeginEditing="YES" minimumFontSize="17" id="rTR-7Q-X8o">
|
||||
<rect key="frame" x="0.0" y="28" width="280" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<fontDescription key="fontDescription" name="Copperplate" family="Copperplate" pointSize="36"/>
|
||||
<textInputTraits key="textInputTraits" enablesReturnKeyAutomatically="YES" secureTextEntry="YES"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Nbn-Rv-sP1" id="Y0T-cI-gF1"/>
|
||||
</connections>
|
||||
</textField>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" image="ui_textfield.png" id="ivR-Xl-NrT">
|
||||
<rect key="frame" x="20" y="89" width="280" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<rect key="contentStretch" x="0.25" y="0.25" width="0.49999999999999961" height="0.49999999999999961"/>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" image="ui_spinner.png" id="27q-lX-0vy">
|
||||
<rect key="frame" x="105" y="29" width="110" height="110"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
</imageView>
|
||||
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" textAlignment="center" clearsOnBeginEditing="YES" minimumFontSize="17" id="rTR-7Q-X8o">
|
||||
<rect key="frame" x="20" y="89" width="280" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<fontDescription key="fontDescription" name="Copperplate" family="Copperplate" pointSize="36"/>
|
||||
<textInputTraits key="textInputTraits" secureTextEntry="YES"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Nbn-Rv-sP1" id="Y0T-cI-gF1"/>
|
||||
</connections>
|
||||
</textField>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Enter your master password:" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="10" id="RhX-bA-EhC">
|
||||
<rect key="frame" x="32" y="61" width="256" height="20"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" name="Copperplate" family="Copperplate" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" id="Wu7-Mg-9SD">
|
||||
<rect key="frame" x="0.0" y="391" width="320" height="89"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" delaysContentTouches="NO" id="Blg-F1-9NA">
|
||||
<rect key="frame" x="0.0" y="20" width="320" height="160"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Trying to log in with another master password?" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumFontSize="10" id="vnS-n6-NZI">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="15"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
<fontDescription key="fontDescription" name="Copperplate-Bold" family="Copperplate" pointSize="11"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" reversesTitleShadowWhenHighlighted="YES" lineBreakMode="middleTruncation" id="wad-V1-K3f">
|
||||
<rect key="frame" x="10" y="23" width="300" height="46"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="top" showsTouchWhenHighlighted="YES" lineBreakMode="middleTruncation" id="Ten-ig-gog">
|
||||
<rect key="frame" x="105" y="10" width="110" height="110"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<color key="backgroundColor" red="0.40000000596046448" green="0.80000001192092896" blue="1" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
|
||||
<size key="titleShadowOffset" width="0.0" height="1"/>
|
||||
<state key="normal" title="Change master password" backgroundImage="ui_button_standard_large.png">
|
||||
<color key="titleColor" cocoaTouchSystemColor="lightTextColor"/>
|
||||
<color key="titleShadowColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<state key="normal" backgroundImage="avatar-male-1.png">
|
||||
<color key="titleColor" red="0.19607843459999999" green="0.30980393290000002" blue="0.52156865600000002" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</state>
|
||||
<state key="highlighted">
|
||||
<color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="changeMP" destination="Nbn-Rv-sP1" eventType="touchUpInside" id="7RI-hu-SiS"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<gestureRecognizers/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Nbn-Rv-sP1" id="E1h-WA-PYV"/>
|
||||
<outletCollection property="gestureRecognizers" destination="9WS-yS-aqQ" appends="YES" id="B9k-bg-gqA"/>
|
||||
</connections>
|
||||
</scrollView>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" alpha="0.0" contentMode="left" text="Maarten Billemont" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumFontSize="10" id="8s0-nT-Aoq">
|
||||
<rect key="frame" x="90" y="289" width="140" height="15"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
<fontDescription key="fontDescription" name="Copperplate-Bold" family="Copperplate" pointSize="13"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Maarten Billemont" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumFontSize="10" id="0NM-NI-7UR">
|
||||
<rect key="frame" x="90" y="76.5" width="140" height="15"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<fontDescription key="fontDescription" name="Copperplate-Bold" family="Copperplate" pointSize="13"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView hidden="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="keypad.png" id="4tz-l4-4Kj">
|
||||
<rect key="frame" x="0.0" y="264" width="320" height="216"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
<nil key="simulatedStatusBarMetrics"/>
|
||||
<connections>
|
||||
<outlet property="changeMPView" destination="Wu7-Mg-9SD" id="84H-HT-5ml"/>
|
||||
<outlet property="field" destination="rTR-7Q-X8o" id="DHg-gg-MVD"/>
|
||||
<outlet property="lock" destination="Lpf-KA-3CV" id="6cE-2g-4XQ"/>
|
||||
<outlet property="messageLabel" destination="oU9-lf-nnJ" id="Ahc-hl-KnJ"/>
|
||||
<outlet property="deleteTip" destination="DBJ-Qi-ZcF" id="VXD-Zc-UYi"/>
|
||||
<outlet property="oldUsernameLabel" destination="8s0-nT-Aoq" id="mXR-VG-2js"/>
|
||||
<outlet property="passwordField" destination="rTR-7Q-X8o" id="CDA-iP-kCm"/>
|
||||
<outlet property="passwordView" destination="7cc-yu-i0m" id="WoF-Ab-PPC"/>
|
||||
<outlet property="spinner" destination="27q-lX-0vy" id="jUx-GK-Lgf"/>
|
||||
<outlet property="userButtonTemplate" destination="Ten-ig-gog" id="hSM-hh-ofy"/>
|
||||
<outlet property="usernameLabel" destination="0NM-NI-7UR" id="Sa4-pY-87O"/>
|
||||
<outlet property="usersView" destination="Blg-F1-9NA" id="SsC-1H-fIB"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<pongPressGestureRecognizer allowableMovement="10" minimumPressDuration="0.5" id="9WS-yS-aqQ">
|
||||
<connections>
|
||||
<action selector="deleteTargetedUser:" destination="Nbn-Rv-sP1" id="cBQ-oQ-c7g"/>
|
||||
</connections>
|
||||
</pongPressGestureRecognizer>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="455" y="1425"/>
|
||||
</scene>
|
||||
@@ -857,9 +872,319 @@ L4m3P4sSw0rD</string>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-85" y="182"/>
|
||||
</scene>
|
||||
<!--App Settings View Controller-->
|
||||
<scene sceneID="4E1-FA-zP1">
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Xkt-gh-nAq" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<viewController id="Ypi-Ct-X6S" customClass="IASKAppSettingsViewController" sceneMemberID="viewController">
|
||||
<navigationItem key="navigationItem" id="u18-ao-KIy"/>
|
||||
</viewController>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1537" y="785"/>
|
||||
</scene>
|
||||
<!--Preferences View Controller - User Profile-->
|
||||
<scene sceneID="cMu-Lq-D10">
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="CEl-jQ-l9k" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<tableViewController id="oLN-6u-GLb" customClass="MPPreferencesViewController" sceneMemberID="viewController">
|
||||
<tableView key="view" opaque="NO" clipsSubviews="YES" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="plain" separatorStyle="none" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22" id="oSh-Ap-kLt">
|
||||
<rect key="frame" x="0.0" y="64" width="320" height="416"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="0.12549020350000001" green="0.1411764771" blue="0.14901961389999999" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<view key="tableFooterView" contentMode="scaleToFill" id="63M-7L-M7o">
|
||||
<rect key="frame" x="0.0" y="508" width="320" height="20"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
<sections>
|
||||
<tableViewSection id="OJv-8V-W2l">
|
||||
<cells>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" rowHeight="226" id="9gC-Vq-Ta8">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="226"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="225"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Avatar" lineBreakMode="tailTruncation" minimumFontSize="10" id="0Wd-wn-t6z">
|
||||
<rect key="frame" x="20" y="20" width="280" height="21"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" name="GillSans-Bold" family="Gill Sans" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<color key="shadowColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<size key="shadowOffset" width="0.0" height="1"/>
|
||||
</label>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="This is the icon that represents you on the Master Password unlock screen." lineBreakMode="tailTruncation" numberOfLines="0" minimumFontSize="10" id="DgX-yc-sKT">
|
||||
<rect key="frame" x="20" y="49" width="280" height="38"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" name="GillSans-LightItalic" family="Gill Sans" pointSize="14"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<color key="shadowColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<size key="shadowOffset" width="0.0" height="1"/>
|
||||
</label>
|
||||
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" id="TMH-Xt-EnD">
|
||||
<rect key="frame" x="0.0" y="95" width="320" height="130"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" id="jEL-UA-da6">
|
||||
<rect key="frame" x="0.0" y="0.0" width="614" height="130"/>
|
||||
<autoresizingMask key="autoresizingMask" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="top" lineBreakMode="middleTruncation" id="6ap-Xw-Ubd">
|
||||
<rect key="frame" x="20" y="10" width="110" height="110"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
|
||||
<state key="normal" image="avatar-male-1.png">
|
||||
<color key="titleColor" red="0.19607843459999999" green="0.30980393290000002" blue="0.52156865600000002" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</state>
|
||||
<state key="highlighted">
|
||||
<color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</state>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="top" lineBreakMode="middleTruncation" id="DwC-Fa-yaI">
|
||||
<rect key="frame" x="138" y="10" width="110" height="110"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
|
||||
<state key="normal" image="avatar-female-1.png">
|
||||
<color key="titleColor" red="0.19607843459999999" green="0.30980393290000002" blue="0.52156865600000002" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</state>
|
||||
<state key="highlighted">
|
||||
<color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</state>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="top" lineBreakMode="middleTruncation" id="TF1-Zi-jZK">
|
||||
<rect key="frame" x="256" y="10" width="110" height="110"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
|
||||
<state key="normal" image="avatar-male-2.png">
|
||||
<color key="titleColor" red="0.19607843459999999" green="0.30980393290000002" blue="0.52156865600000002" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</state>
|
||||
<state key="highlighted">
|
||||
<color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</state>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="top" lineBreakMode="middleTruncation" id="32d-Yo-SGt">
|
||||
<rect key="frame" x="366" y="10" width="110" height="110"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
|
||||
<state key="normal" image="avatar-female-2.png">
|
||||
<color key="titleColor" red="0.19607843459999999" green="0.30980393290000002" blue="0.52156865600000002" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</state>
|
||||
<state key="highlighted">
|
||||
<color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</state>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="top" lineBreakMode="middleTruncation" id="LTD-gI-8bH">
|
||||
<rect key="frame" x="484" y="10" width="110" height="110"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="boldSystem" pointSize="15"/>
|
||||
<state key="normal" image="avatar-male-3.png">
|
||||
<color key="titleColor" red="0.19607843459999999" green="0.30980393290000002" blue="0.52156865600000002" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</state>
|
||||
<state key="highlighted">
|
||||
<color key="titleColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</state>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
</subviews>
|
||||
</scrollView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection id="TRa-Gy-tG5">
|
||||
<cells>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" rowHeight="119" id="n1H-sG-HDc">
|
||||
<rect key="frame" x="0.0" y="226" width="320" height="119"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="118"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Preferences" lineBreakMode="tailTruncation" minimumFontSize="10" id="ylz-i5-tem">
|
||||
<rect key="frame" x="20" y="20" width="280" height="21"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" name="GillSans-Bold" family="Gill Sans" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<color key="shadowColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<size key="shadowOffset" width="0.0" height="1"/>
|
||||
</label>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="These are user-specific configuration settings." lineBreakMode="tailTruncation" numberOfLines="0" minimumFontSize="10" id="WCm-A3-a1a">
|
||||
<rect key="frame" x="20" y="49" width="280" height="16"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" name="GillSans-LightItalic" family="Gill Sans" pointSize="14"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<color key="shadowColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<size key="shadowOffset" width="0.0" height="1"/>
|
||||
</label>
|
||||
<imageView userInteractionEnabled="NO" contentMode="top" image="ui_list_first.png" id="tK4-bL-QK1">
|
||||
<rect key="frame" x="10" y="75" width="300" height="50"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
<rect key="contentStretch" x="0.10000000000000001" y="0.20000000000000001" width="0.79999999999999982" height="0.59999999999999964"/>
|
||||
</imageView>
|
||||
<imageView userInteractionEnabled="NO" contentMode="bottom" image="ui_list_last.png" id="fAY-fK-Uzn">
|
||||
<rect key="frame" x="10" y="109" width="300" height="10"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
<rect key="contentStretch" x="0.10000000000000001" y="0.20000000000000001" width="0.79999999999999982" height="0.59999999999999964"/>
|
||||
</imageView>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Save Password" lineBreakMode="tailTruncation" minimumFontSize="10" id="mmP-r2-iNF">
|
||||
<rect key="frame" x="20" y="75" width="280" height="23"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
<fontDescription key="fontDescription" name="GillSans" family="Gill Sans" pointSize="18"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<color key="shadowColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<size key="shadowOffset" width="0.0" height="1"/>
|
||||
</label>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Stores your password on the device." lineBreakMode="tailTruncation" minimumFontSize="10" id="LRv-ac-sdH">
|
||||
<rect key="frame" x="20" y="98" width="280" height="20"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
<fontDescription key="fontDescription" name="GillSans-LightItalic" family="Gill Sans" pointSize="14"/>
|
||||
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<switch opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" id="seh-qE-k6P">
|
||||
<rect key="frame" x="221" y="83" width="79" height="27"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<color key="onTintColor" red="0.12549020350000001" green="0.1411764771" blue="0.14901961389999999" alpha="1" colorSpace="calibratedRGB"/>
|
||||
</switch>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection id="dN3-cJ-9WA">
|
||||
<cells>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" rowHeight="119" id="xBt-OT-BYA">
|
||||
<rect key="frame" x="0.0" y="345" width="320" height="119"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="118"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Operations" lineBreakMode="tailTruncation" minimumFontSize="10" id="VI9-uT-nrJ">
|
||||
<rect key="frame" x="20" y="20" width="280" height="21"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" name="GillSans-Bold" family="Gill Sans" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<color key="shadowColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<size key="shadowOffset" width="0.0" height="1"/>
|
||||
</label>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Advanced operations for managing your passwords." lineBreakMode="tailTruncation" numberOfLines="0" minimumFontSize="10" id="blC-Mc-59X">
|
||||
<rect key="frame" x="20" y="49" width="280" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" name="GillSans-LightItalic" family="Gill Sans" pointSize="14"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<color key="shadowColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<size key="shadowOffset" width="0.0" height="1"/>
|
||||
</label>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" image="ui_list_first.png" id="zeK-qR-5wK">
|
||||
<rect key="frame" x="10" y="75" width="300" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
<rect key="contentStretch" x="0.10000000000000001" y="0.20000000000000001" width="0.79999999999999982" height="0.59999999999999964"/>
|
||||
</imageView>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Export" lineBreakMode="tailTruncation" minimumFontSize="10" id="DTP-qu-VqT">
|
||||
<rect key="frame" x="20" y="75" width="280" height="23"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
<fontDescription key="fontDescription" name="GillSans" family="Gill Sans" pointSize="18"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<color key="shadowColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<size key="shadowOffset" width="0.0" height="1"/>
|
||||
</label>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Save all your sites to a file." lineBreakMode="tailTruncation" minimumFontSize="10" id="oi9-2Y-2KM">
|
||||
<rect key="frame" x="20" y="98" width="280" height="20"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
<fontDescription key="fontDescription" name="GillSans-LightItalic" family="Gill Sans" pointSize="14"/>
|
||||
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" id="glr-eJ-qKq">
|
||||
<rect key="frame" x="0.0" y="464" width="320" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="43"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" image="ui_list_last.png" id="rbd-L8-fSm">
|
||||
<rect key="frame" x="10" y="0.0" width="300" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<rect key="contentStretch" x="0.10000000000000001" y="0.20000000000000001" width="0.79999999999999982" height="0.59999999999999964"/>
|
||||
</imageView>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Change Master Password" lineBreakMode="tailTruncation" minimumFontSize="10" id="M38-26-Zzv">
|
||||
<rect key="frame" x="20" y="0.0" width="280" height="24"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
<fontDescription key="fontDescription" name="GillSans" family="Gill Sans" pointSize="18"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
<color key="shadowColor" cocoaTouchSystemColor="darkTextColor"/>
|
||||
<size key="shadowOffset" width="0.0" height="1"/>
|
||||
</label>
|
||||
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" text="Change your user's login master password." lineBreakMode="tailTruncation" minimumFontSize="10" id="tUk-yd-cyq">
|
||||
<rect key="frame" x="20" y="23" width="280" height="20"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||
<fontDescription key="fontDescription" name="GillSans-LightItalic" family="Gill Sans" pointSize="14"/>
|
||||
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="oLN-6u-GLb" id="a6n-fI-h3l"/>
|
||||
<outlet property="delegate" destination="oLN-6u-GLb" id="MM9-S2-8rf"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="User Profile" id="reg-hh-9Ra">
|
||||
<barButtonItem key="rightBarButtonItem" title="Settings" id="Bac-IA-e0Z">
|
||||
<connections>
|
||||
<segue destination="Ypi-Ct-X6S" kind="push" id="vLK-dS-gos"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="avatarScrollView" destination="TMH-Xt-EnD" id="zdf-xt-piZ"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="996" y="785"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="Square-bottom.png" width="551" height="58"/>
|
||||
<image name="avatar-female-1.png" width="110" height="110"/>
|
||||
<image name="avatar-female-2.png" width="110" height="110"/>
|
||||
<image name="avatar-male-1.png" width="110" height="110"/>
|
||||
<image name="avatar-male-2.png" width="110" height="110"/>
|
||||
<image name="avatar-male-3.png" width="110" height="110"/>
|
||||
<image name="background.png" width="480" height="480"/>
|
||||
<image name="guide_page_0.png" width="320" height="480"/>
|
||||
<image name="guide_page_1.png" width="320" height="480"/>
|
||||
@@ -872,7 +1197,7 @@ L4m3P4sSw0rD</string>
|
||||
<image name="icon_cancel.png" width="32" height="32"/>
|
||||
<image name="icon_edit.png" width="32" height="32"/>
|
||||
<image name="icon_plus.png" width="32" height="32"/>
|
||||
<image name="lock_idle.png" width="100" height="100"/>
|
||||
<image name="keypad.png" width="320" height="216"/>
|
||||
<image name="tip_alert_black.png" width="235" height="81"/>
|
||||
<image name="tip_basic_black.png" width="210" height="60"/>
|
||||
<image name="tip_basic_black_top.png" width="210" height="60"/>
|
||||
@@ -887,6 +1212,86 @@ L4m3P4sSw0rD</string>
|
||||
<image name="ui_spinner.png" width="75" height="75"/>
|
||||
<image name="ui_textfield.png" width="158" height="34"/>
|
||||
</resources>
|
||||
<classes>
|
||||
<class className="IASKAppSettingsViewController" superclassName="UITableViewController">
|
||||
<source key="sourceIdentifier" type="project" relativePath="./Classes/IASKAppSettingsViewController.h"/>
|
||||
<relationships>
|
||||
<relationship kind="action" name="dismiss:"/>
|
||||
<relationship kind="outlet" name="delegate"/>
|
||||
</relationships>
|
||||
</class>
|
||||
<class className="MPGuideViewController" superclassName="UIViewController">
|
||||
<source key="sourceIdentifier" type="project" relativePath="./Classes/MPGuideViewController.h"/>
|
||||
<relationships>
|
||||
<relationship kind="action" name="close"/>
|
||||
<relationship kind="outlet" name="scrollView" candidateClass="UIScrollView"/>
|
||||
</relationships>
|
||||
</class>
|
||||
<class className="MPMainViewController" superclassName="UIViewController">
|
||||
<source key="sourceIdentifier" type="project" relativePath="./Classes/MPMainViewController.h"/>
|
||||
<relationships>
|
||||
<relationship kind="action" name="action:" candidateClass="UIBarButtonItem"/>
|
||||
<relationship kind="action" name="closeAlert"/>
|
||||
<relationship kind="action" name="copyContent"/>
|
||||
<relationship kind="action" name="editPassword"/>
|
||||
<relationship kind="action" name="incrementPasswordCounter"/>
|
||||
<relationship kind="action" name="resetPasswordCounter:" candidateClass="UILongPressGestureRecognizer"/>
|
||||
<relationship kind="outlet" name="actionsTipContainer" candidateClass="UIView"/>
|
||||
<relationship kind="outlet" name="alertBody" candidateClass="UITextView"/>
|
||||
<relationship kind="outlet" name="alertContainer" candidateClass="UIView"/>
|
||||
<relationship kind="outlet" name="alertTitle" candidateClass="UILabel"/>
|
||||
<relationship kind="outlet" name="contentContainer" candidateClass="UIView"/>
|
||||
<relationship kind="outlet" name="contentField" candidateClass="UITextField"/>
|
||||
<relationship kind="outlet" name="contentTipBody" candidateClass="UILabel"/>
|
||||
<relationship kind="outlet" name="contentTipContainer" candidateClass="UIView"/>
|
||||
<relationship kind="outlet" name="contentTipEditIcon" candidateClass="UIImageView"/>
|
||||
<relationship kind="outlet" name="helpContainer" candidateClass="UIView"/>
|
||||
<relationship kind="outlet" name="helpView" candidateClass="UIWebView"/>
|
||||
<relationship kind="outlet" name="passwordCounter" candidateClass="UILabel"/>
|
||||
<relationship kind="outlet" name="passwordEdit" candidateClass="UIButton"/>
|
||||
<relationship kind="outlet" name="passwordIncrementer" candidateClass="UIButton"/>
|
||||
<relationship kind="outlet" name="searchResultsController" candidateClass="MPSearchDelegate"/>
|
||||
<relationship kind="outlet" name="searchTipContainer" candidateClass="UIView"/>
|
||||
<relationship kind="outlet" name="siteName" candidateClass="UILabel"/>
|
||||
<relationship kind="outlet" name="typeButton" candidateClass="UIButton"/>
|
||||
<relationship kind="outlet" name="typeTipContainer" candidateClass="UIView"/>
|
||||
</relationships>
|
||||
</class>
|
||||
<class className="MPPreferencesViewController" superclassName="UITableViewController">
|
||||
<source key="sourceIdentifier" type="project" relativePath="./Classes/MPPreferencesViewController.h"/>
|
||||
<relationships>
|
||||
<relationship kind="outlet" name="avatarScrollView" candidateClass="UIScrollView"/>
|
||||
</relationships>
|
||||
</class>
|
||||
<class className="MPSearchDelegate" superclassName="NSObject">
|
||||
<source key="sourceIdentifier" type="project" relativePath="./Classes/MPSearchDelegate.h"/>
|
||||
<relationships>
|
||||
<relationship kind="outlet" name="delegate"/>
|
||||
<relationship kind="outlet" name="searchDisplayController" candidateClass="UISearchDisplayController"/>
|
||||
<relationship kind="outlet" name="searchTipContainer" candidateClass="UIView"/>
|
||||
</relationships>
|
||||
</class>
|
||||
<class className="MPTypeViewController" superclassName="UITableViewController">
|
||||
<source key="sourceIdentifier" type="project" relativePath="./Classes/MPTypeViewController.h"/>
|
||||
<relationships>
|
||||
<relationship kind="outlet" name="recommendedTipContainer" candidateClass="UIView"/>
|
||||
</relationships>
|
||||
</class>
|
||||
<class className="MPUnlockViewController" superclassName="UIViewController">
|
||||
<source key="sourceIdentifier" type="project" relativePath="./Classes/MPUnlockViewController.h"/>
|
||||
<relationships>
|
||||
<relationship kind="action" name="deleteTargetedUser:" candidateClass="UILongPressGestureRecognizer"/>
|
||||
<relationship kind="outlet" name="deleteTip" candidateClass="UILabel"/>
|
||||
<relationship kind="outlet" name="oldUsernameLabel" candidateClass="UILabel"/>
|
||||
<relationship kind="outlet" name="passwordField" candidateClass="UITextField"/>
|
||||
<relationship kind="outlet" name="passwordView" candidateClass="UIView"/>
|
||||
<relationship kind="outlet" name="spinner" candidateClass="UIImageView"/>
|
||||
<relationship kind="outlet" name="userButtonTemplate" candidateClass="UIButton"/>
|
||||
<relationship kind="outlet" name="usernameLabel" candidateClass="UILabel"/>
|
||||
<relationship kind="outlet" name="usersView" candidateClass="UIScrollView"/>
|
||||
</relationships>
|
||||
</class>
|
||||
</classes>
|
||||
<simulatedMetricsContainer key="defaultSimulatedMetrics">
|
||||
<nil key="statusBar"/>
|
||||
<simulatedOrientationMetrics key="orientation"/>
|
||||
|
Reference in New Issue
Block a user