diff --git a/aqt/main.py b/aqt/main.py
index 8b9745cf9..e70b3d377 100755
--- a/aqt/main.py
+++ b/aqt/main.py
@@ -55,7 +55,8 @@ class AnkiQt(QMainWindow):
def setupUI(self):
self.col = None
- self.state = None
+ self.state = "overview"
+ self.setupKeys()
self.setupThreads()
self.setupMainWindow()
self.setupStyle()
@@ -188,14 +189,10 @@ Are you sure?"""):
self.show()
self.activateWindow()
self.raise_()
- # maybe sync
- self.onSync()
- # then load collection and launch into the deck browser
- print "fixme: safeguard against multiple instances"
- self.col = Collection(self.pm.collectionPath())
- self.progress.setupDB(self.col.db)
+ # maybe sync (will load DB)
+ self.onSync(auto=True)
# skip the reset step; open overview directly
- self.moveToState("review")
+ self.moveToState("overview")
def unloadProfile(self):
self.col = None
@@ -245,10 +242,11 @@ Are you sure?"""):
# Resetting state
##########################################################################
- def reset(self, type="all", *args):
+ def reset(self, guiOnly=False):
"Called for non-trivial edits. Rebuilds queue and updates UI."
if self.col:
- self.col.reset()
+ if not guiOnly:
+ self.col.reset()
runHook("reset")
self.moveToState(self.state)
@@ -453,13 +451,17 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors")
# Syncing
##########################################################################
- def onSync(self):
- from aqt.sync import Syncer
+ def onSync(self, auto=False):
+ from aqt.sync import SyncManager
# close collection if loaded
if self.col:
self.col.close()
- #
- Syncer()
+ self.syncer = SyncManager(self, self.pm)
+ self.syncer.sync(auto)
+ # then load collection and launch into the deck browser
+ self.col = Collection(self.pm.collectionPath())
+ self.progress.setupDB(self.col.db)
+ self.reset(guiOnly=True)
# Tools
##########################################################################
diff --git a/aqt/sync.py b/aqt/sync.py
index b6af7f07f..087353693 100755
--- a/aqt/sync.py
+++ b/aqt/sync.py
@@ -2,224 +2,147 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from aqt.qt import *
-import os, types, socket, time, traceback
+import os, types, socket, time, traceback, gc
import aqt
-import anki
-#from anki.sync import SyncClient, HttpSyncServerProxy, copyLocalMedia from
-#anki.sync import SYNC_HOST, SYNC_PORT
-from anki.errors import *
-from anki.db import sqlite
-import aqt.forms
+from anki import Collection
+from anki.sync import Syncer, RemoteServer, FullSyncer, MediaSyncer, \
+ RemoteMediaServer
from anki.hooks import addHook, removeHook
+from aqt.utils import tooltip, askUserDialog
-class Syncer(object):
+# Sync manager
+######################################################################
- # Syncing
- ##########################################################################
+# are we doing this in main?
+# self.closeAllDeckWindows()
- def syncDeck(self, interactive=True, onlyMerge=False, reload=True):
- "Synchronise a deck with the server."
- self.raiseMain()
- #self.setNotice()
- # vet input
- if interactive:
- self.ensureSyncParams()
- u=self.pm.profile['syncUsername']
- p=self.pm.profile['syncPassword']
+class SyncManager(QObject):
+
+ def __init__(self, mw, pm):
+ QObject.__init__(self, mw)
+ self.mw = mw
+ self.pm = pm
+
+ def sync(self, auto=False):
+ if not self.pm.profile['syncKey']:
+ if auto:
+ return
+ auth = self._getUserPass()
+ if not auth:
+ return
+ self._sync(auth)
+ else:
+ self._sync()
+
+ def _sync(self, auth=None):
+ # to avoid gui widgets being garbage collected in the worker thread,
+ # run gc in advance
+ gc.collect()
+ # create the thread, setup signals and start running
+ t = self.thread = SyncThread(
+ self.pm.collectionPath(), self.pm.profile['syncKey'], auth)
+ print "created thread"
+ self.connect(t, SIGNAL("event"), self.onEvent)
+ self.mw.progress.start(immediate=True, label=_("Connecting..."))
+ print "starting thread"
+ self.thread.start()
+ while not self.thread.isFinished():
+ self.mw.app.processEvents()
+ self.thread.wait(100)
+ print "finished"
+ self.mw.progress.finish()
+
+ def onEvent(self, evt, *args):
+ if evt == "badAuth":
+ return tooltip(
+ _("AnkiWeb ID or password was incorrect; please try again."),
+ parent=self.mw)
+ elif evt == "newKey":
+ self.pm.profile['syncKey'] = args[0]
+ self.pm.save()
+ print "saved hkey"
+ elif evt == "sync":
+ self.mw.progress.update(label="sync: "+args[0])
+ elif evt == "mediaSync":
+ self.mw.progress.update(label="media: "+args[0])
+ elif evt == "error":
+ print "error occurred", args[0]
+ elif evt == "clockOff":
+ print "clock is wrong"
+ elif evt == "noChanges":
+ print "no changes found"
+ elif evt == "fullSync":
+ self._confirmFullSync()
+ elif evt == "success":
+ print "sync successful"
+ elif evt == "upload":
+ print "upload successful"
+ elif evt == "download":
+ print "download successful"
+ elif evt == "noMediaChanges":
+ print "no media changes"
+ elif evt == "mediaSuccess":
+ print "media sync successful"
+ else:
+ print "unknown evt", evt
+
+ def _getUserPass(self):
+ d = QDialog(self.mw)
+ d.setWindowTitle("Anki")
+ vbox = QVBoxLayout()
+ l = QLabel(_("""\
+
Account Required
+A free account is required to keep your collection synchronized. Please \
+sign up for an account, then \
+enter your details below."""))
+ l.setOpenExternalLinks(True)
+ l.setWordWrap(True)
+ vbox.addWidget(l)
+ vbox.addSpacing(20)
+ g = QGridLayout()
+ l1 = QLabel(_("AnkiWeb ID:"))
+ g.addWidget(l1, 0, 0)
+ user = QLineEdit()
+ g.addWidget(user, 0, 1)
+ l2 = QLabel(_("Password:"))
+ g.addWidget(l2, 1, 0)
+ passwd = QLineEdit()
+ passwd.setEchoMode(QLineEdit.Password)
+ g.addWidget(passwd, 1, 1)
+ vbox.addLayout(g)
+ bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
+ bb.button(QDialogButtonBox.Ok).setAutoDefault(True)
+ self.connect(bb, SIGNAL("accepted()"), d.accept)
+ self.connect(bb, SIGNAL("rejected()"), d.reject)
+ vbox.addWidget(bb)
+ d.setLayout(vbox)
+ d.show()
+ d.exec_()
+ u = user.text()
+ p = passwd.text()
if not u or not p:
return
- if self.deck:
- if not self.deck.path:
- if not self.save(required=True):
- return
- if self.deck and not self.deck.syncName:
- if interactive:
- if (not self.pm.profile['mediaLocation']
- and self.deck.db.scalar("select 1 from media limit 1")):
- showInfo(_("""\
-Syncing sounds and images requires a free file synchronization service like \
-DropBox. Click help to learn more, and OK to continue syncing."""),
- help="SyncingMedia")
- # enable syncing
- self.deck.enableSyncing()
- else:
- return
- if self.deck is None and getattr(self, 'deckPath', None) is None:
- # sync all decks
- self.loadAfterSync = -1
- self.syncName = None
- self.syncDecks = self.decksToSync()
- if not self.syncDecks:
- if interactive:
- showInfo(_("""\
-Please open a deck and run File>Sync. After you do this once, the deck \
-will sync automatically from then on."""))
- return
- else:
- # sync one deck
- # hide all deck-associated dialogs
- self.closeAllDeckWindows()
- if self.deck:
- # save first, so we can rollback on failure
- self.deck.save()
- # store data we need before closing the deck
- self.deckPath = self.deck.path
- self.syncName = self.deck.name()
- self.lastSync = self.deck.lastSync
- self.deck.close()
- self.deck = None
- self.loadAfterSync = reload
- # bug triggered by preferences dialog - underlying c++ widgets are not
- # garbage collected until the middle of the child thread
- self.state = "nostate"
- import gc; gc.collect()
- self.form.welcomeText.setText(u"")
- self.syncThread = aqt.sync.Sync(self, u, p, interactive, onlyMerge)
- self.connect(self.syncThread, SIGNAL("setStatus"), self.setSyncStatus)
- self.connect(self.syncThread, SIGNAL("showWarning"), self.showSyncWarning)
- self.connect(self.syncThread, SIGNAL("moveToState"), self.moveToState)
- self.connect(self.syncThread, SIGNAL("noMatchingDeck"), self.selectSyncDeck)
- self.connect(self.syncThread, SIGNAL("syncClockOff"), self.syncClockOff)
- self.connect(self.syncThread, SIGNAL("cleanNewDeck"), self.cleanNewDeck)
- self.connect(self.syncThread, SIGNAL("syncFinished"), self.onSyncFinished)
- self.connect(self.syncThread, SIGNAL("openSyncProgress"), self.openSyncProgress)
- self.connect(self.syncThread, SIGNAL("closeSyncProgress"), self.closeSyncProgress)
- self.connect(self.syncThread, SIGNAL("updateSyncProgress"), self.updateSyncProgress)
- self.connect(self.syncThread, SIGNAL("bulkSyncFailed"), self.bulkSyncFailed)
- self.connect(self.syncThread, SIGNAL("fullSyncStarted"), self.fullSyncStarted)
- 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.connect(self.syncThread, SIGNAL("syncClobber"), self.onClobber)
- self.syncThread.start()
- self.switchToWelcomeScreen()
- self.setEnabled(False)
- self.syncFinished = False
- while not self.syncFinished:
- self.app.processEvents()
- self.syncThread.wait(100)
- self.setEnabled(True)
- return True
+ return (u, p)
- def decksToSync(self):
- ok = []
- for d in self.pm.profile['recentDeckPaths']:
- if os.path.exists(d):
- ok.append(d)
- return ok
-
- def onConflict(self, deckName):
+ def _confirmFullSync(self):
diag = askUserDialog(_("""\
-%s has been changed on both
-the local and remote side. What do
-you want to do?""" % deckName),
- [_("Keep Local"),
- _("Keep Remote"),
- _("Cancel")])
+Because this is your first time synchronizing, or because unmergable \
+changes have been made, your collection needs to be either uploaded or \
+downloaded in full.
+
+Do you want to keep the local version, overwriting the AnkiWeb version? Or \
+do you want to keep the AnkiWeb version, overwriting the version here?"""),
+ [_("Keep Local"),
+ _("Keep AnkiWeb"),
+ _("Cancel")])
diag.setDefault(2)
ret = diag.run()
if ret == _("Keep Local"):
- self.syncThread.conflictResolution = "keepLocal"
- elif ret == _("Keep Remote"):
- self.syncThread.conflictResolution = "keepRemote"
+ self.thread.fullSyncChoice = "upload"
+ elif ret == _("Keep AnkiWeb"):
+ self.thread.fullSyncChoice = "download"
else:
- self.syncThread.conflictResolution = "cancel"
-
- def onClobber(self, deckName):
- diag = askUserDialog(_("""\
-You are about to upload %s
-to AnkiOnline. This will overwrite
-the online copy of this deck.
-Are you sure?""" % deckName),
- [_("Upload"),
- _("Cancel")])
- diag.setDefault(1)
- ret = diag.run()
- if ret == _("Upload"):
- self.syncThread.clobberChoice = "overwrite"
- else:
- self.syncThread.clobberChoice = "cancel"
-
- def onSyncFinished(self):
- "Reopen after sync finished."
- self.form.buttonStack.show()
- try:
- try:
- if not self.showBrowser:
- # no deck load & no deck browser, as we're about to quit or do
- # something manually
- pass
- else:
- if self.loadAfterSync == -1:
- # after sync all, so refresh browser list
- self.browserLastRefreshed = 0
- self.moveToState("deckBrowser")
- elif self.loadAfterSync and self.deckPath:
- if self.loadAfterSync == 2:
- name = re.sub("[<>]", "", self.syncName)
- p = os.path.join(self.pm.profile['documentDir'], name + ".anki")
- shutil.copy2(self.deckPath, p)
- self.deckPath = p
- # since we've moved the deck, we have to set sync path
- # ourselves
- c = sqlite.connect(p)
- v = c.execute(
- "select version from decks").fetchone()[0]
- if v >= 52:
- # deck has bene upgraded already, so we can
- # use a checksum
- name = checksum(p.encode("utf-8"))
- else:
- # FIXME: compat code because deck hasn't been
- # upgraded yet. can be deleted in the future.
- # strip off .anki part
- name = os.path.splitext(
- os.path.basename(p))[0]
- c.execute("update decks set syncName = ?", (name,))
- c.commit()
- c.close()
- self.loadDeck(self.deckPath)
- else:
- self.moveToState("deckBrowser")
- except:
- self.moveToState("deckBrowser")
- raise
- finally:
- self.deckPath = None
- self.syncFinished = True
-
- def selectSyncDeck(self, decks):
- name = aqt.sync.DeckChooser(self, decks).getName()
- self.syncName = name
- if name:
- # name chosen
- p = os.path.join(self.pm.profile['documentDir'], name + ".anki")
- if os.path.exists(p):
- d = askUserDialog(_("""\
-This deck already exists on your computer. Overwrite the local copy?"""),
- ["Overwrite", "Cancel"])
- d.setDefault(1)
- if d.run() == "Overwrite":
- self.syncDeck(interactive=False, onlyMerge=True)
- else:
- self.syncFinished = True
- self.cleanNewDeck()
- else:
- self.syncDeck(interactive=False, onlyMerge=True)
- return
- self.syncFinished = True
- self.cleanNewDeck()
-
- def cleanNewDeck(self):
- "Unload a new deck if an initial sync failed."
- self.deck = None
- self.deckPath = None
- self.moveToState("deckBrowser")
- self.syncFinished = True
-
- def setSyncStatus(self, text, *args):
- self.form.welcomeText.append("" + text + "")
+ self.thread.fullSyncChoice = "cancel"
def syncClockOff(self, diff):
showWarning(
@@ -231,391 +154,100 @@ This deck already exists on your computer. Overwrite the local copy?"""),
)
self.onSyncFinished()
- def showSyncWarning(self, text):
- showWarning(text, self)
- self.setStatus("")
-
def badUserPass(self):
aqt.preferences.Preferences(self, self.pm.profile).dialog.tabWidget.\
setCurrentIndex(1)
- def openSyncProgress(self):
- self.syncProgressDialog = QProgressDialog(_("Syncing Media..."),
- "", 0, 0, self)
- self.syncProgressDialog.setWindowTitle(_("Syncing Media..."))
- self.syncProgressDialog.setCancelButton(None)
- self.syncProgressDialog.setAutoClose(False)
- self.syncProgressDialog.setAutoReset(False)
+# Sync thread
+######################################################################
- def closeSyncProgress(self):
- self.syncProgressDialog.cancel()
+class SyncThread(QThread):
- def updateSyncProgress(self, args):
- (type, x, y, fname) = args
- self.syncProgressDialog.setMaximum(y)
- self.syncProgressDialog.setValue(x)
- self.syncProgressDialog.setMinimumDuration(0)
- if type == "up":
- self.syncProgressDialog.setLabelText("Uploading %s..." % fname)
- else:
- self.syncProgressDialog.setLabelText("Downloading %s..." % fname)
-
- def bulkSyncFailed(self):
- showWarning(_(
- "Failed to upload media. Please run 'check media db'."), self)
-
- def fullSyncStarted(self, max):
- self.startProgress(max=max)
-
- def fullSyncFinished(self):
- self.finishProgress()
- # need to deactivate interface again
- self.setEnabled(False)
-
- def fullSyncProgress(self, type, val):
- if type == "fromLocal":
- s = _("Uploaded %dKB to server...")
- self.updateProgress(label=s % (val / 1024), value=val)
- else:
- s = _("Downloaded %dKB from server...")
- self.updateProgress(label=s % (val / 1024))
-
- def ensureSyncParams(self):
- if not self.pm.profile['syncUsername'] or not self.pm.profile['syncPassword']:
- d = QDialog(self)
- vbox = QVBoxLayout()
- l = QLabel(_(
- 'Online Account
'
- 'To use your free online account,
'
- "please enter your details below.
"
- "You can change your details later with
"
- "Settings->Preferences->Sync
"))
- l.setOpenExternalLinks(True)
- vbox.addWidget(l)
- g = QGridLayout()
- l1 = QLabel(_("Username:"))
- g.addWidget(l1, 0, 0)
- user = QLineEdit()
- g.addWidget(user, 0, 1)
- l2 = QLabel(_("Password:"))
- g.addWidget(l2, 1, 0)
- passwd = QLineEdit()
- passwd.setEchoMode(QLineEdit.Password)
- g.addWidget(passwd, 1, 1)
- vbox.addLayout(g)
- bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
- self.connect(bb, SIGNAL("accepted()"), d.accept)
- self.connect(bb, SIGNAL("rejected()"), d.reject)
- vbox.addWidget(bb)
- d.setLayout(vbox)
- d.exec_()
- self.pm.profile['syncUsername'] = unicode(user.text())
- self.pm.profile['syncPassword'] = unicode(passwd.text())
-
-
-# Synchronising a deck with a public server
-##########################################################################
-
-class Sync(QThread):
-
- def __init__(self, parent, user, pwd, interactive, onlyMerge):
+ def __init__(self, path, hkey, auth=None):
QThread.__init__(self)
- self.parent = parent
- self.interactive = interactive
- self.user = user
- self.pwd = pwd
- self.ok = True
- self.onlyMerge = onlyMerge
- self.proxy = None
- addHook('fullSyncStarted', self.fullSyncStarted)
- addHook('fullSyncFinished', self.fullSyncFinished)
- addHook('fullSyncProgress', self.fullSyncProgress)
-
- def setStatus(self, msg, timeout=5000):
- self.emit(SIGNAL("setStatus"), msg, timeout)
+ self.path = path
+ self.hkey = hkey
+ self.auth = auth
def run(self):
- if self.parent.syncName:
- self.syncDeck()
- else:
- self.syncAllDecks()
- removeHook('fullSyncStarted', self.fullSyncStarted)
- removeHook('fullSyncFinished', self.fullSyncFinished)
- removeHook('fullSyncProgress', self.fullSyncProgress)
-
- def fullSyncStarted(self, max):
- self.emit(SIGNAL("fullSyncStarted"), max)
-
- def fullSyncFinished(self):
- self.emit(SIGNAL("fullSyncFinished"))
-
- def fullSyncProgress(self, type, val):
- self.emit(SIGNAL("fullSyncProgress"), type, val)
-
- def error(self, error):
- if getattr(error, 'data', None) is None:
- error.data = {}
- if error.data.get('type') == 'clockOff':
- pass
- else:
- error = self.getErrorMessage(error)
- self.emit(SIGNAL("showWarning"), error)
- if self.onlyMerge:
- # new file needs cleaning up
- self.emit(SIGNAL("cleanNewDeck"))
- else:
- self.emit(SIGNAL("syncFinished"))
-
- def getErrorMessage(self, error):
- if error.data.get('status') == "invalidUserPass":
- msg=_("Please double-check your username/password.")
- self.emit(SIGNAL("badUserPass"))
- elif error.data.get('status') == "oldVersion":
- msg=_("The sync protocol has changed. Please upgrade.")
- elif "busy" in error.data.get('status', ''):
- msg=_("""\
-AnkiWeb is under heavy load at the moment. Please try again in a little while.""")
- elif error.data.get('type') == 'noResponse':
- msg=_("""\
-The server didn't reply. Please try again shortly, and if the problem \
-persists, please report it on the forums.""")
- elif error.data.get('type') == 'connectionError':
- msg=_("""\
-There was a connection error. If it persists, please try disabling your
-firewall software temporarily, or try again from a different network.
-
-Debugging info: %s""") % error.data.get("exc", "")
- else:
- tb = traceback.format_exc()
- if "missingNotes" in tb:
- msg=_("""Notes were missing after sync, so the \
-sync was aborted. Please report this error.""")
- else:
- msg=_("Unknown error: %s") % tb
- return msg
-
- def connect(self, *args):
- # connect, check auth
- if not self.proxy:
- self.setStatus(_("Connecting..."), 0)
- proxy = HttpSyncServerProxy(self.user, self.pwd)
- proxy.connect("aqt-" + ankiqt.appVersion)
- self.proxy = proxy
- # check clock
- if proxy.timediff > 300:
- self.emit(SIGNAL("syncClockOff"), proxy.timediff)
- raise SyncError(type="clockOff")
- return self.proxy
-
- def syncAllDecks(self):
- decks = self.parent.syncDecks
- for d in decks:
- ret = self.syncDeck(deck=d)
- if not ret:
- # failed but not cleaned up
- break
- elif ret == -1:
- # failed and already cleaned up
- return
- elif ret == -2:
- # current deck set not to sync
- continue
- self.setStatus(_("Sync Finished."), 0)
- time.sleep(1)
- self.emit(SIGNAL("syncFinished"))
-
- def syncDeck(self, deck=None):
+ self.col = Collection(self.path)
+ self.server = RemoteServer(self.hkey)
+ self.client = Syncer(self.col, self.server)
+ def syncEvent(type):
+ self.fireEvent("sync", type)
+ def mediaSync(type):
+ self.fireEvent("mediaSync", type)
+ addHook("sync", syncEvent)
+ addHook("mediaSync", mediaSync)
+ # run sync and catch any errors
try:
- if deck:
- # multi-mode setup
- sqlpath = deck.encode("utf-8")
- c = sqlite.connect(sqlpath)
- (syncName, localMod, localSync) = c.execute(
- "select syncName, modified, lastSync from decks").fetchone()
- c.close()
- if not syncName:
- return -2
- syncName = os.path.splitext(os.path.basename(deck))[0]
- path = deck
- else:
- syncName = self.parent.syncName
- path = self.parent.deckPath
- sqlpath = path.encode("utf-8")
- c = sqlite.connect(sqlpath)
- (localMod, localSync) = c.execute(
- "select modified, lastSync from decks").fetchone()
- c.close()
+ self._sync()
except Exception, e:
- # we don't know which db library we're using, so do string match
- if "locked" in unicode(e):
- return
- # unknown error
- self.error(e)
- return -1
- # ensure deck mods cached
- try:
- proxy = self.connect()
- except SyncError, e:
- self.error(e)
- return -1
- # exists on server?
- deckCreated = False
- if not proxy.hasDeck(syncName):
- if self.onlyMerge:
- keys = [k for (k,v) in proxy.decks.items() if v[1] != -1]
- self.emit(SIGNAL("noMatchingDeck"), keys)
- self.setStatus("")
- return
- try:
- proxy.createDeck(syncName)
- deckCreated = True
- except SyncError, e:
- self.error(e)
- return -1
- # check conflicts
- proxy.deckName = syncName
- remoteMod = proxy.modified()
- remoteSync = proxy._lastSync()
- minSync = min(localSync, remoteSync)
- self.conflictResolution = None
- if (localMod != remoteMod and minSync > 0 and
- localMod > minSync and remoteMod > minSync):
- self.emit(SIGNAL("syncConflicts"), syncName)
- while not self.conflictResolution:
- time.sleep(0.2)
- if self.conflictResolution == "cancel":
- # alert we're finished early
- self.emit(SIGNAL("syncFinished"))
- return -1
- # reopen
- self.setStatus(_("Syncing %s...") % syncName, 0)
- self.deck = None
- try:
- self.deck = DeckStorage.Deck(path)
- disable = False
- if deck and not self.deck.syncName:
- # multi-mode sync and syncing has been disabled by upgrade
- disable = True
- client = SyncClient(self.deck)
- client.setServer(proxy)
- # need to do anything?
- start = time.time()
- if client.prepareSync(proxy.timediff) and not disable:
- if self.deck.lastSync <= 0:
- if client.remoteTime > client.localTime:
- self.conflictResolution = "keepRemote"
- else:
- self.conflictResolution = "keepLocal"
- changes = True
- # summary
- if not self.conflictResolution and not self.onlyMerge:
- self.setStatus(_("Fetching summary from server..."), 0)
- sums = client.summaries()
- if (self.conflictResolution or
- self.onlyMerge or client.needFullSync(sums)):
- self.setStatus(_("Preparing full sync..."), 0)
- if self.conflictResolution == "keepLocal":
- client.remoteTime = 0
- elif self.conflictResolution == "keepRemote" or self.onlyMerge:
- client.localTime = 0
- lastSync = self.deck.lastSync
- ret = client.prepareFullSync()
- if ret[0] == "fromLocal":
- if not self.conflictResolution:
- if lastSync <= 0 and not deckCreated:
- self.clobberChoice = None
- self.emit(SIGNAL("syncClobber"), syncName)
- while not self.clobberChoice:
- time.sleep(0.2)
- if self.clobberChoice == "cancel":
- # disable syncing on this deck
- c = sqlite.connect(sqlpath)
- c.execute(
- "update decks set syncName = null, "
- "lastSync = 0")
- c.commit()
- c.close()
- if not deck:
- # alert we're finished early
- self.emit(SIGNAL("syncFinished"))
- return True
- self.setStatus(_("Uploading..."), 0)
- client.fullSyncFromLocal(ret[1], ret[2])
- else:
- self.setStatus(_("Downloading..."), 0)
- client.fullSyncFromServer(ret[1], ret[2])
- self.setStatus(_("Sync complete."), 0)
- else:
- # diff
- self.setStatus(_("Determining differences..."), 0)
- payload = client.genPayload(sums)
- # send payload
- if not deck:
- pr = client.payloadChangeReport(payload)
- self.setStatus("
" + pr + "
", 0)
- self.setStatus(_("Transferring payload..."), 0)
- res = client.server.applyPayload(payload)
- # apply reply
- self.setStatus(_("Applying reply..."), 0)
- client.applyPayloadReply(res)
- # now that both sides have successfully applied, tell
- # server to save, then save local
- client.server.finish()
- self.deck.lastLoaded = self.deck.modified
- self.deck.db.commit()
- self.setStatus(_("Sync complete."))
+ print e
+ self.fireEvent("error", unicode(e))
+ finally:
+ # don't bump mod time unless we explicitly save
+ self.col.close(save=False)
+
+ def _sync(self):
+ if self.auth:
+ # need to authenticate and obtain host key
+ hkey = self.server.hostKey(*self.auth)
+ print "hkey was", hkey
+ if not hkey:
+ # provided details were invalid
+ return self.fireEvent("badAuth")
else:
- changes = False
- if disable:
- self.setStatus(_("Disabled by upgrade."))
- elif not deck:
- self.setStatus(_("No changes found."))
- # close and send signal to main thread
- self.deck.close()
- if not deck:
- taken = time.time() - start
- if changes and taken < 2.5:
- time.sleep(2.5 - taken)
- else:
- time.sleep(0.25)
- self.emit(SIGNAL("syncFinished"))
- return True
- except Exception, e:
- self.ok = False
- if self.deck:
- self.deck.close()
- self.error(e)
- return -1
+ # write new details and tell calling thread to save
+ self.fireEvent("newKey", hkey)
+ # run sync and check state
+ ret = self.client.sync()
+ if ret == "badAuth":
+ return self.fireEvent("badAuth")
+ elif ret == "clockOff":
+ return self.fireEvent("clockOff")
+ # note mediaUSN for later
+ self.mediaUsn = self.client.mediaUsn
+ # full sync?
+ if ret == "fullSync":
+ return self._fullSync()
+ # save and note success state
+ self.col.save()
+ if ret == "noChanges":
+ self.fireEvent("noChanges")
+ else:
+ self.fireEvent("success")
+ # then move on to media sync
+ self._syncMedia()
-# Downloading personal decks
-##########################################################################
+ def _fullSync(self):
+ # tell the calling thread we need a decision on sync direction, and
+ # wait for a reply
+ self.fullSyncChoice = False
+ self.fireEvent("fullSync")
+ while not self.fullSyncChoice:
+ time.sleep(0.1)
+ f = self.fullSyncChoice
+ if f == "cancel":
+ return
+ self.client = FullSyncer(self.col, self.hkey, self.server.con)
+ if f == "upload":
+ self.client.upload()
+ self.fireEvent("upload")
+ else:
+ self.client.download()
+ self.fireEvent("download")
+ # move on to media sync
+ self._syncMedia()
-class DeckChooser(QDialog):
+ def _syncMedia(self):
+ self.server = RemoteMediaServer(self.hkey, self.server.con)
+ self.client = MediaSyncer(self.col, self.server)
+ ret = self.client.sync(self.mediaUsn)
+ if ret == "noChanges":
+ self.fireEvent("noMediaChanges")
+ else:
+ self.fireEvent("mediaSuccess")
- def __init__(self, parent, decks):
- QDialog.__init__(self, parent, Qt.Window)
- self.parent = parent
- self.decks = decks
- self.dialog = aqt.forms.syncdeck.Ui_DeckChooser()
- self.dialog.setupUi(self)
- self.dialog.topLabel.setText(_("Download Personal Deck
"))
- self.decks.sort()
- for name in decks:
- name = os.path.splitext(name)[0]
- msg = name
- item = QListWidgetItem(msg)
- self.dialog.decks.addItem(item)
- self.dialog.decks.setCurrentRow(0)
- # the list widget will swallow the enter key
- s = QShortcut(QKeySequence("Return"), self)
- self.connect(s, SIGNAL("activated()"), self.accept)
- self.name = None
-
- def getName(self):
- self.exec_()
- return self.name
-
- def accept(self):
- idx = self.dialog.decks.currentRow()
- self.name = self.decks[self.dialog.decks.currentRow()]
- self.close()
+ def fireEvent(self, *args):
+ self.emit(SIGNAL("event"), *args)
diff --git a/aqt/update.py b/aqt/update.py
index a0ad9f7d7..7069b56da 100644
--- a/aqt/update.py
+++ b/aqt/update.py
@@ -49,8 +49,7 @@ class LatestVersionFinder(QThread):
if resp['latestVersion'] > aqt.appVersion:
self.emit(SIGNAL("newVerAvail"), resp)
diff = resp['currentTime'] - time.time()
- # a fairly liberal time check - sync is more strict
- if abs(diff) > 86400:
+ if abs(diff) > 300:
self.emit(SIGNAL("clockIsOff"), diff)
def askAndUpdate(parent, version=None):
diff --git a/aqt/utils.py b/aqt/utils.py
index a14b27cf5..582838742 100644
--- a/aqt/utils.py
+++ b/aqt/utils.py
@@ -354,14 +354,14 @@ def maybeHideClose(bbox):
_tooltipTimer = None
_tooltipLabel = None
-def tooltip(msg, period=3000):
+def tooltip(msg, period=3000, parent=None):
global _tooltipTimer, _tooltipLabel
class CustomLabel(QLabel):
def mousePressEvent(self, evt):
evt.accept()
self.hide()
closeTooltip()
- aw = aqt.mw.app.activeWindow()
+ aw = parent or aqt.mw.app.activeWindow()
lab = CustomLabel("""\