mirror of
https://github.com/ankitects/anki.git
synced 2025-11-09 06:07:11 -05:00
new sync gui
This commit is contained in:
parent
94baee058c
commit
7a71a0798c
4 changed files with 229 additions and 596 deletions
30
aqt/main.py
30
aqt/main.py
|
|
@ -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,10 +242,11 @@ 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:
|
||||||
self.col.reset()
|
if not guiOnly:
|
||||||
|
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
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
|
||||||
788
aqt/sync.py
788
aqt/sync.py
|
|
@ -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.
|
||||||
[_("Keep Local"),
|
|
||||||
_("Keep Remote"),
|
Do you want to keep the local version, overwriting the AnkiWeb version? Or \
|
||||||
_("Cancel")])
|
do you want to keep the AnkiWeb version, overwriting the version here?"""),
|
||||||
|
[_("Keep Local"),
|
||||||
|
_("Keep AnkiWeb"),
|
||||||
|
_("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))
|
||||||
return
|
finally:
|
||||||
# unknown error
|
# don't bump mod time unless we explicitly save
|
||||||
self.error(e)
|
self.col.close(save=False)
|
||||||
return -1
|
|
||||||
# ensure deck mods cached
|
def _sync(self):
|
||||||
try:
|
if self.auth:
|
||||||
proxy = self.connect()
|
# need to authenticate and obtain host key
|
||||||
except SyncError, e:
|
hkey = self.server.hostKey(*self.auth)
|
||||||
self.error(e)
|
print "hkey was", hkey
|
||||||
return -1
|
if not hkey:
|
||||||
# exists on server?
|
# provided details were invalid
|
||||||
deckCreated = False
|
return self.fireEvent("badAuth")
|
||||||
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:
|
|
||||||
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("<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:
|
else:
|
||||||
changes = False
|
# write new details and tell calling thread to save
|
||||||
if disable:
|
self.fireEvent("newKey", hkey)
|
||||||
self.setStatus(_("Disabled by upgrade."))
|
# run sync and check state
|
||||||
elif not deck:
|
ret = self.client.sync()
|
||||||
self.setStatus(_("No changes found."))
|
if ret == "badAuth":
|
||||||
# close and send signal to main thread
|
return self.fireEvent("badAuth")
|
||||||
self.deck.close()
|
elif ret == "clockOff":
|
||||||
if not deck:
|
return self.fireEvent("clockOff")
|
||||||
taken = time.time() - start
|
# note mediaUSN for later
|
||||||
if changes and taken < 2.5:
|
self.mediaUsn = self.client.mediaUsn
|
||||||
time.sleep(2.5 - taken)
|
# full sync?
|
||||||
else:
|
if ret == "fullSync":
|
||||||
time.sleep(0.25)
|
return self._fullSync()
|
||||||
self.emit(SIGNAL("syncFinished"))
|
# save and note success state
|
||||||
return True
|
self.col.save()
|
||||||
except Exception, e:
|
if ret == "noChanges":
|
||||||
self.ok = False
|
self.fireEvent("noChanges")
|
||||||
if self.deck:
|
else:
|
||||||
self.deck.close()
|
self.fireEvent("success")
|
||||||
self.error(e)
|
# then move on to media sync
|
||||||
return -1
|
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):
|
def fireEvent(self, *args):
|
||||||
QDialog.__init__(self, parent, Qt.Window)
|
self.emit(SIGNAL("event"), *args)
|
||||||
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()
|
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue