2019-02-05 04:59:03 +01:00
|
|
|
# Copyright: Ankitects Pty Ltd and contributors
|
2012-12-21 08:51:59 +01:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
2019-12-20 10:19:03 +01:00
|
|
|
from copy import deepcopy
|
2019-12-20 08:55:19 +01:00
|
|
|
from typing import Any
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
import aqt
|
2019-12-20 10:19:03 +01:00
|
|
|
from anki.errors import DeckRenameError
|
2016-07-08 04:59:10 +02:00
|
|
|
from anki.hooks import runHook
|
2019-03-04 02:58:34 +01:00
|
|
|
from anki.lang import _, ngettext
|
2019-12-20 10:19:03 +01:00
|
|
|
from anki.sound import clearAudioQueue
|
|
|
|
from anki.utils import fmtTimeSpan, ids2str
|
|
|
|
from aqt.qt import *
|
|
|
|
from aqt.utils import (askUser, getOnlyText, openHelp, openLink, shortcut,
|
|
|
|
showWarning)
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2017-02-06 23:21:33 +01:00
|
|
|
class DeckBrowser:
|
2019-12-20 08:55:19 +01:00
|
|
|
_dueTree: Any
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def __init__(self, mw):
|
|
|
|
self.mw = mw
|
|
|
|
self.web = mw.web
|
|
|
|
self.bottom = aqt.toolbar.BottomBar(mw, mw.bottomWeb)
|
2014-02-17 03:04:36 +01:00
|
|
|
self.scrollPos = QPoint(0, 0)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def show(self):
|
|
|
|
clearAudioQueue()
|
2016-05-31 10:51:40 +02:00
|
|
|
self.web.resetHandlers()
|
2016-06-06 07:50:03 +02:00
|
|
|
self.web.onBridgeCmd = self._linkHandler
|
2012-12-21 08:51:59 +01:00
|
|
|
self._renderPage()
|
|
|
|
|
|
|
|
def refresh(self):
|
|
|
|
self._renderPage()
|
|
|
|
|
|
|
|
# Event handlers
|
|
|
|
##########################################################################
|
|
|
|
|
|
|
|
def _linkHandler(self, url):
|
|
|
|
if ":" in url:
|
|
|
|
(cmd, arg) = url.split(":")
|
|
|
|
else:
|
|
|
|
cmd = url
|
|
|
|
if cmd == "open":
|
|
|
|
self._selDeck(arg)
|
|
|
|
elif cmd == "opts":
|
|
|
|
self._showOptions(arg)
|
|
|
|
elif cmd == "shared":
|
|
|
|
self._onShared()
|
|
|
|
elif cmd == "import":
|
|
|
|
self.mw.onImport()
|
2013-05-22 04:44:18 +02:00
|
|
|
elif cmd == "lots":
|
|
|
|
openHelp("using-decks-appropriately")
|
|
|
|
elif cmd == "hidelots":
|
|
|
|
self.mw.pm.profile['hideDeckLotsMsg'] = True
|
|
|
|
self.refresh()
|
2012-12-21 08:51:59 +01:00
|
|
|
elif cmd == "create":
|
2013-04-11 08:49:10 +02:00
|
|
|
deck = getOnlyText(_("Name for deck:"))
|
2012-12-21 08:51:59 +01:00
|
|
|
if deck:
|
|
|
|
self.mw.col.decks.id(deck)
|
|
|
|
self.refresh()
|
|
|
|
elif cmd == "drag":
|
|
|
|
draggedDeckDid, ontoDeckDid = arg.split(',')
|
|
|
|
self._dragDeckOnto(draggedDeckDid, ontoDeckDid)
|
|
|
|
elif cmd == "collapse":
|
|
|
|
self._collapse(arg)
|
2016-05-31 10:51:40 +02:00
|
|
|
return False
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def _selDeck(self, did):
|
|
|
|
self.mw.col.decks.select(did)
|
|
|
|
self.mw.onOverview()
|
|
|
|
|
|
|
|
# HTML generation
|
|
|
|
##########################################################################
|
|
|
|
|
|
|
|
_body = """
|
|
|
|
<center>
|
|
|
|
<table cellspacing=0 cellpading=3>
|
|
|
|
%(tree)s
|
|
|
|
</table>
|
|
|
|
|
|
|
|
<br>
|
|
|
|
%(stats)s
|
2013-05-22 04:44:18 +02:00
|
|
|
%(countwarn)s
|
2012-12-21 08:51:59 +01:00
|
|
|
</center>
|
|
|
|
"""
|
|
|
|
|
|
|
|
def _renderPage(self, reuse=False):
|
|
|
|
if not reuse:
|
|
|
|
self._dueTree = self.mw.col.sched.deckDueTree()
|
2019-04-29 08:46:13 +02:00
|
|
|
self.__renderPage(None)
|
2019-06-01 08:35:19 +02:00
|
|
|
return
|
2018-06-12 05:46:15 +02:00
|
|
|
self.web.evalWithCallback("window.pageYOffset", self.__renderPage)
|
|
|
|
|
|
|
|
def __renderPage(self, offset):
|
2012-12-21 08:51:59 +01:00
|
|
|
tree = self._renderDeckTree(self._dueTree)
|
|
|
|
stats = self._renderStats()
|
2013-05-22 04:44:18 +02:00
|
|
|
self.web.stdHtml(self._body%dict(
|
2017-08-10 11:02:32 +02:00
|
|
|
tree=tree, stats=stats, countwarn=self._countWarn()),
|
|
|
|
css=["deckbrowser.css"],
|
|
|
|
js=["jquery.js", "jquery-ui.js", "deckbrowser.js"])
|
2012-12-21 08:51:59 +01:00
|
|
|
self.web.key = "deckBrowser"
|
|
|
|
self._drawButtons()
|
2019-04-29 08:46:13 +02:00
|
|
|
if offset is not None:
|
|
|
|
self._scrollToOffset(offset)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2018-06-12 05:46:15 +02:00
|
|
|
def _scrollToOffset(self, offset):
|
|
|
|
self.web.eval("$(function() { window.scrollTo(0, %d, 'instant'); });" % offset)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def _renderStats(self):
|
|
|
|
cards, thetime = self.mw.col.db.first("""
|
|
|
|
select count(), sum(time)/1000 from revlog
|
|
|
|
where id > ?""", (self.mw.col.sched.dayCutoff-86400)*1000)
|
|
|
|
cards = cards or 0
|
|
|
|
thetime = thetime or 0
|
2013-01-13 20:20:17 +01:00
|
|
|
msgp1 = ngettext("<!--studied-->%d card", "<!--studied-->%d cards", cards) % cards
|
2017-11-30 20:03:51 +01:00
|
|
|
buf = _("Studied %(a)s %(b)s today.") % dict(a=msgp1,
|
|
|
|
b=fmtTimeSpan(thetime, unit=1, inTime=True))
|
2012-12-21 08:51:59 +01:00
|
|
|
return buf
|
|
|
|
|
2013-05-22 04:44:18 +02:00
|
|
|
def _countWarn(self):
|
|
|
|
if (self.mw.col.decks.count() < 25 or
|
|
|
|
self.mw.pm.profile.get("hideDeckLotsMsg")):
|
|
|
|
return ""
|
|
|
|
return "<br><div style='width:50%;border: 1px solid #000;padding:5px;'>"+(
|
2013-05-24 03:46:44 +02:00
|
|
|
_("You have a lot of decks. Please see %(a)s. %(b)s") % dict(
|
2018-10-23 09:05:47 +02:00
|
|
|
a=("<a href=# onclick=\"return pycmd('lots')\">%s</a>" % _(
|
2016-05-31 10:51:40 +02:00
|
|
|
"this page")),
|
2018-10-23 09:05:47 +02:00
|
|
|
b=("<br><small><a href=# onclick='return pycmd(\"hidelots\")'>("
|
2016-05-31 10:51:40 +02:00
|
|
|
"%s)</a></small>" % (_("hide"))+
|
2017-05-28 03:13:16 +02:00
|
|
|
"</div>")))
|
2013-05-22 04:44:18 +02:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
def _renderDeckTree(self, nodes, depth=0):
|
|
|
|
if not nodes:
|
|
|
|
return ""
|
|
|
|
if depth == 0:
|
|
|
|
buf = """
|
|
|
|
<tr><th colspan=5 align=left>%s</th><th class=count>%s</th>
|
2016-05-31 10:51:40 +02:00
|
|
|
<th class=count>%s</th><th class=optscol></th></tr>""" % (
|
2012-12-21 08:51:59 +01:00
|
|
|
_("Deck"), _("Due"), _("New"))
|
|
|
|
buf += self._topLevelDragRow()
|
|
|
|
else:
|
|
|
|
buf = ""
|
2018-05-31 08:24:34 +02:00
|
|
|
nameMap = self.mw.col.decks.nameMap()
|
2012-12-21 08:51:59 +01:00
|
|
|
for node in nodes:
|
2018-05-31 08:24:34 +02:00
|
|
|
buf += self._deckRow(node, depth, len(nodes), nameMap)
|
2012-12-21 08:51:59 +01:00
|
|
|
if depth == 0:
|
|
|
|
buf += self._topLevelDragRow()
|
|
|
|
return buf
|
|
|
|
|
2018-05-31 08:24:34 +02:00
|
|
|
def _deckRow(self, node, depth, cnt, nameMap):
|
2012-12-21 08:51:59 +01:00
|
|
|
name, did, due, lrn, new, children = node
|
|
|
|
deck = self.mw.col.decks.get(did)
|
|
|
|
if did == 1 and cnt > 1 and not children:
|
|
|
|
# if the default deck is empty, hide it
|
|
|
|
if not self.mw.col.db.scalar("select 1 from cards where did = 1"):
|
|
|
|
return ""
|
|
|
|
# parent toggled for collapsing
|
2018-05-31 08:24:34 +02:00
|
|
|
for parent in self.mw.col.decks.parents(did, nameMap):
|
2012-12-21 08:51:59 +01:00
|
|
|
if parent['collapsed']:
|
|
|
|
buff = ""
|
|
|
|
return buff
|
|
|
|
prefix = "-"
|
|
|
|
if self.mw.col.decks.get(did)['collapsed']:
|
|
|
|
prefix = "+"
|
|
|
|
due += lrn
|
|
|
|
def indent():
|
|
|
|
return " "*6*depth
|
|
|
|
if did == self.mw.col.conf['curDeck']:
|
|
|
|
klass = 'deck current'
|
|
|
|
else:
|
|
|
|
klass = 'deck'
|
|
|
|
buf = "<tr class='%s' id='%d'>" % (klass, did)
|
|
|
|
# deck link
|
|
|
|
if children:
|
2018-10-23 09:05:47 +02:00
|
|
|
collapse = "<a class=collapse href=# onclick='return pycmd(\"collapse:%d\")'>%s</a>" % (did, prefix)
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
|
|
|
collapse = "<span class=collapse></span>"
|
|
|
|
if deck['dyn']:
|
|
|
|
extraclass = "filtered"
|
|
|
|
else:
|
|
|
|
extraclass = ""
|
|
|
|
buf += """
|
|
|
|
|
2016-05-31 10:51:40 +02:00
|
|
|
<td class=decktd colspan=5>%s%s<a class="deck %s"
|
2018-10-23 08:47:01 +02:00
|
|
|
href=# onclick="return pycmd('open:%d')">%s</a></td>"""% (
|
2012-12-21 08:51:59 +01:00
|
|
|
indent(), collapse, extraclass, did, name)
|
|
|
|
# due counts
|
|
|
|
def nonzeroColour(cnt, colour):
|
|
|
|
if not cnt:
|
|
|
|
colour = "#e0e0e0"
|
|
|
|
if cnt >= 1000:
|
|
|
|
cnt = "1000+"
|
|
|
|
return "<font color='%s'>%s</font>" % (colour, cnt)
|
|
|
|
buf += "<td align=right>%s</td><td align=right>%s</td>" % (
|
|
|
|
nonzeroColour(due, "#007700"),
|
|
|
|
nonzeroColour(new, "#000099"))
|
|
|
|
# options
|
2018-10-23 09:05:47 +02:00
|
|
|
buf += ("<td align=center class=opts><a onclick='return pycmd(\"opts:%d\");'>"
|
2017-08-15 05:54:58 +02:00
|
|
|
"<img src='/_anki/imgs/gears.svg' class=gears></a></td></tr>" % did)
|
2012-12-21 08:51:59 +01:00
|
|
|
# children
|
|
|
|
buf += self._renderDeckTree(children, depth+1)
|
|
|
|
return buf
|
|
|
|
|
|
|
|
def _topLevelDragRow(self):
|
|
|
|
return "<tr class='top-level-drag-row'><td colspan='6'> </td></tr>"
|
|
|
|
|
|
|
|
# Options
|
|
|
|
##########################################################################
|
|
|
|
|
|
|
|
def _showOptions(self, did):
|
|
|
|
m = QMenu(self.mw)
|
|
|
|
a = m.addAction(_("Rename"))
|
2016-05-31 10:51:40 +02:00
|
|
|
a.triggered.connect(lambda b, did=did: self._rename(did))
|
2012-12-21 08:51:59 +01:00
|
|
|
a = m.addAction(_("Options"))
|
2016-05-31 10:51:40 +02:00
|
|
|
a.triggered.connect(lambda b, did=did: self._options(did))
|
2014-06-20 02:13:12 +02:00
|
|
|
a = m.addAction(_("Export"))
|
2016-05-31 10:51:40 +02:00
|
|
|
a.triggered.connect(lambda b, did=did: self._export(did))
|
2012-12-21 08:51:59 +01:00
|
|
|
a = m.addAction(_("Delete"))
|
2016-05-31 10:51:40 +02:00
|
|
|
a.triggered.connect(lambda b, did=did: self._delete(did))
|
2016-07-04 08:32:30 +02:00
|
|
|
runHook("showDeckOptions", m, did)
|
2012-12-21 08:51:59 +01:00
|
|
|
m.exec_(QCursor.pos())
|
|
|
|
|
2014-06-20 02:13:12 +02:00
|
|
|
def _export(self, did):
|
|
|
|
self.mw.onExport(did=did)
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
def _rename(self, did):
|
|
|
|
self.mw.checkpoint(_("Rename Deck"))
|
|
|
|
deck = self.mw.col.decks.get(did)
|
|
|
|
oldName = deck['name']
|
|
|
|
newName = getOnlyText(_("New deck name:"), default=oldName)
|
2013-06-12 04:12:03 +02:00
|
|
|
newName = newName.replace('"', "")
|
2012-12-21 08:51:59 +01:00
|
|
|
if not newName or newName == oldName:
|
|
|
|
return
|
|
|
|
try:
|
|
|
|
self.mw.col.decks.rename(deck, newName)
|
2016-05-12 06:45:35 +02:00
|
|
|
except DeckRenameError as e:
|
2012-12-21 08:51:59 +01:00
|
|
|
return showWarning(e.description)
|
|
|
|
self.show()
|
|
|
|
|
|
|
|
def _options(self, did):
|
|
|
|
# select the deck first, because the dyn deck conf assumes the deck
|
|
|
|
# we're editing is the current one
|
|
|
|
self.mw.col.decks.select(did)
|
|
|
|
self.mw.onDeckConf()
|
|
|
|
|
|
|
|
def _collapse(self, did):
|
|
|
|
self.mw.col.decks.collapse(did)
|
|
|
|
self._renderPage(reuse=True)
|
|
|
|
|
|
|
|
def _dragDeckOnto(self, draggedDeckDid, ontoDeckDid):
|
|
|
|
try:
|
|
|
|
self.mw.col.decks.renameForDragAndDrop(draggedDeckDid, ontoDeckDid)
|
2016-05-12 06:45:35 +02:00
|
|
|
except DeckRenameError as e:
|
2012-12-21 08:51:59 +01:00
|
|
|
return showWarning(e.description)
|
|
|
|
|
|
|
|
self.show()
|
|
|
|
|
|
|
|
def _delete(self, did):
|
|
|
|
if str(did) == '1':
|
|
|
|
return showWarning(_("The default deck can't be deleted."))
|
|
|
|
self.mw.checkpoint(_("Delete Deck"))
|
|
|
|
deck = self.mw.col.decks.get(did)
|
|
|
|
if not deck['dyn']:
|
|
|
|
dids = [did] + [r[1] for r in self.mw.col.decks.children(did)]
|
|
|
|
cnt = self.mw.col.db.scalar(
|
|
|
|
"select count() from cards where did in {0} or "
|
|
|
|
"odid in {0}".format(ids2str(dids)))
|
|
|
|
if cnt:
|
|
|
|
extra = ngettext(" It has %d card.", " It has %d cards.", cnt) % cnt
|
|
|
|
else:
|
|
|
|
extra = None
|
|
|
|
if deck['dyn'] or not extra or askUser(
|
|
|
|
(_("Are you sure you wish to delete %s?") % deck['name']) +
|
|
|
|
extra):
|
|
|
|
self.mw.progress.start(immediate=True)
|
|
|
|
self.mw.col.decks.rem(did, True)
|
|
|
|
self.mw.progress.finish()
|
|
|
|
self.show()
|
|
|
|
|
|
|
|
# Top buttons
|
|
|
|
######################################################################
|
|
|
|
|
2017-07-15 15:39:01 +02:00
|
|
|
drawLinks = [
|
2012-12-21 08:51:59 +01:00
|
|
|
["", "shared", _("Get Shared")],
|
|
|
|
["", "create", _("Create Deck")],
|
2017-07-15 15:39:01 +02:00
|
|
|
["Ctrl+I", "import", _("Import File")], # Ctrl+I works from menu
|
|
|
|
]
|
|
|
|
|
|
|
|
def _drawButtons(self):
|
2012-12-21 08:51:59 +01:00
|
|
|
buf = ""
|
2017-08-06 17:03:11 +02:00
|
|
|
drawLinks = deepcopy(self.drawLinks)
|
|
|
|
for b in drawLinks:
|
2012-12-21 08:51:59 +01:00
|
|
|
if b[0]:
|
|
|
|
b[0] = _("Shortcut key: %s") % shortcut(b[0])
|
|
|
|
buf += """
|
2016-06-06 07:50:03 +02:00
|
|
|
<button title='%s' onclick='pycmd(\"%s\");'>%s</button>""" % tuple(b)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.bottom.draw(buf)
|
2016-06-06 07:50:03 +02:00
|
|
|
self.bottom.web.onBridgeCmd = self._linkHandler
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def _onShared(self):
|
|
|
|
openLink(aqt.appShared+"decks/")
|