From e3f50540d5b2c6eb6a3b05a0166e54d617a257ee Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 21 Jul 2010 18:35:55 +0900 Subject: [PATCH] detect sync conflicts and offer choice of direction --- ankiqt/ui/main.py | 17 +++++++++++++++++ ankiqt/ui/sync.py | 28 +++++++++++++++++++++++++--- ankiqt/ui/utils.py | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/ankiqt/ui/main.py b/ankiqt/ui/main.py index 1e78ba68c..dcaa38552 100755 --- a/ankiqt/ui/main.py +++ b/ankiqt/ui/main.py @@ -2168,6 +2168,7 @@ it to your friends. self.connect(self.syncThread, SIGNAL("fullSyncFinished"), self.fullSyncFinished) self.connect(self.syncThread, SIGNAL("fullSyncProgress"), self.fullSyncProgress) self.connect(self.syncThread, SIGNAL("badUserPass"), self.badUserPass) + self.connect(self.syncThread, SIGNAL("syncConflicts"), self.onConflict) self.syncThread.start() self.switchToWelcomeScreen() self.setEnabled(False) @@ -2185,6 +2186,22 @@ it to your friends. ok.append(d) return ok + def onConflict(self, deckName): + diag = ui.utils.askUserDialog(_("""\ +%s has been changed on both the local
+and remote side. What do you want to do?""" % deckName), + [_("Keep Local"), + _("Keep Remote"), + _("Cancel")]) + diag.setDefault(2) + ret = diag.run() + if ret == _("Keep Local"): + self.syncThread.conflictResolution = "keepLocal" + elif ret == _("Keep Remote"): + self.syncThread.conflictResolution = "keepRemote" + else: + self.syncThread.conflictResolution = "cancel" + def onSyncFinished(self): "Reopen after sync finished." self.mainWin.buttonStack.show() diff --git a/ankiqt/ui/sync.py b/ankiqt/ui/sync.py index 1711d63e2..a7c88a63a 100755 --- a/ankiqt/ui/sync.py +++ b/ankiqt/ui/sync.py @@ -100,7 +100,8 @@ class Sync(QThread): # multi-mode setup if deck: c = sqlite.connect(deck) - syncName = c.execute("select syncName from decks").fetchone()[0] + (syncName, localChanged) = c.execute( + "select syncName, modified > lastSync from decks").fetchone() c.close() if not syncName: return @@ -108,6 +109,10 @@ class Sync(QThread): else: syncName = self.parent.syncName path = self.parent.deckPath + c = sqlite.connect(path) + localChanged = c.execute( + "select modified > lastSync from decks").fetchone()[0] + c.close() # ensure deck mods cached try: proxy = self.connect() @@ -127,12 +132,25 @@ class Sync(QThread): self.emit(SIGNAL("noMatchingDeck"), keys, not self.onlyMerge) self.setStatus("") return - self.setStatus(_("Syncing %s...") % syncName, 0) + # check clock timediff = abs(proxy.timestamp - time.time()) if timediff > 300: self.emit(SIGNAL("syncClockOff"), timediff) return + # check conflicts + remoteChanged = proxy.hasChanged(syncName) + self.conflictResolution = None + if localChanged and remoteChanged: + self.emit(SIGNAL("syncConflicts"), syncName) + while not self.conflictResolution: + time.sleep(0.2) + if self.conflictResolution == "cancel": + if not deck: + # alert we're finished early + self.emit(SIGNAL("syncFinished")) + return # reopen + self.setStatus(_("Syncing %s...") % syncName, 0) self.deck = None try: self.deck = DeckStorage.Deck(path) @@ -146,8 +164,12 @@ class Sync(QThread): # summary self.setStatus(_("Fetching summary from server..."), 0) sums = client.summaries() - if client.needFullSync(sums): + if self.conflictResolution or client.needFullSync(sums): self.setStatus(_("Preparing full sync..."), 0) + if self.conflictResolution == "keepLocal": + client.remoteTime = 0 + elif self.conflictResolution == "keepRemote": + client.localTime = 0 ret = client.prepareFullSync() if ret[0] == "fromLocal": self.setStatus(_("Uploading..."), 0) diff --git a/ankiqt/ui/utils.py b/ankiqt/ui/utils.py index 1650e2522..094ad464e 100644 --- a/ankiqt/ui/utils.py +++ b/ankiqt/ui/utils.py @@ -75,6 +75,44 @@ def askUser(text, parent=None, help=""): break return r == QMessageBox.Yes +class ButtonedDialog(QMessageBox): + + def __init__(self, text, buttons, parent=None, help=""): + QDialog.__init__(self, parent) + self.buttons = [] + self.setWindowTitle("Anki") + self.help = help + self.setIcon(QMessageBox.Warning) + self.setText(text) + # v = QVBoxLayout() + # v.addWidget(QLabel(text)) + # box = QDialogButtonBox() + # v.addWidget(box) + for b in buttons: + self.buttons.append( + self.addButton(b, QMessageBox.AcceptRole)) + if help: + self.addButton(_("Help"), QMessageBox.HelpRole) + buttons.append(_("Help")) + #self.setLayout(v) + + def run(self): + self.exec_() + but = self.clickedButton().text() + if but == "Help": + # FIXME stop dialog closing? + openWikiLink(self.help) + return self.clickedButton().text() + + def setDefault(self, idx): + self.setDefaultButton(self.buttons[idx]) + +def askUserDialog(text, buttons, parent=None, help=""): + if not parent: + parent = ankiqt.mw + diag = ButtonedDialog(text, buttons, parent, help) + return diag + class GetTextDialog(QDialog): def __init__(self, parent, question, help=None, edit=None):