mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
move various major functions into separate files
This commit is contained in:
parent
73edb5eb28
commit
698ba38e74
7 changed files with 937 additions and 1121 deletions
46
aqt/cram.py
Normal file
46
aqt/cram.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
# Copyright: Damien Elmes <anki@ichi2.net>
|
||||
# -*- coding: utf-8 -*-
|
||||
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
|
||||
|
||||
def onCram(self, cardIds=[]):
|
||||
te = aqt.tagedit.TagEdit(self)
|
||||
te.setDeck(self.deck, "all")
|
||||
diag = GetTextDialog(
|
||||
self, _("Tags to cram:"), help="CramMode", edit=te)
|
||||
l = diag.layout()
|
||||
g = QGroupBox(_("Review Mode"))
|
||||
l.insertWidget(2, g)
|
||||
box = QVBoxLayout()
|
||||
g.setLayout(box)
|
||||
keep = QRadioButton(_("Show oldest modified first"))
|
||||
box.addWidget(keep)
|
||||
keep.setChecked(True)
|
||||
diag.setTabOrder(diag.l, keep)
|
||||
order = QRadioButton(_("Show in order added"))
|
||||
box.addWidget(order)
|
||||
random = QRadioButton(_("Show in random order"))
|
||||
box.addWidget(random)
|
||||
# hide tag list if we have ids
|
||||
if cardIds:
|
||||
diag.l.hide()
|
||||
diag.qlabel.hide()
|
||||
if diag.exec_():
|
||||
if keep.isChecked():
|
||||
order = "type, modified"
|
||||
elif order.isChecked():
|
||||
order = "created"
|
||||
else:
|
||||
order = "random()"
|
||||
if cardIds:
|
||||
active = cardIds
|
||||
else:
|
||||
active = unicode(diag.l.text())
|
||||
self.deck.setupCramScheduler(active, order)
|
||||
if self.state == "studyScreen":
|
||||
self.onStartReview()
|
||||
else:
|
||||
self.deck.reset()
|
||||
self.deck.getCard() # so scheduler will reset if empty
|
||||
self.moveToState("initial")
|
||||
if not self.deck.finishScheduler:
|
||||
showInfo(_("No cards matched the provided tags."))
|
49
aqt/editcurrent.py
Normal file
49
aqt/editcurrent.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
# Copyright: Damien Elmes <anki@ichi2.net>
|
||||
# -*- coding: utf-8 -*-
|
||||
# License: GNU GPL, version 3 or later; http://www.gnu.org/copyleft/gpl.html
|
||||
|
||||
class EditCurrent(object):
|
||||
|
||||
def __init__(self, mw):
|
||||
self.mw = mw
|
||||
|
||||
def _editCurrentState(self, oldState):
|
||||
if self.lastState == "editCurrentFact":
|
||||
return self.moveToState("saveEdit")
|
||||
self.form.actionRepeatAudio.setEnabled(False)
|
||||
self.deck.db.flush()
|
||||
self.showEditor()
|
||||
|
||||
def _saveEditState(self, oldState):
|
||||
self.form.actionRepeatAudio.setEnabled(True)
|
||||
self.editor.saveFieldsNow()
|
||||
self.form.buttonStack.show()
|
||||
return self.reset()
|
||||
|
||||
# Edit current fact
|
||||
##########################################################################
|
||||
|
||||
def setupEditor(self):
|
||||
print "setupeditor"
|
||||
return
|
||||
self.editor = aqt.facteditor.FactEditor(
|
||||
self, self.form.fieldsArea, self.deck)
|
||||
self.editor.clayout.setShortcut("")
|
||||
self.editor.onFactValid = self.onFactValid
|
||||
self.editor.onFactInvalid = self.onFactInvalid
|
||||
self.editor.resetOnEdit = False
|
||||
# editor
|
||||
self.connect(self.form.saveEditorButton, SIGNAL("clicked()"),
|
||||
lambda: self.moveToState("saveEdit"))
|
||||
|
||||
def showEditor(self):
|
||||
self.form.buttonStack.hide()
|
||||
self.switchToEditScreen()
|
||||
self.editor.setFact(self.currentCard.fact)
|
||||
self.editor.card = self.currentCard
|
||||
|
||||
def onFactValid(self, fact):
|
||||
self.form.saveEditorButton.setEnabled(True)
|
||||
|
||||
def onFactInvalid(self, fact):
|
||||
self.form.saveEditorButton.setEnabled(False)
|
1272
aqt/main.py
1272
aqt/main.py
File diff suppressed because it is too large
Load diff
|
@ -23,6 +23,7 @@ class Reviewer(object):
|
|||
self._answeredIds = []
|
||||
self.state = None
|
||||
self._setupStatus()
|
||||
addHook("leech", self.onLeech)
|
||||
|
||||
def show(self):
|
||||
self.web.setKeyHandler(self._keyHandler)
|
||||
|
@ -579,3 +580,19 @@ div#filler {
|
|||
p.setColor(QPalette.Highlight, QColor("#00ee00"))
|
||||
self.progressBar.setPalette(p)
|
||||
self.progressBar.setValue(perc)
|
||||
|
||||
# Leeches
|
||||
##########################################################################
|
||||
|
||||
# fixme: update; clear on card transition
|
||||
def onLeech(self, card):
|
||||
print "leech"
|
||||
return
|
||||
txt = (_("""\
|
||||
<b>%s</b>... is a <a href="http://ichi2.net/anki/wiki/Leeches">leech</a>.""")
|
||||
% stripHTML(stripSounds(self.currentCard.question)).\
|
||||
replace("\n", " ")[0:30])
|
||||
if isLeech and self.deck.db.scalar(
|
||||
"select 1 from cards where id = :id and type < 0", id=cardId):
|
||||
txt += _(" It has been suspended.")
|
||||
self.setNotice(txt)
|
||||
|
|
89
aqt/share.py
Normal file
89
aqt/share.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
# to be moved into libanki
|
||||
|
||||
def _copyToTmpDeck(self, name="cram.anki", tags="", ids=[]):
|
||||
ndir = tempfile.mkdtemp(prefix="anki")
|
||||
path = os.path.join(ndir, name)
|
||||
from anki.exporting import AnkiExporter
|
||||
e = AnkiExporter(self.deck)
|
||||
e.includeMedia = False
|
||||
if tags:
|
||||
e.limitTags = parseTags(tags)
|
||||
if ids:
|
||||
e.limitCardIds = ids
|
||||
path = unicode(path, sys.getfilesystemencoding())
|
||||
e.exportInto(path)
|
||||
return (e, path)
|
||||
|
||||
def onShare(self, tags):
|
||||
pwd = os.getcwd()
|
||||
# open tmp deck
|
||||
(e, path) = self._copyToTmpDeck(name="shared.anki", tags=tags)
|
||||
if not e.exportedCards:
|
||||
showInfo(_("No cards matched the provided tags."))
|
||||
return
|
||||
self.deck.startProgress()
|
||||
self.deck.updateProgress()
|
||||
d = DeckStorage.Deck(path, backup=False)
|
||||
# reset scheduling to defaults
|
||||
d.newCardsPerDay = 20
|
||||
d.delay0 = 600
|
||||
d.delay1 = 0
|
||||
d.delay2 = 0
|
||||
d.hardIntervalMin = 1.0
|
||||
d.hardIntervalMax = 1.1
|
||||
d.midIntervalMin = 3.0
|
||||
d.midIntervalMax = 5.0
|
||||
d.easyIntervalMin = 7.0
|
||||
d.easyIntervalMax = 9.0
|
||||
d.syncName = None
|
||||
d.setVar("newActive", u"")
|
||||
d.setVar("newInactive", u"")
|
||||
d.setVar("revActive", u"")
|
||||
d.setVar("revInactive", u"")
|
||||
self.deck.updateProgress()
|
||||
# unsuspend cards
|
||||
d.unsuspendCards(d.s.column0("select id from cards where type < 0"))
|
||||
self.deck.updateProgress()
|
||||
d.utcOffset = -2
|
||||
d.flushMod()
|
||||
d.save()
|
||||
self.deck.updateProgress()
|
||||
# media
|
||||
d.s.statement("update deckVars set value = '' where key = 'mediaURL'")
|
||||
self.deck.updateProgress()
|
||||
d.s.statement("vacuum")
|
||||
self.deck.updateProgress()
|
||||
nfacts = d.factCount
|
||||
mdir = self.deck.mediaDir()
|
||||
d.close()
|
||||
dir = os.path.dirname(path)
|
||||
zippath = os.path.join(dir, "shared-%d.zip" % time.time())
|
||||
# zip it up
|
||||
zip = zipfile.ZipFile(zippath, "w", zipfile.ZIP_DEFLATED)
|
||||
zip.writestr("facts", str(nfacts))
|
||||
zip.writestr("version", str(2))
|
||||
readmep = os.path.join(dir, "README.html")
|
||||
readme = open(readmep, "w")
|
||||
readme.write('''\
|
||||
<html><body>
|
||||
This is an exported packaged deck created by Anki.<p>
|
||||
|
||||
To share this deck with other people, upload it to
|
||||
<a href="http://anki.ichi2.net/file/upload">
|
||||
http://anki.ichi2.net/file/upload</a>, or email
|
||||
it to your friends.
|
||||
</body></html>''')
|
||||
readme.close()
|
||||
zip.write(readmep, "README.html")
|
||||
zip.write(path, "shared.anki")
|
||||
if mdir:
|
||||
for f in os.listdir(mdir):
|
||||
zip.write(os.path.join(mdir, f),
|
||||
os.path.join("shared.media/", f))
|
||||
os.chdir(pwd)
|
||||
os.chdir(pwd)
|
||||
self.deck.updateProgress()
|
||||
zip.close()
|
||||
os.unlink(path)
|
||||
self.deck.finishProgress()
|
||||
self.onOpenPluginFolder(dir)
|
284
aqt/studyopts.py
Normal file
284
aqt/studyopts.py
Normal file
|
@ -0,0 +1,284 @@
|
|||
|
||||
def _studyScreenState(self, oldState):
|
||||
self.currentCard = None
|
||||
# if self.deck.finishScheduler:
|
||||
# self.deck.finishScheduler()
|
||||
self.disableCardMenuItems()
|
||||
self.showStudyScreen()
|
||||
|
||||
# Study screen
|
||||
##########################################################################
|
||||
|
||||
def setupStudyScreen(self):
|
||||
return
|
||||
self.form.buttonStack.hide()
|
||||
self.form.newCardOrder.insertItems(
|
||||
0, QStringList(anki.consts.newCardOrderLabels().values()))
|
||||
self.form.newCardScheduling.insertItems(
|
||||
0, QStringList(anki.consts.newCardSchedulingLabels().values()))
|
||||
self.form.revCardOrder.insertItems(
|
||||
0, QStringList(anki.consts.revCardOrderLabels().values()))
|
||||
self.connect(self.form.optionsHelpButton,
|
||||
SIGNAL("clicked()"),
|
||||
lambda: QDesktopServices.openUrl(QUrl(
|
||||
aqt.appWiki + "StudyOptions")))
|
||||
self.connect(self.form.minuteLimit,
|
||||
SIGNAL("textChanged(QString)"), self.onMinuteLimitChanged)
|
||||
self.connect(self.form.questionLimit,
|
||||
SIGNAL("textChanged(QString)"), self.onQuestionLimitChanged)
|
||||
self.connect(self.form.newPerDay,
|
||||
SIGNAL("textChanged(QString)"), self.onNewLimitChanged)
|
||||
self.connect(self.form.startReviewingButton,
|
||||
SIGNAL("clicked()"),
|
||||
self.onStartReview)
|
||||
self.connect(self.form.newCardOrder,
|
||||
SIGNAL("activated(int)"), self.onNewCardOrderChanged)
|
||||
self.connect(self.form.failedCardMax,
|
||||
SIGNAL("editingFinished()"),
|
||||
self.onFailedMaxChanged)
|
||||
self.connect(self.form.newCategories,
|
||||
SIGNAL("clicked()"), self.onNewCategoriesClicked)
|
||||
self.connect(self.form.revCategories,
|
||||
SIGNAL("clicked()"), self.onRevCategoriesClicked)
|
||||
self.form.tabWidget.setCurrentIndex(self.config['studyOptionsTab'])
|
||||
|
||||
def onNewCategoriesClicked(self):
|
||||
aqt.activetags.show(self, "new")
|
||||
|
||||
def onRevCategoriesClicked(self):
|
||||
aqt.activetags.show(self, "rev")
|
||||
|
||||
def onFailedMaxChanged(self):
|
||||
try:
|
||||
v = int(self.form.failedCardMax.text())
|
||||
if v == 1 or v < 0:
|
||||
v = 2
|
||||
self.deck.failedCardMax = v
|
||||
except ValueError:
|
||||
pass
|
||||
self.form.failedCardMax.setText(str(self.deck.failedCardMax))
|
||||
self.deck.flushMod()
|
||||
|
||||
def onMinuteLimitChanged(self, qstr):
|
||||
try:
|
||||
val = float(self.form.minuteLimit.text()) * 60
|
||||
if self.deck.sessionTimeLimit == val:
|
||||
return
|
||||
self.deck.sessionTimeLimit = val
|
||||
except ValueError:
|
||||
pass
|
||||
self.deck.flushMod()
|
||||
self.updateStudyStats()
|
||||
|
||||
def onQuestionLimitChanged(self, qstr):
|
||||
try:
|
||||
val = int(self.form.questionLimit.text())
|
||||
if self.deck.sessionRepLimit == val:
|
||||
return
|
||||
self.deck.sessionRepLimit = val
|
||||
except ValueError:
|
||||
pass
|
||||
self.deck.flushMod()
|
||||
self.updateStudyStats()
|
||||
|
||||
def onNewLimitChanged(self, qstr):
|
||||
try:
|
||||
val = int(self.form.newPerDay.text())
|
||||
if self.deck.newCardsPerDay == val:
|
||||
return
|
||||
self.deck.newCardsPerDay = val
|
||||
except ValueError:
|
||||
pass
|
||||
self.deck.flushMod()
|
||||
self.deck.reset()
|
||||
self.statusView.redraw()
|
||||
self.updateStudyStats()
|
||||
|
||||
def onNewCardOrderChanged(self, ncOrd):
|
||||
def uf(obj, field, value):
|
||||
if getattr(obj, field) != value:
|
||||
setattr(obj, field, value)
|
||||
self.deck.flushMod()
|
||||
if ncOrd != 0:
|
||||
if self.deck.newCardOrder == 0:
|
||||
# need to put back in order
|
||||
self.mw.startProgress()
|
||||
self.mw.updateProgress(_("Ordering..."))
|
||||
self.deck.orderNewCards()
|
||||
self.deck.finishProgress()
|
||||
uf(self.deck, 'newCardOrder', ncOrd)
|
||||
elif ncOrd == 0:
|
||||
# (re-)randomize
|
||||
self.deck.startProgress()
|
||||
self.deck.updateProgress(_("Randomizing..."))
|
||||
self.deck.randomizeNewCards()
|
||||
self.deck.finishProgress()
|
||||
uf(self.deck, 'newCardOrder', ncOrd)
|
||||
|
||||
def updateActives(self):
|
||||
labels = [
|
||||
_("Show All Due Cards"),
|
||||
_("Show Chosen Categories")
|
||||
]
|
||||
if self.deck.getVar("newActive") or self.deck.getVar("newInactive"):
|
||||
new = labels[1]
|
||||
else:
|
||||
new = labels[0]
|
||||
self.form.newCategoryLabel.setText(new)
|
||||
if self.deck.getVar("revActive") or self.deck.getVar("revInactive"):
|
||||
rev = labels[1]
|
||||
else:
|
||||
rev = labels[0]
|
||||
self.form.revCategoryLabel.setText(rev)
|
||||
|
||||
def updateStudyStats(self):
|
||||
self.form.buttonStack.hide()
|
||||
self.deck.reset()
|
||||
self.updateActives()
|
||||
wasReached = self.deck.timeboxReached()
|
||||
sessionColour = '<font color=#0000ff>%s</font>'
|
||||
cardColour = '<font color=#0000ff>%s</font>'
|
||||
# top label
|
||||
h = {}
|
||||
h['ret'] = cardColour % (self.deck.revCount+self.deck.failedSoonCount)
|
||||
h['new'] = cardColour % self.deck.newCount
|
||||
h['newof'] = str(self.deck.newCountAll())
|
||||
# counts & time for today
|
||||
todayStart = self.deck.failedCutoff - 86400
|
||||
sql = "select count(), sum(userTime) from revlog"
|
||||
(reps, time_) = self.deck.db.first(
|
||||
sql + " where time > :start", start=todayStart)
|
||||
h['timeToday'] = sessionColour % (
|
||||
anki.utils.fmtTimeSpan(time_ or 0, short=True, point=1))
|
||||
h['repsToday'] = sessionColour % reps
|
||||
# and yesterday
|
||||
yestStart = todayStart - 86400
|
||||
(reps, time_) = self.deck.db.first(
|
||||
sql + " where time > :start and time <= :end",
|
||||
start=yestStart, end=todayStart)
|
||||
h['timeTodayChg'] = str(
|
||||
anki.utils.fmtTimeSpan(time_ or 0, short=True, point=1))
|
||||
h['repsTodayChg'] = str(reps)
|
||||
# session counts
|
||||
limit = self.deck.sessionTimeLimit
|
||||
start = self.deck.sessionStartTime or time.time() - limit
|
||||
start2 = self.deck.lastSessionStart or start - limit
|
||||
last10 = self.deck.db.scalar(
|
||||
"select count(*) from revlog where time >= :t",
|
||||
t=start)
|
||||
last20 = self.deck.db.scalar(
|
||||
"select count(*) from revlog where "
|
||||
"time >= :t and time < :t2",
|
||||
t=start2, t2=start)
|
||||
h['repsInSes'] = sessionColour % last10
|
||||
h['repsInSesChg'] = str(last20)
|
||||
h['cs_header'] = "<b>" + _("Cards/session:") + "</b>"
|
||||
h['cd_header'] = "<b>" + _("Cards/day:") + "</b>"
|
||||
h['td_header'] = "<b>" + _("Time/day:") + "</b>"
|
||||
h['rd_header'] = "<b>" + _("Reviews due:") + "</b>"
|
||||
h['ntod_header'] = "<b>" + _("New today:") + "</b>"
|
||||
h['ntot_header'] = "<b>" + _("New total:") + "</b>"
|
||||
stats1 = ("""\
|
||||
<table>
|
||||
<tr><td width=150>%(cs_header)s</td><td width=50><b>%(repsInSesChg)s</b></td>
|
||||
<td><b>%(repsInSes)s</b></td></tr></table>
|
||||
<hr>
|
||||
<table>
|
||||
<tr><td width=150>
|
||||
%(cd_header)s</td><td width=50><b>%(repsTodayChg)s</b></td>
|
||||
<td><b>%(repsToday)s</b></td></tr>
|
||||
<tr><td>%(td_header)s</td><td><b>%(timeTodayChg)s</b></td>
|
||||
<td><b>%(timeToday)s</b></td></tr>
|
||||
</table>""") % h
|
||||
|
||||
stats2 = ("""\
|
||||
<table>
|
||||
<tr><td width=180>%(rd_header)s</td><td align=right><b>%(ret)s</b></td></tr>
|
||||
<tr><td>%(ntod_header)s</td><td align=right><b>%(new)s</b></td></tr>
|
||||
<tr><td>%(ntot_header)s</td><td align=right>%(newof)s</td></tr>
|
||||
</table>""") % h
|
||||
self.form.optionsLabel.setText("""\
|
||||
<p><table><tr>
|
||||
%s
|
||||
</tr><tr>
|
||||
<td><hr>%s<hr></td></tr></table>""" % (stats1, stats2))
|
||||
h['tt_header'] = _("Session Statistics")
|
||||
h['cs_tip'] = _("The number of cards you studied in the current \
|
||||
session (blue) and previous session (black)")
|
||||
h['cd_tip'] = _("The number of cards you studied today (blue) and \
|
||||
yesterday (black)")
|
||||
h['td_tip'] = _("The number of minutes you studied today (blue) and \
|
||||
yesterday (black)")
|
||||
h['rd_tip'] = _("The number of cards that are waiting to be reviewed \
|
||||
today")
|
||||
h['ntod_tip'] = _("The number of new cards that are waiting to be \
|
||||
learnt today")
|
||||
h['ntot_tip'] = _("The total number of new cards in the deck")
|
||||
statToolTip = ("""<h1>%(tt_header)s</h1>
|
||||
<dl><dt><b>%(cs_header)s</b></dt><dd>%(cs_tip)s</dd></dl>
|
||||
<dl><dt><b>%(cd_header)s</b></dt><dd>%(cd_tip)s</dd></dl>
|
||||
<dl><dt><b>%(td_header)s</b></dt><dd>%(td_tip)s</dd></dl>
|
||||
<dl><dt><b>%(rd_header)s</b></dt><dd>%(rd_tip)s</dd></dl>
|
||||
<dl><dt><b>%(ntod_header)s</b></dt><dd>%(ntod_tip)s</dd></dl>
|
||||
<dl><dt><b>%(ntot_header)s</b></dt><dd>%(ntot_tip)s<</dd></dl>""") % h
|
||||
|
||||
self.form.optionsLabel.setToolTip(statToolTip)
|
||||
|
||||
def showStudyScreen(self):
|
||||
# forget last card
|
||||
self.lastCard = None
|
||||
self.switchToStudyScreen()
|
||||
self.updateStudyStats()
|
||||
self.form.startReviewingButton.setFocus()
|
||||
self.setupStudyOptions()
|
||||
self.form.studyOptionsFrame.setMaximumWidth(500)
|
||||
self.form.studyOptionsFrame.show()
|
||||
|
||||
def setupStudyOptions(self):
|
||||
self.form.newPerDay.setText(str(self.deck.newCardsPerDay))
|
||||
lim = self.deck.sessionTimeLimit/60
|
||||
if int(lim) == lim:
|
||||
lim = int(lim)
|
||||
self.form.minuteLimit.setText(str(lim))
|
||||
self.form.questionLimit.setText(str(self.deck.sessionRepLimit))
|
||||
self.form.newCardOrder.setCurrentIndex(self.deck.newCardOrder)
|
||||
self.form.newCardScheduling.setCurrentIndex(self.deck.newCardSpacing)
|
||||
self.form.revCardOrder.setCurrentIndex(self.deck.revCardOrder)
|
||||
self.form.failedCardsOption.clear()
|
||||
if self.deck.getFailedCardPolicy() == 5:
|
||||
labels = failedCardOptionLabels().values()
|
||||
else:
|
||||
labels = failedCardOptionLabels().values()[0:-1]
|
||||
self.form.failedCardsOption.insertItems(0, labels)
|
||||
self.form.failedCardsOption.setCurrentIndex(self.deck.getFailedCardPolicy())
|
||||
self.form.failedCardMax.setText(unicode(self.deck.failedCardMax))
|
||||
|
||||
def onStartReview(self):
|
||||
def uf(obj, field, value):
|
||||
if getattr(obj, field) != value:
|
||||
setattr(obj, field, value)
|
||||
self.deck.flushMod()
|
||||
self.form.studyOptionsFrame.hide()
|
||||
# make sure the size is updated before button stack shown
|
||||
self.app.processEvents()
|
||||
uf(self.deck, 'newCardSpacing',
|
||||
self.form.newCardScheduling.currentIndex())
|
||||
uf(self.deck, 'revCardOrder',
|
||||
self.form.revCardOrder.currentIndex())
|
||||
pol = self.deck.getFailedCardPolicy()
|
||||
if (pol != 5 and pol !=
|
||||
self.form.failedCardsOption.currentIndex()):
|
||||
self.deck.setFailedCardPolicy(
|
||||
self.form.failedCardsOption.currentIndex())
|
||||
self.deck.flushMod()
|
||||
self.deck.reset()
|
||||
if not self.deck.finishScheduler:
|
||||
self.deck.startTimebox()
|
||||
self.config['studyOptionsTab'] = self.form.tabWidget.currentIndex()
|
||||
self.moveToState("getQuestion")
|
||||
|
||||
def onStudyOptions(self):
|
||||
if self.state == "studyScreen":
|
||||
pass
|
||||
else:
|
||||
self.moveToState("studyScreen")
|
301
aqt/sync.py
301
aqt/sync.py
|
@ -14,6 +14,307 @@ from anki.db import sqlite
|
|||
import aqt.forms
|
||||
from anki.hooks import addHook, removeHook
|
||||
|
||||
class SyncManager(object):
|
||||
|
||||
# Syncing
|
||||
##########################################################################
|
||||
|
||||
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.config['syncUsername']
|
||||
p=self.config['syncPassword']
|
||||
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.config['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):
|
||||
ok = []
|
||||
for d in self.config['recentDeckPaths']:
|
||||
if os.path.exists(d):
|
||||
ok.append(d)
|
||||
return ok
|
||||
|
||||
def onConflict(self, deckName):
|
||||
diag = askUserDialog(_("""\
|
||||
<b>%s</b> has been changed on both
|
||||
the local and remote side. What do
|
||||
you want to do?""" % deckName),
|
||||
[_("Keep Local"),
|
||||
_("Keep Remote"),
|
||||
_("Cancel")])
|
||||
diag.setDefault(2)
|
||||
ret = diag.run()
|
||||
if ret == _("Keep Local"):
|
||||
self.syncThread.conflictResolution = "keepLocal"
|
||||
elif ret == _("Keep Remote"):
|
||||
self.syncThread.conflictResolution = "keepRemote"
|
||||
else:
|
||||
self.syncThread.conflictResolution = "cancel"
|
||||
|
||||
def 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.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.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):
|
||||
showWarning(
|
||||
_("The time or date on your computer is not correct.\n") +
|
||||
ngettext("It is off by %d second.\n\n",
|
||||
"It is off by %d seconds.\n\n", diff) % diff +
|
||||
_("Since this can cause many problems with syncing,\n"
|
||||
"syncing is disabled until you fix the problem.")
|
||||
)
|
||||
self.onSyncFinished()
|
||||
|
||||
def showSyncWarning(self, text):
|
||||
showWarning(text, self)
|
||||
self.setStatus("")
|
||||
|
||||
def badUserPass(self):
|
||||
aqt.preferences.Preferences(self, self.config).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)
|
||||
|
||||
def closeSyncProgress(self):
|
||||
self.syncProgressDialog.cancel()
|
||||
|
||||
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.config['syncUsername'] or not self.config['syncPassword']:
|
||||
d = QDialog(self)
|
||||
vbox = QVBoxLayout()
|
||||
l = QLabel(_(
|
||||
'<h1>Online Account</h1>'
|
||||
'To use your free <a href="http://anki.ichi2.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.config['syncUsername'] = unicode(user.text())
|
||||
self.config['syncPassword'] = unicode(passwd.text())
|
||||
|
||||
|
||||
# Synchronising a deck with a public server
|
||||
##########################################################################
|
||||
|
||||
|
|
Loading…
Reference in a new issue