new sync gui

This commit is contained in:
Damien Elmes 2011-12-04 13:54:00 +09:00
parent 94baee058c
commit 7a71a0798c
4 changed files with 229 additions and 596 deletions

View file

@ -55,7 +55,8 @@ class AnkiQt(QMainWindow):
def setupUI(self): def setupUI(self):
self.col = None self.col = None
self.state = None self.state = "overview"
self.setupKeys()
self.setupThreads() self.setupThreads()
self.setupMainWindow() self.setupMainWindow()
self.setupStyle() self.setupStyle()
@ -188,14 +189,10 @@ Are you sure?"""):
self.show() self.show()
self.activateWindow() self.activateWindow()
self.raise_() self.raise_()
# maybe sync # maybe sync (will load DB)
self.onSync() self.onSync(auto=True)
# 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)
# skip the reset step; open overview directly # skip the reset step; open overview directly
self.moveToState("review") self.moveToState("overview")
def unloadProfile(self): def unloadProfile(self):
self.col = None self.col = None
@ -245,9 +242,10 @@ Are you sure?"""):
# Resetting state # Resetting state
########################################################################## ##########################################################################
def reset(self, type="all", *args): def reset(self, guiOnly=False):
"Called for non-trivial edits. Rebuilds queue and updates UI." "Called for non-trivial edits. Rebuilds queue and updates UI."
if self.col: if self.col:
if not guiOnly:
self.col.reset() self.col.reset()
runHook("reset") runHook("reset")
self.moveToState(self.state) self.moveToState(self.state)
@ -453,13 +451,17 @@ Debug info:\n%s""") % traceback.format_exc(), help="DeckErrors")
# Syncing # Syncing
########################################################################## ##########################################################################
def onSync(self): def onSync(self, auto=False):
from aqt.sync import Syncer from aqt.sync import SyncManager
# close collection if loaded # close collection if loaded
if self.col: if self.col:
self.col.close() self.col.close()
# self.syncer = SyncManager(self, self.pm)
Syncer() 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 # Tools
########################################################################## ##########################################################################

View file

@ -2,224 +2,147 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from aqt.qt import * from aqt.qt import *
import os, types, socket, time, traceback import os, types, socket, time, traceback, gc
import aqt import aqt
import anki from anki import Collection
#from anki.sync import SyncClient, HttpSyncServerProxy, copyLocalMedia from from anki.sync import Syncer, RemoteServer, FullSyncer, MediaSyncer, \
#anki.sync import SYNC_HOST, SYNC_PORT RemoteMediaServer
from anki.errors import *
from anki.db import sqlite
import aqt.forms
from anki.hooks import addHook, removeHook 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): class SyncManager(QObject):
"Synchronise a deck with the server."
self.raiseMain() def __init__(self, mw, pm):
#self.setNotice() QObject.__init__(self, mw)
# vet input self.mw = mw
if interactive: self.pm = pm
self.ensureSyncParams()
u=self.pm.profile['syncUsername'] def sync(self, auto=False):
p=self.pm.profile['syncPassword'] 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(_("""\
<h1>Account Required</h1>
A free account is required to keep your collection synchronized. Please \
<a href="http://ankiweb.net/account/login">sign up</a> 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: if not u or not p:
return return
if self.deck: return (u, p)
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
def decksToSync(self): def _confirmFullSync(self):
ok = []
for d in self.pm.profile['recentDeckPaths']:
if os.path.exists(d):
ok.append(d)
return ok
def onConflict(self, deckName):
diag = askUserDialog(_("""\ diag = askUserDialog(_("""\
<b>%s</b> has been changed on both Because this is your first time synchronizing, or because unmergable \
the local and remote side. What do changes have been made, your collection needs to be either uploaded or \
you want to do?""" % deckName), 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 Local"),
_("Keep Remote"), _("Keep AnkiWeb"),
_("Cancel")]) _("Cancel")])
diag.setDefault(2) diag.setDefault(2)
ret = diag.run() ret = diag.run()
if ret == _("Keep Local"): if ret == _("Keep Local"):
self.syncThread.conflictResolution = "keepLocal" self.thread.fullSyncChoice = "upload"
elif ret == _("Keep Remote"): elif ret == _("Keep AnkiWeb"):
self.syncThread.conflictResolution = "keepRemote" self.thread.fullSyncChoice = "download"
else: else:
self.syncThread.conflictResolution = "cancel" self.thread.fullSyncChoice = "cancel"
def onClobber(self, deckName):
diag = askUserDialog(_("""\
You are about to upload <b>%s</b>
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("<font size=+2>" + text + "</font>")
def syncClockOff(self, diff): def syncClockOff(self, diff):
showWarning( showWarning(
@ -231,391 +154,100 @@ This deck already exists on your computer. Overwrite the local copy?"""),
) )
self.onSyncFinished() self.onSyncFinished()
def showSyncWarning(self, text):
showWarning(text, self)
self.setStatus("")
def badUserPass(self): def badUserPass(self):
aqt.preferences.Preferences(self, self.pm.profile).dialog.tabWidget.\ aqt.preferences.Preferences(self, self.pm.profile).dialog.tabWidget.\
setCurrentIndex(1) setCurrentIndex(1)
def openSyncProgress(self): # Sync thread
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)
def closeSyncProgress(self): class SyncThread(QThread):
self.syncProgressDialog.cancel()
def updateSyncProgress(self, args): def __init__(self, path, hkey, auth=None):
(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(_(
'<h1>Online Account</h1>'
'To use your free <a href="http://ankiweb.net/">online account</a>,<br>'
"please enter your details below.<br><br>"
"You can change your details later with<br>"
"Settings->Preferences->Sync<br>"))
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):
QThread.__init__(self) QThread.__init__(self)
self.parent = parent self.path = path
self.interactive = interactive self.hkey = hkey
self.user = user self.auth = auth
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)
def run(self): def run(self):
if self.parent.syncName: self.col = Collection(self.path)
self.syncDeck() self.server = RemoteServer(self.hkey)
else: self.client = Syncer(self.col, self.server)
self.syncAllDecks() def syncEvent(type):
removeHook('fullSyncStarted', self.fullSyncStarted) self.fireEvent("sync", type)
removeHook('fullSyncFinished', self.fullSyncFinished) def mediaSync(type):
removeHook('fullSyncProgress', self.fullSyncProgress) self.fireEvent("mediaSync", type)
addHook("sync", syncEvent)
def fullSyncStarted(self, max): addHook("mediaSync", mediaSync)
self.emit(SIGNAL("fullSyncStarted"), max) # run sync and catch any errors
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", "<none>")
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):
try: try:
if deck: self._sync()
# 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()
except Exception, e: except Exception, e:
# we don't know which db library we're using, so do string match print e
if "locked" in unicode(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:
# 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()
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 return
# unknown error self.client = FullSyncer(self.col, self.hkey, self.server.con)
self.error(e) if f == "upload":
return -1 self.client.upload()
# ensure deck mods cached self.fireEvent("upload")
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 <b>%s</b>...") % 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: else:
self.conflictResolution = "keepLocal" self.client.download()
changes = True self.fireEvent("download")
# summary # move on to media sync
if not self.conflictResolution and not self.onlyMerge: self._syncMedia()
self.setStatus(_("Fetching summary from server..."), 0)
sums = client.summaries() def _syncMedia(self):
if (self.conflictResolution or self.server = RemoteMediaServer(self.hkey, self.server.con)
self.onlyMerge or client.needFullSync(sums)): self.client = MediaSyncer(self.col, self.server)
self.setStatus(_("Preparing full sync..."), 0) ret = self.client.sync(self.mediaUsn)
if self.conflictResolution == "keepLocal": if ret == "noChanges":
client.remoteTime = 0 self.fireEvent("noMediaChanges")
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: else:
self.setStatus(_("Downloading..."), 0) self.fireEvent("mediaSuccess")
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("<br>" + pr + "<br>", 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."))
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
# Downloading personal decks def fireEvent(self, *args):
########################################################################## self.emit(SIGNAL("event"), *args)
class DeckChooser(QDialog):
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(_("<h1>Download Personal Deck</h1>"))
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()

View file

@ -49,8 +49,7 @@ class LatestVersionFinder(QThread):
if resp['latestVersion'] > aqt.appVersion: if resp['latestVersion'] > aqt.appVersion:
self.emit(SIGNAL("newVerAvail"), resp) self.emit(SIGNAL("newVerAvail"), resp)
diff = resp['currentTime'] - time.time() diff = resp['currentTime'] - time.time()
# a fairly liberal time check - sync is more strict if abs(diff) > 300:
if abs(diff) > 86400:
self.emit(SIGNAL("clockIsOff"), diff) self.emit(SIGNAL("clockIsOff"), diff)
def askAndUpdate(parent, version=None): def askAndUpdate(parent, version=None):

View file

@ -354,14 +354,14 @@ def maybeHideClose(bbox):
_tooltipTimer = None _tooltipTimer = None
_tooltipLabel = None _tooltipLabel = None
def tooltip(msg, period=3000): def tooltip(msg, period=3000, parent=None):
global _tooltipTimer, _tooltipLabel global _tooltipTimer, _tooltipLabel
class CustomLabel(QLabel): class CustomLabel(QLabel):
def mousePressEvent(self, evt): def mousePressEvent(self, evt):
evt.accept() evt.accept()
self.hide() self.hide()
closeTooltip() closeTooltip()
aw = aqt.mw.app.activeWindow() aw = parent or aqt.mw.app.activeWindow()
lab = CustomLabel("""\ lab = CustomLabel("""\
<table cellpadding=10> <table cellpadding=10>
<tr> <tr>