mirror of
https://github.com/TermiT/Flycut.git
synced 2025-10-01 01:14:26 +08:00
Add mechanism to prompt user only if necessary, after receiving first sync pull, to merge or overwrite existing lists or disable sync. Include support for merge at initial pull, seubsequent pulls, and conflict resolution. Ask user if they would like to enable iCloud Sync on first iOS launch since preferences are not yet implemented on that platform.
This commit is contained in:
parent
442a5252a3
commit
4d11c5e934
5 changed files with 343 additions and 56 deletions
|
@ -22,7 +22,7 @@
|
|||
|
||||
@class SGHotKey;
|
||||
|
||||
@interface AppController : NSObject <NSMenuDelegate, NSApplicationDelegate, FlycutStoreDelegate> {
|
||||
@interface AppController : NSObject <NSMenuDelegate, NSApplicationDelegate, FlycutStoreDelegate, FlycutOperatorDelegate> {
|
||||
BezelWindow *bezel;
|
||||
SGHotKey *mainHotKey;
|
||||
IBOutlet SRRecorderControl *mainRecorder;
|
||||
|
|
|
@ -112,6 +112,7 @@
|
|||
|
||||
// Initialize the FlycutOperator
|
||||
flycutOperator = [[FlycutOperator alloc] init];
|
||||
flycutOperator.delegate = self;
|
||||
[flycutOperator setClippingsStoreDelegate:self];
|
||||
[flycutOperator setFavoritesStoreDelegate:self];
|
||||
[flycutOperator awakeFromNibDisplaying:[[NSUserDefaults standardUserDefaults] integerForKey:@"displayNum"]
|
||||
|
@ -1080,20 +1081,6 @@ didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
|
|||
}
|
||||
}
|
||||
|
||||
if ( [[NSUserDefaults standardUserDefaults] boolForKey:@"syncClippingsViaICloud"] ) {
|
||||
NSAlert *alert = [[NSAlert alloc] init];
|
||||
[alert setMessageText:@"Warning"];
|
||||
[alert addButtonWithTitle:@"Ok"];
|
||||
[alert addButtonWithTitle:@"Cancel"];
|
||||
[alert setInformativeText:@"Enabling iCloud Clippings Sync will overwrite local clippings if your iCloud account already has Flycut clippings. If you have never enabled this in Flycut on any computer, your current clippings will be retained and loaded into iCloud."];
|
||||
if ( [alert runModal] != NSAlertFirstButtonReturn )
|
||||
{
|
||||
[[NSUserDefaults standardUserDefaults] setValue:[NSNumber numberWithBool:NO]
|
||||
forKey:@"syncClippingsViaICloud"];
|
||||
}
|
||||
// Add option to overwrite iCloud.
|
||||
}
|
||||
|
||||
[self registerOrDeregisterICloudSync];
|
||||
}
|
||||
|
||||
|
@ -1287,6 +1274,20 @@ didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
|
|||
DLog(@"code: %ld, flags: %lu", (long)newKeyCombo.code, (unsigned long)newKeyCombo.flags);
|
||||
}
|
||||
|
||||
- (NSString*)alertWithMessageText:(NSString*)message informationText:(NSString*)information buttonsTexts:(NSArray*)buttons {
|
||||
NSAlert *alert = [[NSAlert alloc] init];
|
||||
[alert setMessageText:message];
|
||||
[buttons enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
[alert addButtonWithTitle:obj];
|
||||
}];
|
||||
[alert setInformativeText:information];
|
||||
NSInteger result = [alert runModal];
|
||||
[alert release];
|
||||
if ( result < NSAlertFirstButtonReturn || result >= NSAlertFirstButtonReturn + [buttons count] )
|
||||
return nil;
|
||||
return buttons[result - NSAlertFirstButtonReturn];
|
||||
}
|
||||
|
||||
- (void)beginUpdates {
|
||||
needBezelUpdate = NO;
|
||||
needMenuUpdate = NO;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, FlycutStoreDelegate {
|
||||
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, FlycutStoreDelegate, FlycutOperatorDelegate {
|
||||
|
||||
let flycut:FlycutOperator = FlycutOperator()
|
||||
var activeUpdates:Int = 0
|
||||
|
@ -17,6 +17,8 @@ class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSour
|
|||
var pbCount:Int = -1
|
||||
|
||||
let pasteboardInteractionQueue = DispatchQueue(label: "com.Flycut.pasteboardInteractionQueue")
|
||||
let alertHandlingQueue = DispatchQueue(label: "com.Flycut.alertHandlingQueue")
|
||||
|
||||
|
||||
// Some buttons we will reuse.
|
||||
var deleteButton:MGSwipeButton? = nil
|
||||
|
@ -56,11 +58,14 @@ class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSour
|
|||
return true;
|
||||
})
|
||||
|
||||
// Enable sync by default on iOS until we have a mechanism to adjust preferences on-device.
|
||||
UserDefaults.standard.set(NSNumber(value: true), forKey: "syncSettingsViaICloud")
|
||||
UserDefaults.standard.set(NSNumber(value: true), forKey: "syncClippingsViaICloud")
|
||||
// Force sync disable for test if needed.
|
||||
//UserDefaults.standard.set(NSNumber(value: false), forKey: "syncSettingsViaICloud")
|
||||
//UserDefaults.standard.set(NSNumber(value: false), forKey: "syncClippingsViaICloud")
|
||||
// Force to ask to enable sync for test if needed.
|
||||
//UserDefaults.standard.set(false, forKey: "alreadyAskedToEnableSync")
|
||||
|
||||
flycut.setClippingsStoreDelegate(self);
|
||||
flycut.setClippingsStoreDelegate(self)
|
||||
flycut.delegate = self
|
||||
|
||||
flycut.awake(fromNibDisplaying: 10, withDisplayLength: 140, withSave: #selector(savePreferences(toDict:)), forTarget: self) // The 10 isn't used in iOS right now and 140 characters seems to be enough to cover the width of the largest screen.
|
||||
|
||||
|
@ -69,6 +74,27 @@ class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSour
|
|||
NotificationCenter.default.addObserver(self, selector: #selector(self.applicationWillTerminate), name: .UIApplicationWillTerminate, object: nil)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
// Ask once to enable Sync. The syntax below will take the else unless alreadyAnswered is non-nil and true.
|
||||
let alreadyAsked = UserDefaults.standard.value(forKey: "alreadyAskedToEnableSync")
|
||||
if let answer = alreadyAsked, answer as! Bool
|
||||
{
|
||||
}
|
||||
else
|
||||
{
|
||||
// Don't use DispatchQueue.main.async since that will still end up blocking the UI draw until the user responds to what hasn't been drawn yet. Just create a queue to get us away from main, since this is a one-time code path.
|
||||
DispatchQueue(label: "com.Flycut.alertHandlingQueue", qos: .userInitiated ).async {
|
||||
let selection = self.alert(withMessageText: "iCloud Sync", informationText: "Would you like to enable Flycut's iCloud Sync for Settings and Clippings?", buttonsTexts: ["Yes", "No"])
|
||||
|
||||
let response = (selection == "Yes");
|
||||
UserDefaults.standard.set(NSNumber(value: response), forKey: "syncSettingsViaICloud")
|
||||
UserDefaults.standard.set(NSNumber(value: response), forKey: "syncClippingsViaICloud")
|
||||
UserDefaults.standard.set(true, forKey: "alreadyAskedToEnableSync")
|
||||
self.flycut.registerOrDeregisterICloudSync()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func savePreferences(toDict: NSMutableDictionary)
|
||||
{
|
||||
}
|
||||
|
@ -146,6 +172,31 @@ class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSour
|
|||
tableView.moveRow(at: IndexPath(row: Int(index), section: 0), to: IndexPath(row: Int(newIndex), section: 0))
|
||||
}
|
||||
|
||||
func alert(withMessageText message: String!, informationText information: String!, buttonsTexts buttons: [Any]!) -> String! {
|
||||
// Don't use DispatchQueue.main.async since that will still end up blocking the UI draw until the user responds to what hasn't been drawn yet. This isn't a great check, as it is OS-version-limited and results in a EXC_BAD_INSTRUCTION if it fails, but is good enough for development / test.
|
||||
if #available(iOS 10.0, *) {
|
||||
__dispatch_assert_queue_not(DispatchQueue.main)
|
||||
}
|
||||
|
||||
let alertController = UIAlertController(title: message, message: information, preferredStyle: .alert)
|
||||
var selection:String? = nil
|
||||
for option in buttons
|
||||
{
|
||||
alertController.addAction(UIAlertAction(title: option as? String, style: .default) { action in
|
||||
selection = action.title
|
||||
self.alertHandlingQueue.resume()
|
||||
})
|
||||
}
|
||||
|
||||
// Transform the asynchronous UIAlertController into a synchronous alert by suspending a GCD serial queue before presenting then placing an empty sync on that queue to block until it is resumed, and resuming after selection. The GCD sync can't complete until the selection resumes the queue.
|
||||
|
||||
alertHandlingQueue.suspend()
|
||||
self.present(alertController, animated: true)
|
||||
alertHandlingQueue.sync { } // To wait for queue to resume.
|
||||
|
||||
return selection
|
||||
}
|
||||
|
||||
func checkForClippingAddedToClipboard()
|
||||
{
|
||||
pasteboardInteractionQueue.async {
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
|
||||
BOOL disableStore;
|
||||
BOOL inhibitSaveEngineAfterListModification;
|
||||
BOOL firstClippingsSyncAfterEnabling;
|
||||
BOOL inhibitAutosaveClippings;
|
||||
}
|
||||
|
||||
|
@ -100,6 +101,9 @@
|
|||
-(void)setClippingsStoreDelegate:(id<FlycutStoreDelegate>) delegate;
|
||||
-(void)setFavoritesStoreDelegate:(id<FlycutStoreDelegate>) delegate;
|
||||
|
||||
/** optional delegate (not retained) */
|
||||
@property (nonatomic, nullable, assign) id<FlycutOperatorDelegate> delegate;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* FlycutOperator_h */
|
||||
|
|
303
FlycutOperator.m
303
FlycutOperator.m
|
@ -526,13 +526,27 @@
|
|||
[MJCloudKitUserDefaultsSync stopForKeyMatchList:settingsSyncList];
|
||||
}
|
||||
|
||||
if ( [[NSUserDefaults standardUserDefaults] boolForKey:@"syncClippingsViaICloud"] ) {
|
||||
BOOL syncClippings = [[NSUserDefaults standardUserDefaults] boolForKey:@"syncClippingsViaICloud"];
|
||||
BOOL changedSyncClippings = ( ![[NSUserDefaults standardUserDefaults] objectForKey:@"previousSyncClippingsViaICloud"]
|
||||
|| syncClippings != [[NSUserDefaults standardUserDefaults] boolForKey:@"previousSyncClippingsViaICloud"] );
|
||||
|
||||
[[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 removeNotificationsFor:MJSyncNotificationChanges forTarget:self];
|
||||
[MJCloudKitUserDefaultsSync addNotificationFor:MJSyncNotificationChanges withSelector:@selector(checkPreferencesChanges:) withTarget: self];
|
||||
|
||||
[MJCloudKitUserDefaultsSync removeNotificationsFor:MJSyncNotificationConflicts forTarget:self];
|
||||
[MJCloudKitUserDefaultsSync addNotificationFor:MJSyncNotificationConflicts withSelector:@selector(checkPreferencesConflicts:) withTarget: self];
|
||||
|
||||
[MJCloudKitUserDefaultsSync removeNotificationsFor:MJSyncNotificationSaveSuccess forTarget:self];
|
||||
[MJCloudKitUserDefaultsSync addNotificationFor:MJSyncNotificationSaveSuccess withSelector:@selector(checkPreferencesSaveSuccess:) withTarget: self];
|
||||
|
||||
[MJCloudKitUserDefaultsSync startWithKeyMatchList:@[@"store"]
|
||||
withContainerIdentifier:@"iCloud.com.mark-a-jerde.Flycut"];
|
||||
}
|
||||
|
@ -547,53 +561,222 @@
|
|||
{
|
||||
if ( [changes valueForKey:@"store"] )
|
||||
{
|
||||
inhibitSaveEngineAfterListModification = YES;
|
||||
|
||||
[self integrateStoreAtKey:@"jcList" into:clippingStore];
|
||||
[self integrateStoreAtKey:@"favoritesList" into:favoritesStore];
|
||||
|
||||
inhibitSaveEngineAfterListModification = NO;
|
||||
[self actionAfterListModification];
|
||||
[self integrateAllStores];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
-(void) integrateStoreAtKey:(NSString*)listKey into:(FlycutStore*)store
|
||||
-(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.
|
||||
[newClipping setDisplayLength:displayLength];
|
||||
[store insertClipping:newClipping atIndex:i];
|
||||
[self integrateInsertClipping:newClipping toStore:store atIndex:(i+offsetForMerge) withMerge:mergeLists];
|
||||
}
|
||||
else if ( ![newClipping isEqual:[store clippingAtPosition:i]] )
|
||||
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 )
|
||||
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.
|
||||
[newClipping setDisplayLength:displayLength];
|
||||
[store insertClipping:newClipping atIndex:i];
|
||||
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 ( [newContent indexOfClipping:[store clippingAtPosition:i]] < 0 )
|
||||
else if ( contentAtThisStorePositionNotInNewContent )
|
||||
{
|
||||
// Contents in the store at this position didn't exist in the newContent. Handle deletion.
|
||||
[store clearItem:i];
|
||||
i--;
|
||||
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];
|
||||
[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.
|
||||
|
@ -604,27 +787,29 @@
|
|||
}
|
||||
else
|
||||
{
|
||||
[newClipping setDisplayLength:displayLength];
|
||||
[store insertClipping:newClipping atIndex:i];
|
||||
[self integrateInsertClipping:newClipping toStore:store atIndex:(i+offsetForMerge) withMerge:mergeLists];
|
||||
}
|
||||
}
|
||||
}
|
||||
while ( [store jcListCount] > newCount )
|
||||
[store clearItem:newCount];
|
||||
while ( [store jcListCount] > newCount + offsetForMerge )
|
||||
[store clearItem:(newCount + offsetForMerge)];
|
||||
|
||||
#ifdef DEBUG
|
||||
[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
|
||||
if ( !mergeLists )
|
||||
{
|
||||
for ( int i = 0 ; i < newCount ; i++ )
|
||||
[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
|
||||
{
|
||||
if ( ![[store clippingAtPosition:i] isEqual:[newContent clippingAtPosition:i]] )
|
||||
NSLog(@"Error in integrateStoreAtKey with mismatching clippings at index %i!", i);
|
||||
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
|
||||
|
@ -633,20 +818,59 @@
|
|||
[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"] )
|
||||
{
|
||||
// Just clobber back, for now. They already finished their save, so they won't notice and will just think we made a calculated change.
|
||||
NSLog(@"Oh. My changes were clobbered. I will clobber back.");
|
||||
// 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"] = [changes valueForKey:@"store"][1]; // We win.
|
||||
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 checkCloudKitUpdates];
|
||||
|
@ -797,4 +1021,11 @@
|
|||
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
|
||||
|
|
Loading…
Add table
Reference in a new issue