mirror of
https://github.com/TermiT/Flycut.git
synced 2024-09-20 15:26:05 +08:00
1073 lines
35 KiB
Objective-C
1073 lines
35 KiB
Objective-C
//
|
|
// FlycutOperator.m
|
|
// Flycut
|
|
//
|
|
// Flycut by Gennadiy Potapov and contributors. Based on Jumpcut by Steve Cook.
|
|
// Copyright 2011 General Arcade. All rights reserved.
|
|
//
|
|
// This code is open-source software subject to the MIT License; see the homepage
|
|
// at <https://github.com/TermiT/Flycut> for details.
|
|
//
|
|
|
|
// FlycutOperator owns and interacts with the FlycutStores, providing
|
|
// manipulation of the stores.
|
|
|
|
#import <Foundation/Foundation.h>
|
|
#import "FlycutOperator.h"
|
|
#import "MJCloudKitUserDefaultsSync/MJCloudKitUserDefaultsSync/MJCloudKitUserDefaultsSync.h"
|
|
|
|
@implementation FlycutOperator
|
|
|
|
- (id)init
|
|
{
|
|
[[NSUserDefaults standardUserDefaults] registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:
|
|
[NSNumber numberWithInt:40],
|
|
@"rememberNum",
|
|
[NSNumber numberWithInt:40],
|
|
@"favoritesRememberNum",
|
|
[NSNumber numberWithInt:1],
|
|
@"savePreference",
|
|
[NSDictionary dictionary],
|
|
@"store",
|
|
[NSNumber numberWithBool:YES],
|
|
@"skipPasswordFields",
|
|
[NSNumber numberWithBool:YES],
|
|
@"skipPboardTypes",
|
|
@"PasswordPboardType",
|
|
@"skipPboardTypesList",
|
|
[NSNumber numberWithBool:NO],
|
|
@"skipPasswordLengths",
|
|
@"12, 20, 32",
|
|
@"skipPasswordLengthsList",
|
|
[NSNumber numberWithBool:NO],
|
|
@"revealPasteboardTypes",
|
|
[NSNumber numberWithBool:NO],
|
|
@"removeDuplicates",
|
|
[NSNumber numberWithBool:NO],
|
|
@"pasteMovesToTop",
|
|
[NSNumber numberWithBool:NO],
|
|
@"syncSettingsViaICloud",
|
|
[NSNumber numberWithBool:NO],
|
|
@"syncClippingsViaICloud",
|
|
nil]];
|
|
|
|
settingsSyncList = @[@"rememberNum",
|
|
@"favoritesRememberNum",
|
|
@"savePreference",
|
|
@"skipPasswordFields",
|
|
@"skipPboardTypes",
|
|
@"skipPboardTypesList",
|
|
@"skipPasswordLengths",
|
|
@"skipPasswordLengthsList",
|
|
@"removeDuplicates",
|
|
@"pasteMovesToTop"];
|
|
[settingsSyncList retain];
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)awakeFromNibDisplaying:(int) dispNum withDisplayLength:(int) dispLength withSaveSelector:(SEL) selector forTarget:(NSObject*) target
|
|
{
|
|
displayNum = dispNum;
|
|
displayLength = dispLength;
|
|
saveSelector = selector;
|
|
saveTarget = target;
|
|
|
|
// Initialize the FlycutStore
|
|
[self initializeStoresAndLoadContents];
|
|
|
|
// Stack position starts @ 0 by default
|
|
stackPosition = favoritesStackPosition = stashedStackPosition = 0;
|
|
|
|
[self registerOrDeregisterICloudSync];
|
|
}
|
|
|
|
-(FlycutStore*) allocInitFlycutStoreRemembering:(int) remembering
|
|
{
|
|
return [[FlycutStore alloc] initRemembering:remembering
|
|
displaying:displayNum
|
|
withDisplayLength:displayLength];
|
|
}
|
|
|
|
-(void) initializeStores
|
|
{
|
|
// Fixme - These stores are not released anywhere.
|
|
if ( !clippingStore )
|
|
{
|
|
clippingStore = [self allocInitFlycutStoreRemembering:[[NSUserDefaults standardUserDefaults] integerForKey:@"rememberNum"]];
|
|
clippingStore.deleteDelegate = self;
|
|
}
|
|
else
|
|
{
|
|
[clippingStore setDisplayNum:displayNum];
|
|
[clippingStore setDisplayLen:displayLength];
|
|
}
|
|
|
|
if ( ! favoritesStore )
|
|
{
|
|
favoritesStore = [self allocInitFlycutStoreRemembering:[[NSUserDefaults standardUserDefaults] integerForKey:@"favoritesRememberNum"]];
|
|
favoritesStore.deleteDelegate = self;
|
|
}
|
|
else
|
|
{
|
|
[favoritesStore setDisplayNum:displayNum];
|
|
[favoritesStore setDisplayLen:displayLength];
|
|
}
|
|
|
|
stashedStore = NULL;
|
|
}
|
|
|
|
-(void) initializeStoresAndLoadContents
|
|
{
|
|
[self initializeStores];
|
|
|
|
// If our preferences indicate that we are saving, load the dictionary from the saved plist
|
|
// and use it to get everything set up.
|
|
if ( [[NSUserDefaults standardUserDefaults] integerForKey:@"savePreference"] >= 1 ) {
|
|
[self loadEngineFromPList];
|
|
}
|
|
}
|
|
|
|
-(void) willShowPreferences
|
|
{
|
|
issuedRememberResizeWarning = NO;
|
|
}
|
|
|
|
-(int) setRememberNum:(int)newRemember forPrimaryStore:(BOOL) isPrimaryStore
|
|
{
|
|
int oldRemeber = [self rememberNum];
|
|
|
|
// Ensure that we don't remember zero or fewer clippings.
|
|
if ( newRemember <= 0 )
|
|
{
|
|
newRemember = oldRemeber;
|
|
if ( newRemember <= 0 )
|
|
newRemember = 40;
|
|
}
|
|
|
|
if ( newRemember < [self jcListCount] &&
|
|
! issuedRememberResizeWarning &&
|
|
! [[NSUserDefaults standardUserDefaults] boolForKey:@"stifleRememberResizeWarning"]
|
|
) {
|
|
|
|
NSString *choice = [self delegateAlertWithMessageText:@"Resize Stack"
|
|
informationText:@"Resizing the stack to a value below its present size will cause clippings to be lost."
|
|
buttonsTexts:@[@"Resize", @"Cancel", @"Don't Warn Me Again"]];
|
|
if ( [choice isEqualToString:@"Cancel"] ) {
|
|
// Cancel - Change to prior setting.
|
|
newRemember = oldRemeber;
|
|
if ( isPrimaryStore ) {
|
|
[[NSUserDefaults standardUserDefaults] setValue:[NSNumber numberWithInt:newRemember]
|
|
forKey:@"rememberNum"];
|
|
} else {
|
|
[[NSUserDefaults standardUserDefaults] setValue:[NSNumber numberWithInt:newRemember]
|
|
forKey:@"favoritesRememberNum"];
|
|
}
|
|
} else if ( [choice isEqualToString:@"Don't Warn Me Again"] ) {
|
|
// Don't Warn Me Again
|
|
[[NSUserDefaults standardUserDefaults] setValue:[NSNumber numberWithBool:YES]
|
|
forKey:@"stifleRememberResizeWarning"];
|
|
} else {
|
|
// Resize
|
|
issuedRememberResizeWarning = YES;
|
|
}
|
|
}
|
|
|
|
// Set the value.
|
|
[clippingStore setRememberNum:newRemember];
|
|
|
|
return newRemember;
|
|
}
|
|
|
|
-(void)toggleToFromFavoritesStore
|
|
{
|
|
if (NULL != stashedStore)
|
|
[self restoreStashedStore];
|
|
else
|
|
[self switchToFavoritesStore];
|
|
}
|
|
|
|
-(bool)favoritesStoreIsSelected
|
|
{
|
|
return clippingStore == favoritesStore;
|
|
}
|
|
|
|
-(void)switchToFavoritesStore
|
|
{
|
|
stashedStore = clippingStore;
|
|
clippingStore = favoritesStore;
|
|
stashedStackPosition = stackPosition;
|
|
stackPosition = favoritesStackPosition;
|
|
}
|
|
|
|
- (bool)restoreStashedStore
|
|
{
|
|
if (NULL != stashedStore)
|
|
{
|
|
clippingStore = stashedStore;
|
|
stashedStore = NULL;
|
|
favoritesStackPosition = stackPosition;
|
|
stackPosition = stashedStackPosition;
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (NSString*)getPasteFromStackPosition
|
|
{
|
|
if ( [clippingStore jcListCount] > stackPosition ) {
|
|
return [self getPasteFromIndex: stackPosition];
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (bool)saveFromStack
|
|
{
|
|
return [self saveFromStackWithPrefix:@""];
|
|
}
|
|
|
|
- (bool)saveFromStackWithPrefix:(NSString*) prefix
|
|
{
|
|
return [self saveFromStore:clippingStore atIndex:stackPosition withPrefix:prefix];
|
|
}
|
|
|
|
- (bool)saveFromStore:(FlycutStore*)store atIndex:(int)index withPrefix:(NSString*) prefix
|
|
{
|
|
if ( [store jcListCount] > index ) {
|
|
// Get text from clipping store.
|
|
NSString *pbFullText = [self clippingStringWithCount:index inStore:store];
|
|
pbFullText = [pbFullText stringByReplacingOccurrencesOfString:@"\r" withString:@"\r\n"];
|
|
|
|
// Get the Desktop directory:
|
|
NSArray *paths = NSSearchPathForDirectoriesInDomains
|
|
(NSDesktopDirectory, NSUserDomainMask, YES);
|
|
NSString *desktopDirectory = [paths objectAtIndex:0];
|
|
|
|
// Get the timestamp string:
|
|
NSDate *currentDate = [NSDate date];
|
|
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
|
|
[dateFormatter setDateFormat:@"YYYY-MM-dd 'at' HH.mm.ss"];
|
|
NSString *dateString = [dateFormatter stringFromDate:currentDate];
|
|
|
|
// Make a file name to write the data to using the Desktop directory:
|
|
NSString *fileName = [NSString stringWithFormat:@"%@/%@%@Clipping %@.txt",
|
|
desktopDirectory, prefix, store == favoritesStore ? @"Favorite " : @"", dateString];
|
|
|
|
// Save content to the file
|
|
[pbFullText writeToFile:fileName
|
|
atomically:NO
|
|
encoding:NSNonLossyASCIIStringEncoding
|
|
error:nil];
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (bool)saveFromStackToFavorites
|
|
{
|
|
if ( clippingStore != favoritesStore && [clippingStore jcListCount] > stackPosition ) {
|
|
// Get text from clipping store.
|
|
[favoritesStore addClipping:[clippingStore clippingAtPosition:stackPosition] ];
|
|
[self clearItemAtStackPosition];
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (NSString*)getPasteFromIndex:(int) position {
|
|
NSString *clipping = [self getClipFromCount:position];
|
|
|
|
if ( [[NSUserDefaults standardUserDefaults] boolForKey:@"pasteMovesToTop"] ) {
|
|
[clippingStore clippingMoveToTop:position];
|
|
stackPosition = 0;
|
|
|
|
[self actionAfterListModification];
|
|
}
|
|
return clipping;
|
|
}
|
|
|
|
-(NSString*)getClipFromCount:(int)indexInt
|
|
{
|
|
NSString *pbFullText;
|
|
NSArray *pbTypes;
|
|
if ( (indexInt + 1) > [clippingStore jcListCount] ) {
|
|
// We're asking for a clipping that isn't there yet
|
|
// This only tends to happen immediately on startup when not saving, as the entire list is empty.
|
|
DLog(@"Out of bounds request to jcList ignored.");
|
|
return nil;
|
|
}
|
|
return [self clippingStringWithCount:indexInt];
|
|
}
|
|
|
|
-(BOOL)shouldSkip:(NSString *)contents ofType:(NSString *)type
|
|
{
|
|
// Check to see if we are skipping passwords based on length and characters.
|
|
if ( [[NSUserDefaults standardUserDefaults] boolForKey:@"skipPasswordFields"] )
|
|
{
|
|
// Check to see if they want a little help figuring out what types to enter.
|
|
if ( [[NSUserDefaults standardUserDefaults] boolForKey:@"revealPasteboardTypes"] )
|
|
[clippingStore addClipping:type ofType:type fromAppLocalizedName:@"Flycut" fromAppBundleURL:nil atTimestamp:0];
|
|
[self actionAfterListModification];
|
|
|
|
__block bool skipClipping = NO;
|
|
|
|
// Check the array of types to skip.
|
|
if ( [[NSUserDefaults standardUserDefaults] boolForKey:@"skipPboardTypes"] )
|
|
{
|
|
NSArray *typesArray = [[[[NSUserDefaults standardUserDefaults] stringForKey:@"skipPboardTypesList"] stringByReplacingOccurrencesOfString:@" " withString:@""] componentsSeparatedByString: @","];
|
|
[typesArray enumerateObjectsUsingBlock:^(id typeString, NSUInteger idx, BOOL *stop)
|
|
{
|
|
if ( [type isEqualToString:typeString] )
|
|
{
|
|
skipClipping = YES;
|
|
*stop = YES;
|
|
}
|
|
}];
|
|
}
|
|
if (skipClipping)
|
|
return YES;
|
|
|
|
// Check the array of lengths to skip for suspicious strings.
|
|
if ( [[NSUserDefaults standardUserDefaults] boolForKey:@"skipPasswordLengths"] )
|
|
{
|
|
int contentsLength = [contents length];
|
|
NSArray *lengthsArray = [[[[NSUserDefaults standardUserDefaults] stringForKey:@"skipPasswordLengthsList"] stringByReplacingOccurrencesOfString:@" " withString:@""] componentsSeparatedByString: @","];
|
|
[lengthsArray enumerateObjectsUsingBlock:^(id lengthString, NSUInteger idx, BOOL *stop)
|
|
{
|
|
if ( [lengthString integerValue] == contentsLength )
|
|
{
|
|
NSRange uppercaseLetter = [contents rangeOfCharacterFromSet: [NSCharacterSet uppercaseLetterCharacterSet]];
|
|
NSRange lowercaseLetter = [contents rangeOfCharacterFromSet: [NSCharacterSet lowercaseLetterCharacterSet]];
|
|
NSRange decimalDigit = [contents rangeOfCharacterFromSet: [NSCharacterSet decimalDigitCharacterSet]];
|
|
NSRange punctuation = [contents rangeOfCharacterFromSet: [NSCharacterSet punctuationCharacterSet]];
|
|
NSRange symbol = [contents rangeOfCharacterFromSet: [NSCharacterSet symbolCharacterSet]];
|
|
NSRange control = [contents rangeOfCharacterFromSet: [NSCharacterSet controlCharacterSet]];
|
|
NSRange illegal = [contents rangeOfCharacterFromSet: [NSCharacterSet illegalCharacterSet]];
|
|
NSRange whitespaceAndNewline = [contents rangeOfCharacterFromSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
if ( NSNotFound == control.location
|
|
&& NSNotFound == illegal.location
|
|
&& NSNotFound == whitespaceAndNewline.location
|
|
&& NSNotFound != uppercaseLetter.location
|
|
&& NSNotFound != lowercaseLetter.location
|
|
&& NSNotFound != decimalDigit.location
|
|
&& ( NSNotFound != punctuation.location
|
|
|| NSNotFound != symbol.location ) )
|
|
{
|
|
skipClipping = YES;
|
|
*stop = YES;
|
|
}
|
|
}
|
|
}];
|
|
|
|
if (skipClipping)
|
|
return YES;
|
|
}
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
-(void)setDisableStoreTo:(bool) value
|
|
{
|
|
disableStore = value;
|
|
}
|
|
|
|
-(bool)storeDisabled
|
|
{
|
|
return disableStore;
|
|
}
|
|
|
|
-(void)setClippingsStoreDelegate:(id<FlycutStoreDelegate>) delegate
|
|
{
|
|
if ( !clippingStore )
|
|
[self initializeStores];
|
|
clippingStore.delegate = delegate;
|
|
}
|
|
|
|
-(void)setFavoritesStoreDelegate:(id<FlycutStoreDelegate>) delegate
|
|
{
|
|
if ( !favoritesStore )
|
|
[self initializeStores];
|
|
favoritesStore.delegate = delegate;
|
|
}
|
|
|
|
-(int)indexOfClipping:(NSString*)contents ofType:(NSString*)type fromApp:(NSString *)appName withAppBundleURL:(NSString *)bundleURL
|
|
{
|
|
return [clippingStore indexOfClipping:contents
|
|
ofType:type
|
|
fromAppLocalizedName:appName
|
|
fromAppBundleURL:bundleURL
|
|
atTimestamp:[[NSDate date] timeIntervalSince1970]];
|
|
}
|
|
|
|
-(bool)addClipping:(NSString*)contents ofType:(NSString*)type fromApp:(NSString *)appName withAppBundleURL:(NSString *)bundleURL target:(id)selectorTarget clippingAddedSelector:(SEL)clippingAddedSelector
|
|
{
|
|
if ( [clippingStore jcListCount] == 0 || ! [contents isEqualToString:[clippingStore clippingContentsAtPosition:0]]) {
|
|
bool success = [clippingStore addClipping:contents
|
|
ofType:type
|
|
fromAppLocalizedName:appName
|
|
fromAppBundleURL:bundleURL
|
|
atTimestamp:[[NSDate date] timeIntervalSince1970]];
|
|
|
|
// The below tracks our position down down down... Maybe as an option?
|
|
// if ( [clippingStore jcListCount] > 1 ) stackPosition++;
|
|
stackPosition = 0;
|
|
[selectorTarget performSelector:clippingAddedSelector];
|
|
[self actionAfterListModification];
|
|
|
|
return success;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (void)willDeleteClippingFromStore:(id)store AtIndex:(int)index {
|
|
if ( (!inhibitAutosaveClippings) // Avoid saving things that the user explicitly deletes.
|
|
&& ( store == favoritesStore
|
|
? [[[NSUserDefaults standardUserDefaults] valueForKey:@"saveForgottenFavorites"] boolValue]
|
|
: [[[NSUserDefaults standardUserDefaults] valueForKey:@"saveForgottenClippings"] boolValue] ) )
|
|
{
|
|
// clipping is being removed, so save it before it gets lost.
|
|
// Set to last item, save, and restore position.
|
|
[self saveFromStore:store atIndex:index withPrefix:@"Autosave "];
|
|
}
|
|
}
|
|
|
|
-(void)actionAfterListModification
|
|
{
|
|
if ( !inhibitSaveEngineAfterListModification
|
|
&& [[NSUserDefaults standardUserDefaults] integerForKey:@"savePreference"] >= 2 )
|
|
[self saveEngine];
|
|
}
|
|
|
|
-(int)jcListCount
|
|
{
|
|
return [clippingStore jcListCount];
|
|
}
|
|
|
|
-(int)rememberNum
|
|
{
|
|
return [clippingStore rememberNum];
|
|
}
|
|
|
|
-(int)stackPosition
|
|
{
|
|
return stackPosition;
|
|
}
|
|
|
|
-(bool)setStackPositionToFirstItem
|
|
{
|
|
if ( [clippingStore jcListCount] > 0 ) {
|
|
stackPosition = 0;
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
-(bool)setStackPositionToLastItem
|
|
{
|
|
if ( [clippingStore jcListCount] > 0 ) {
|
|
stackPosition = [clippingStore jcListCount] - 1;
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
-(bool)setStackPositionToTenMoreRecent
|
|
{
|
|
if ( [clippingStore jcListCount] > 0 ) {
|
|
stackPosition = stackPosition - 10; if ( stackPosition < 0 ) stackPosition = 0;
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
-(bool)setStackPositionToTenLessRecent
|
|
{
|
|
if ( [clippingStore jcListCount] > 0 ) {
|
|
stackPosition = stackPosition + 10; if ( stackPosition >= [clippingStore jcListCount] ) stackPosition = [clippingStore jcListCount] - 1;
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
-(bool)clearItemAtStackPosition
|
|
{
|
|
if ([clippingStore jcListCount] == 0)
|
|
return NO;
|
|
|
|
inhibitAutosaveClippings = YES; // Avoid saving things that the user explicitly deletes.
|
|
[clippingStore clearItem:stackPosition];
|
|
inhibitAutosaveClippings = NO;
|
|
[self actionAfterListModification];
|
|
|
|
return YES;
|
|
}
|
|
|
|
-(bool)setStackPositionTo:(int) newStackPosition
|
|
{
|
|
if ( [clippingStore jcListCount] >= newStackPosition ) {
|
|
stackPosition = newStackPosition;
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
// Would probably be good to just prevent this scenario where it originates and
|
|
// delete this check.
|
|
-(void)adjustStackPositionIfOutOfBounds
|
|
{
|
|
if (stackPosition >= [clippingStore jcListCount] && stackPosition != 0) { // deleted last item
|
|
stackPosition = [clippingStore jcListCount] - 1;
|
|
}
|
|
}
|
|
|
|
-(bool)stackPositionIsInBounds
|
|
{
|
|
return ( [clippingStore jcListCount] > 0 && [clippingStore jcListCount] > stackPosition );
|
|
}
|
|
|
|
-(void)clearList
|
|
{
|
|
[clippingStore clearList];
|
|
[self actionAfterListModification];
|
|
}
|
|
|
|
-(void)mergeList
|
|
{
|
|
[clippingStore mergeList];
|
|
}
|
|
|
|
-(BOOL) isValidClippingNumber:(NSNumber *)number {
|
|
return [self isValidClippingNumber:number inStore:clippingStore];
|
|
}
|
|
|
|
-(BOOL) isValidClippingNumber:(NSNumber *)number inStore:(FlycutStore*)store {
|
|
return ( ([number intValue] + 1) <= [store jcListCount] );
|
|
}
|
|
|
|
-(NSString *) clippingStringWithCount:(int)count {
|
|
return [self clippingStringWithCount:count inStore:clippingStore];
|
|
}
|
|
|
|
-(NSString *) clippingStringWithCount:(int)count inStore:(FlycutStore*)store {
|
|
if ( [self isValidClippingNumber:[NSNumber numberWithInt:count] inStore:store] ) {
|
|
return [store clippingContentsAtPosition:count];
|
|
} else { // It fails -- we shouldn't be passed this, but...
|
|
return @"";
|
|
}
|
|
}
|
|
|
|
-(void) registerOrDeregisterICloudSync
|
|
{
|
|
if ( [[NSUserDefaults standardUserDefaults] boolForKey:@"syncSettingsViaICloud"] ) {
|
|
[[MJCloudKitUserDefaultsSync sharedSync] startWithKeyMatchList:settingsSyncList
|
|
withContainerIdentifier:@"iCloud.com.mark-a-jerde.Flycut"];
|
|
}
|
|
else {
|
|
[[MJCloudKitUserDefaultsSync sharedSync] stopForKeyMatchList:settingsSyncList];
|
|
}
|
|
|
|
BOOL syncClippings = [[NSUserDefaults standardUserDefaults] boolForKey:@"syncClippingsViaICloud"];
|
|
BOOL changedSyncClippings = ( ![[NSUserDefaults standardUserDefaults] objectForKey:@"previousSyncClippingsViaICloud"]
|
|
|| syncClippings != [[NSUserDefaults standardUserDefaults] boolForKey:@"previousSyncClippingsViaICloud"] );
|
|
|
|
if ( changedSyncClippings )
|
|
[[NSUserDefaults standardUserDefaults] setValue:[NSNumber numberWithBool:syncClippings]
|
|
forKey:@"previousSyncClippingsViaICloud"];
|
|
|
|
// We will enable / disable regardless of changedSyncClippings because this gets called at app launch, where the feature was previously enabled but needs to be registered.
|
|
if ( syncClippings ) {
|
|
if ( changedSyncClippings )
|
|
firstClippingsSyncAfterEnabling = YES;
|
|
|
|
[[MJCloudKitUserDefaultsSync sharedSync] removeNotificationsFor:MJSyncNotificationChanges forTarget:self];
|
|
[[MJCloudKitUserDefaultsSync sharedSync] addNotificationFor:MJSyncNotificationChanges withSelector:@selector(checkPreferencesChanges:) withTarget: self];
|
|
|
|
[[MJCloudKitUserDefaultsSync sharedSync] removeNotificationsFor:MJSyncNotificationConflicts forTarget:self];
|
|
[[MJCloudKitUserDefaultsSync sharedSync] addNotificationFor:MJSyncNotificationConflicts withSelector:@selector(checkPreferencesConflicts:) withTarget: self];
|
|
|
|
[[MJCloudKitUserDefaultsSync sharedSync] removeNotificationsFor:MJSyncNotificationSaveSuccess forTarget:self];
|
|
[[MJCloudKitUserDefaultsSync sharedSync] addNotificationFor:MJSyncNotificationSaveSuccess withSelector:@selector(checkPreferencesSaveSuccess:) withTarget: self];
|
|
|
|
[[MJCloudKitUserDefaultsSync sharedSync] startWithKeyMatchList:@[@"store"]
|
|
withContainerIdentifier:@"iCloud.com.mark-a-jerde.Flycut"];
|
|
}
|
|
else {
|
|
[[MJCloudKitUserDefaultsSync sharedSync] removeNotificationsFor:MJSyncNotificationChanges forTarget:self];
|
|
|
|
[[MJCloudKitUserDefaultsSync sharedSync] stopForKeyMatchList:@[@"store"]];
|
|
}
|
|
}
|
|
|
|
-(NSDictionary*) checkPreferencesChanges:(NSDictionary*)changes
|
|
{
|
|
if ( [changes valueForKey:@"store"] )
|
|
{
|
|
[self integrateAllStores];
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
-(void) integrateAllStores
|
|
{
|
|
DLog(@"integrating stores");
|
|
inhibitSaveEngineAfterListModification = YES;
|
|
|
|
[self integrateStoreAtKey:@"jcList" into:clippingStore descriptiveName:@""];
|
|
// It is possible that the user would disable sync rather than merge the main clippings store. They would have a poor user experience if they were then asked the same question about the favorites store after believing that they had disabled sync, so check setting before integrating.
|
|
if ( [[NSUserDefaults standardUserDefaults] boolForKey:@"syncClippingsViaICloud"] )
|
|
[self integrateStoreAtKey:@"favoritesList" into:favoritesStore descriptiveName:@"favorites "];
|
|
DLog(@"integrating stores complete");
|
|
|
|
inhibitSaveEngineAfterListModification = NO;
|
|
firstClippingsSyncAfterEnabling = NO;
|
|
[self actionAfterListModification];
|
|
}
|
|
|
|
-(void) integrateStoreAtKey:(NSString*)listKey into:(FlycutStore*)store descriptiveName:(NSString*)name
|
|
{
|
|
FlycutStore *newContent = [self allocInitFlycutStoreRemembering:[clippingStore rememberNum]];
|
|
NSDictionary *loadDict = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"store"] copy];
|
|
|
|
if ( loadDict && [self loadEngineFrom:loadDict key:listKey into:newContent] )
|
|
{
|
|
BOOL mergeLists = NO;
|
|
if ( firstClippingsSyncAfterEnabling )
|
|
{
|
|
if ( 0 == [store jcListCount] )
|
|
{
|
|
// Just accept whatever iCloud has.
|
|
[store clearInsertionJournalCount:[[store insertionJournal] count]];
|
|
[store clearDeletionJournalCount:[[store deletionJournal] count]];
|
|
}
|
|
else if ( 0 == [newContent jcListCount] )
|
|
{
|
|
// We have something. iCloud has nothing. Ignore iCloud this time.
|
|
[newContent release];
|
|
[self actionAfterListModification]; // To overwrite what sync put in the defaults.
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
int newCount = [newContent jcListCount];
|
|
int ourCount = [store jcListCount];
|
|
int newDistinct = 0;
|
|
int ourDistinct = 0;
|
|
for ( int i = 0 ; i < newCount ; i++ )
|
|
{
|
|
if ( 0 > [store indexOfClipping:[newContent clippingAtPosition:i]] )
|
|
{
|
|
newDistinct++;
|
|
}
|
|
}
|
|
for ( int i = 0 ; i < ourCount ; i++ )
|
|
{
|
|
if ( 0 > [newContent indexOfClipping:[store clippingAtPosition:i]] )
|
|
{
|
|
ourDistinct++;
|
|
}
|
|
}
|
|
|
|
BOOL promptUser = NO;
|
|
if ( 0 == ourDistinct )
|
|
{
|
|
// Just accept whatever iCloud has.
|
|
[store clearInsertionJournalCount:[[store insertionJournal] count]];
|
|
[store clearDeletionJournalCount:[[store deletionJournal] count]];
|
|
}
|
|
else if ( 0 == newDistinct )
|
|
{
|
|
// We have something. iCloud has nothing. Ignore iCloud this time.
|
|
[newContent release];
|
|
[self actionAfterListModification]; // To overwrite what sync put in the defaults.
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
// Policy: For sake of user experience, the user will not be asked to merge or overwrite if one is a superset of the other and they have just said that they want sync. Assume they meant, "I want sync and I want it to include all content."
|
|
promptUser = YES;
|
|
}
|
|
|
|
while ( promptUser )
|
|
{
|
|
promptUser = NO;
|
|
|
|
NSString *choice = [self delegateAlertWithMessageText:@"First Sync"
|
|
informationText:[NSString stringWithFormat:@"Flycut found %i %@clipping%@ shared by both iCloud and this device, %i only in iCloud, and \%i only on this device. How can I handle these for you?",
|
|
(ourCount-ourDistinct),
|
|
name,
|
|
((ourCount-ourDistinct)!=1?@"s":@""),
|
|
newDistinct,ourDistinct]
|
|
buttonsTexts:@[@"Merge Lists",
|
|
@"Overwrite Device List",
|
|
@"Overwrite iCloud List",
|
|
@"Disable Sync"]];
|
|
|
|
if (!choice )
|
|
{
|
|
// This most likely means the UI wasn't implemented, so cover it with merge.
|
|
choice = @"Merge Lists";
|
|
}
|
|
|
|
if ( [choice isEqualToString:@"Merge Lists"] )
|
|
{
|
|
mergeLists = YES;
|
|
}
|
|
else if ( [choice isEqualToString:@"Disable Sync"] )
|
|
{
|
|
[[NSUserDefaults standardUserDefaults] setValue:[NSNumber numberWithBool:NO]
|
|
forKey:@"syncClippingsViaICloud"];
|
|
[newContent release];
|
|
[self registerOrDeregisterICloudSync];
|
|
[self actionAfterListModification]; // To overwrite what sync put in the defaults.
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
NSString *okCancel = [self delegateAlertWithMessageText:@"Warning"
|
|
informationText:[NSString stringWithFormat:@"%@ will cause clippings to be lost!", choice]
|
|
buttonsTexts:@[@"Ok", @"Cancel"]];
|
|
|
|
if ( [okCancel isEqualToString:@"Ok"] )
|
|
{
|
|
if ( [choice isEqualToString:@"Overwrite Device List"] )
|
|
{
|
|
// Just accept whatever iCloud has.
|
|
[store clearInsertionJournalCount:[[store insertionJournal] count]];
|
|
[store clearDeletionJournalCount:[[store deletionJournal] count]];
|
|
}
|
|
else if ( [choice isEqualToString:@"Overwrite iCloud List"] )
|
|
{
|
|
// Ignore iCloud this time.
|
|
[newContent release];
|
|
[self actionAfterListModification]; // To overwrite what sync put in the defaults.
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
// This should be impossible, so cover it with disabling sync.
|
|
[[NSUserDefaults standardUserDefaults] setValue:[NSNumber numberWithBool:NO] forKey:@"syncClippingsViaICloud"];
|
|
[newContent release];
|
|
[self registerOrDeregisterICloudSync];
|
|
[self actionAfterListModification]; // To overwrite what sync put in the defaults.
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
promptUser = YES;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
int newCount = [newContent jcListCount];
|
|
int offsetForMerge = 0;
|
|
for ( int i = 0 ; i < newCount ; i++ )
|
|
{
|
|
FlycutClipping *newClipping = [newContent clippingAtPosition:i];
|
|
if ( i >= [store jcListCount] )
|
|
{
|
|
// Clipping was beyond the end of the store, so just add it.
|
|
[self integrateInsertClipping:newClipping toStore:store atIndex:(i+offsetForMerge) withMerge:mergeLists];
|
|
}
|
|
else if ( ![newClipping isEqual:[store clippingAtPosition:(i+offsetForMerge)]] )
|
|
{
|
|
BOOL contentAtThisStorePositionNotInNewContent = (i+offsetForMerge) < [store jcListCount] && [newContent indexOfClipping:[store clippingAtPosition:(i+offsetForMerge)]] < 0;
|
|
int firstIndex = [store indexOfClipping:newClipping];
|
|
if ( firstIndex < 0 && [[store deletionJournal] containsObject:newClipping] )
|
|
{
|
|
// Clipping was deleted locally, so delete from the new content and move on.
|
|
[newContent clearItem:i];
|
|
i--;
|
|
newCount--;
|
|
}
|
|
else if ( firstIndex < 0 )
|
|
{
|
|
// Clipping wasn't previously in the store, so just add it.
|
|
if ( mergeLists && contentAtThisStorePositionNotInNewContent )
|
|
{
|
|
// Give priority to local items so they end up at the top of the list.
|
|
// Look to the next store item.
|
|
offsetForMerge++;
|
|
// While checking this newContent item again.
|
|
i--;
|
|
}
|
|
else
|
|
{
|
|
[self integrateInsertClipping:newClipping toStore:store atIndex:(i+offsetForMerge) withMerge:mergeLists];
|
|
}
|
|
}
|
|
else if ( contentAtThisStorePositionNotInNewContent )
|
|
{
|
|
// Contents in the store at this position didn't exist in the newContent. Handle deletion.
|
|
if ( mergeLists
|
|
|| [[store insertionJournal] containsObject:[store clippingAtPosition:(i+offsetForMerge)]] )
|
|
{
|
|
// Look to the next store item.
|
|
offsetForMerge++;
|
|
// While checking this newContent item again.
|
|
i--;
|
|
}
|
|
else
|
|
{
|
|
[store clearItem:(i+offsetForMerge)];
|
|
i--;
|
|
}
|
|
}
|
|
else if ( [store removeDuplicates] )
|
|
{
|
|
if ( i < firstIndex )
|
|
[store clippingMoveFrom:firstIndex To:(i+offsetForMerge)];
|
|
else
|
|
{
|
|
// This can only happen if the remote store allowed duplicates and we do not. Just delete from the new content and move on.
|
|
[newContent clearItem:i];
|
|
i--;
|
|
newCount--;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[self integrateInsertClipping:newClipping toStore:store atIndex:(i+offsetForMerge) withMerge:mergeLists];
|
|
}
|
|
}
|
|
}
|
|
while ( [store jcListCount] > newCount + offsetForMerge )
|
|
[store clearItem:(newCount + offsetForMerge)];
|
|
|
|
#ifdef DEBUG
|
|
if ( !mergeLists )
|
|
{
|
|
[newContent release];
|
|
newContent = [self allocInitFlycutStoreRemembering:[clippingStore rememberNum]];
|
|
[self loadEngineFrom:loadDict key:listKey into:newContent];
|
|
newCount = [newContent jcListCount];
|
|
if ( newCount != [store jcListCount] )
|
|
NSLog(@"Error in integrateStoreAtKey with mismatching after counts!");
|
|
else
|
|
{
|
|
for ( int i = 0 ; i < newCount ; i++ )
|
|
{
|
|
if ( ![[store clippingAtPosition:i] isEqual:[newContent clippingAtPosition:i]] )
|
|
NSLog(@"Error in integrateStoreAtKey with mismatching clippings at index %i!", i);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
[newContent release];
|
|
}
|
|
|
|
-(void) integrateInsertClipping:(FlycutClipping*)clipping toStore:(FlycutStore*)store atIndex:(int)index withMerge:(BOOL)mergeLists
|
|
{
|
|
[clipping setDisplayLength:displayLength];
|
|
|
|
if ( mergeLists && [store jcListCount] == [store rememberNum] )
|
|
{
|
|
// Grow the rememberNum if needed in merge.
|
|
int newRememberNum = [store rememberNum] + 1;
|
|
[store setRememberNum:newRememberNum];
|
|
if ( store == favoritesStore )
|
|
{
|
|
[[NSUserDefaults standardUserDefaults] setValue:[NSNumber numberWithInt:newRememberNum]
|
|
forKey:@"favoritesRememberNum"];
|
|
}
|
|
else
|
|
{
|
|
[[NSUserDefaults standardUserDefaults] setValue:[NSNumber numberWithInt:newRememberNum]
|
|
forKey:@"rememberNum"];
|
|
}
|
|
}
|
|
|
|
[store insertClipping:clipping atIndex:index];
|
|
}
|
|
|
|
-(NSDictionary*) checkPreferencesConflicts:(NSDictionary*)changes
|
|
{
|
|
NSMutableDictionary *corrections = nil;
|
|
if ( [changes valueForKey:@"store"] )
|
|
{
|
|
// Load the version that the other party pushed.
|
|
[[NSUserDefaults standardUserDefaults] setObject:[changes valueForKey:@"store"][2] forKey:@"store"];
|
|
|
|
// Integrate stores to apply journaled changes to conflict resolution.
|
|
[self integrateAllStores];
|
|
|
|
// Load the resolution into corrections.
|
|
if ( !corrections )
|
|
corrections = [[NSMutableDictionary alloc] init];
|
|
corrections[@"store"] = [[NSUserDefaults standardUserDefaults] objectForKey: @"store"];
|
|
}
|
|
return corrections;
|
|
}
|
|
|
|
-(NSDictionary*) checkPreferencesSaveSuccess:(NSDictionary*)changes
|
|
{
|
|
if ( [changes valueForKey:@"store"] )
|
|
{
|
|
[clippingStore pruneJournals];
|
|
[favoritesStore pruneJournals];
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
-(void) checkCloudKitUpdates
|
|
{
|
|
[[MJCloudKitUserDefaultsSync sharedSync] checkCloudKitUpdates];
|
|
}
|
|
|
|
-(bool) loadEngineFrom:(NSDictionary*)loadDict key:(NSString*)listKey into:(FlycutStore*)store
|
|
{
|
|
NSArray *savedJCList = [loadDict objectForKey:listKey];
|
|
if ( [savedJCList isKindOfClass:[NSArray class]] ) {
|
|
// There's probably a nicer way to prevent the range from going out of bounds, but this works.
|
|
int rangeCap = [savedJCList count] < [store rememberNum] ? [savedJCList count] : [store rememberNum];
|
|
NSRange loadRange = NSMakeRange(0, rangeCap);
|
|
NSArray *toBeRestoredClips = [[[savedJCList subarrayWithRange:loadRange] reverseObjectEnumerator] allObjects];
|
|
for( NSDictionary *aSavedClipping in toBeRestoredClips)
|
|
[store addClipping:[aSavedClipping objectForKey:@"Contents"]
|
|
ofType:[aSavedClipping objectForKey:@"Type"]
|
|
fromAppLocalizedName:[aSavedClipping objectForKey:@"AppLocalizedName"]
|
|
fromAppBundleURL:[aSavedClipping objectForKey:@"AppBundleURL"]
|
|
atTimestamp:[[aSavedClipping objectForKey:@"Timestamp"] integerValue]];
|
|
return YES;
|
|
} else DLog(@"Not array");
|
|
return NO;
|
|
}
|
|
|
|
-(bool) loadEngineFromPList
|
|
{
|
|
NSDictionary *loadDict = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"store"] copy];
|
|
|
|
if ( loadDict != nil ) {
|
|
bool success = NO;
|
|
success |= [self loadEngineFrom:loadDict key:@"jcList" into:clippingStore];
|
|
success |= [self loadEngineFrom:loadDict key:@"favoritesList" into:favoritesStore];
|
|
[loadDict release];
|
|
return success;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
-(bool)setStackPositionToOneLessRecent
|
|
{
|
|
stackPosition++;
|
|
if ( [clippingStore jcListCount] > stackPosition ) {
|
|
return YES;
|
|
} else {
|
|
if ( [[NSUserDefaults standardUserDefaults] boolForKey:@"wraparoundBezel"] ) {
|
|
stackPosition = 0;
|
|
return YES;
|
|
} else {
|
|
stackPosition--;
|
|
}
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
-(bool)setStackPositionToOneMoreRecent
|
|
{
|
|
stackPosition--;
|
|
if ( stackPosition < 0 ) {
|
|
if ( [[NSUserDefaults standardUserDefaults] boolForKey:@"wraparoundBezel"] ) {
|
|
stackPosition = [clippingStore jcListCount] - 1;
|
|
return YES;
|
|
} else {
|
|
stackPosition = 0;
|
|
return NO;
|
|
}
|
|
}
|
|
if ( [clippingStore jcListCount] > stackPosition ) {
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
-(FlycutClipping*)clippingAtStackPosition
|
|
{
|
|
return [clippingStore clippingAtPosition:stackPosition];
|
|
}
|
|
|
|
- (void)saveStore:(FlycutStore *)store toKey:(NSString *)key onDict:(NSMutableDictionary *)saveDict {
|
|
NSMutableArray *jcListArray = [NSMutableArray array];
|
|
for ( int i = 0 ; i < [store jcListCount] ; i++ )
|
|
{
|
|
FlycutClipping *clipping = [store clippingAtPosition:i];
|
|
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
|
|
[clipping contents], @"Contents",
|
|
[clipping type], @"Type",
|
|
[NSNumber numberWithInt:i], @"Position",nil];
|
|
|
|
NSString *val = [clipping appLocalizedName];
|
|
if ( nil != val )
|
|
[dict setObject:val forKey:@"AppLocalizedName"];
|
|
|
|
val = [clipping appBundleURL];
|
|
if ( nil != val )
|
|
[dict setObject:val forKey:@"AppBundleURL"];
|
|
|
|
int timestamp = [clipping timestamp];
|
|
if ( timestamp > 0 )
|
|
[dict setObject:[NSNumber numberWithInt:timestamp] forKey:@"Timestamp"];
|
|
|
|
[jcListArray addObject:dict];
|
|
}
|
|
[saveDict setObject:jcListArray forKey:key];
|
|
[store clearModifiedSinceLastSaveStore];
|
|
}
|
|
|
|
-(void) saveEngine {
|
|
// saveEngine saves to NSUserDefaults. If there have been no modifications, just skip this to avoid busy activity for any observers.
|
|
if ( !([clippingStore modifiedSinceLastSaveStore]
|
|
|| [favoritesStore modifiedSinceLastSaveStore]) )
|
|
return;
|
|
|
|
NSMutableDictionary *saveDict;
|
|
saveDict = [NSMutableDictionary dictionaryWithCapacity:3];
|
|
[saveDict setObject:@"0.7" forKey:@"version"];
|
|
[saveDict setObject:[NSNumber numberWithInt:[[NSUserDefaults standardUserDefaults] integerForKey:@"rememberNum"]]
|
|
forKey:@"rememberNum"];
|
|
[saveDict setObject:[NSNumber numberWithInt:[[NSUserDefaults standardUserDefaults] integerForKey:@"favoritesRememberNum"]]
|
|
forKey:@"favoritesRememberNum"];
|
|
|
|
[saveTarget performSelector:saveSelector withObject:saveDict];
|
|
|
|
[self saveStore:clippingStore toKey:@"jcList" onDict:saveDict];
|
|
[self saveStore:favoritesStore toKey:@"favoritesList" onDict:saveDict];
|
|
|
|
[[NSUserDefaults standardUserDefaults] setObject:saveDict forKey:@"store"];
|
|
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
}
|
|
|
|
- (void)applicationWillTerminate {
|
|
if ( [[NSUserDefaults standardUserDefaults] integerForKey:@"savePreference"] >= 1 ) {
|
|
DLog(@"Saving on exit");
|
|
[self saveEngine];
|
|
} else {
|
|
// Remove clips from store
|
|
[[NSUserDefaults standardUserDefaults] setValue:[NSDictionary dictionary] forKey:@"store"];
|
|
DLog(@"Saving preferences on exit");
|
|
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
}
|
|
}
|
|
|
|
-(NSArray *) previousIndexes:(int)howMany containing:(NSString*)search // This method is in newest-first order.
|
|
{
|
|
return [clippingStore previousIndexes:howMany containing:search];
|
|
}
|
|
|
|
-(NSArray *) previousDisplayStrings:(int)howMany containing:(NSString*)search
|
|
{
|
|
return [clippingStore previousDisplayStrings:howMany containing:search];
|
|
}
|
|
|
|
-(NSString*) delegateAlertWithMessageText:(NSString*)message informationText:(NSString*)information buttonsTexts:(NSArray*)buttons
|
|
{
|
|
if ( self.delegate && [self.delegate respondsToSelector:@selector(alertWithMessageText:informationText:buttonsTexts:)] )
|
|
return [self.delegate alertWithMessageText:message informationText:information buttonsTexts:buttons];
|
|
return nil;
|
|
}
|
|
|
|
@end
|