# Copyright: Ankitects Pty Ltd and contributors # -*- coding: utf-8 -*- # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from copy import deepcopy from typing import Any import aqt from anki.errors import DeckRenameError from anki.lang import _, ngettext from anki.utils import fmtTimeSpan, ids2str from aqt import AnkiQt, gui_hooks from aqt.qt import * from aqt.sound import av_player from aqt.toolbar import BottomBar from aqt.utils import askUser, getOnlyText, openHelp, openLink, shortcut, showWarning class DeckBrowser: _dueTree: Any def __init__(self, mw: AnkiQt) -> None: self.mw = mw self.web = mw.web self.bottom = BottomBar(mw, mw.bottomWeb) self.scrollPos = QPoint(0, 0) def show(self): av_player.stop_and_clear_queue() self.web.set_bridge_command(self._linkHandler, "deck_browser") self._renderPage() # redraw top bar for theme change self.mw.toolbar.draw() 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() elif cmd == "lots": openHelp("using-decks-appropriately") elif cmd == "hidelots": self.mw.pm.profile["hideDeckLotsMsg"] = True self.refresh() elif cmd == "create": deck = getOnlyText(_("Name for deck:")) 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) return False def _selDeck(self, did): self.mw.col.decks.select(did) self.mw.onOverview() # HTML generation ########################################################################## _body = """
%(tree)s

%(stats)s %(countwarn)s
""" def _renderPage(self, reuse=False): if not reuse: self._dueTree = self.mw.col.sched.deckDueTree() self.__renderPage(None) return self.web.evalWithCallback("window.pageYOffset", self.__renderPage) def __renderPage(self, offset): tree = self._renderDeckTree(self._dueTree) stats = self._renderStats() self.web.stdHtml( self._body % dict(tree=tree, stats=stats, countwarn=self._countWarn()), css=["deckbrowser.css"], js=["jquery.js", "jquery-ui.js", "deckbrowser.js"], ) self.web.key = "deckBrowser" self._drawButtons() if offset is not None: self._scrollToOffset(offset) def _scrollToOffset(self, offset): self.web.eval("$(function() { window.scrollTo(0, %d, 'instant'); });" % offset) 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 msgp1 = ( ngettext("%d card", "%d cards", cards) % cards ) buf = _("Studied %(a)s %(b)s today.") % dict( a=msgp1, b=fmtTimeSpan(thetime, unit=1, inTime=True) ) return buf def _countWarn(self): if self.mw.col.decks.count() < 25 or self.mw.pm.profile.get("hideDeckLotsMsg"): return "" return "
" + ( _("You have a lot of decks. Please see %(a)s. %(b)s") % dict( a=( "%s" % _("this page") ), b=( "
(" "%s)" % (_("hide")) + "
" ), ) ) def _renderDeckTree(self, nodes, depth=0): if not nodes: return "" if depth == 0: buf = """ %s%s %s""" % ( _("Deck"), _("Due"), _("New"), ) buf += self._topLevelDragRow() else: buf = "" nameMap = self.mw.col.decks.nameMap() for node in nodes: buf += self._deckRow(node, depth, len(nodes), nameMap) if depth == 0: buf += self._topLevelDragRow() return buf def _deckRow(self, node, depth, cnt, nameMap): 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 for parent in self.mw.col.decks.parents(did, nameMap): 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 = "" % (klass, did) # deck link if children: collapse = ( "%s" % (did, prefix) ) else: collapse = "" if deck["dyn"]: extraclass = "filtered" else: extraclass = "" buf += """ %s%s%s""" % ( indent(), collapse, extraclass, did, name, ) # due counts def nonzeroColour(cnt, klass): if not cnt: klass = "zero-count" if cnt >= 1000: cnt = "1000+" return f'{cnt}' buf += "%s%s" % ( nonzeroColour(due, "review-count"), nonzeroColour(new, "new-count"), ) # options buf += ( "" "" % did ) # children buf += self._renderDeckTree(children, depth + 1) return buf def _topLevelDragRow(self): return " " # Options ########################################################################## def _showOptions(self, did) -> None: m = QMenu(self.mw) a = m.addAction(_("Rename")) qconnect(a.triggered, lambda b, did=did: self._rename(did)) a = m.addAction(_("Options")) qconnect(a.triggered, lambda b, did=did: self._options(did)) a = m.addAction(_("Export")) qconnect(a.triggered, lambda b, did=did: self._export(did)) a = m.addAction(_("Delete")) qconnect(a.triggered, lambda b, did=did: self._delete(did)) gui_hooks.deck_browser_will_show_options_menu(m, did) m.exec_(QCursor.pos()) def _export(self, did): self.mw.onExport(did=did) 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) newName = newName.replace('"', "") if not newName or newName == oldName: return try: self.mw.col.decks.rename(deck, newName) except DeckRenameError as e: 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) except DeckRenameError as e: 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 ###################################################################### drawLinks = [ ["", "shared", _("Get Shared")], ["", "create", _("Create Deck")], ["Ctrl+I", "import", _("Import File")], # Ctrl+I works from menu ] def _drawButtons(self): buf = "" drawLinks = deepcopy(self.drawLinks) for b in drawLinks: if b[0]: b[0] = _("Shortcut key: %s") % shortcut(b[0]) buf += """ """ % tuple( b ) self.bottom.draw(buf) self.bottom.web.set_bridge_command(self._linkHandler, "deck_browser_bottom_bar") def _onShared(self): openLink(aqt.appShared + "decks/")