# Copyright: Ankitects Pty Ltd and contributors # -*- coding: utf-8 -*- # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from __future__ import annotations from copy import deepcopy from dataclasses import dataclass from typing import Any import aqt from anki.errors import DeckRenameError from anki.lang import _, ngettext from anki.rsbackend import TR from anki.utils import 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, openLink, shortcut, showWarning, tr class DeckBrowserBottomBar: def __init__(self, deck_browser: DeckBrowser): self.deck_browser = deck_browser @dataclass class DeckBrowserContent: """Stores sections of HTML content that the deck browser will be populated with. Attributes: tree {str} -- HTML of the deck tree section stats {str} -- HTML of the stats section """ tree: str stats: str 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, self) 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 == "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
""" 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): content = DeckBrowserContent( tree=self._renderDeckTree(self._dueTree), stats=self._renderStats(), ) gui_hooks.deck_browser_will_render_content(self, content) self.web.stdHtml( self._body % content.__dict__, css=["deckbrowser.css"], js=["jquery.js", "jquery-ui.js", "deckbrowser.js"], context=self, ) self.web.key = "deckBrowser" self._drawButtons() if offset is not None: self._scrollToOffset(offset) gui_hooks.deck_browser_did_render(self) 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 buf = self.mw.col.backend.studied_today(cards, float(thetime)) return buf def _renderDeckTree(self, nodes, depth=0): if not nodes: return "" if depth == 0: buf = """ %s%s %s""" % ( _("Deck"), tr(TR.STATISTICS_DUE_COUNT), _("New"), ) buf += self._topLevelDragRow() else: buf = "" for node in nodes: buf += self._deckRow(node, depth) if depth == 0: buf += self._topLevelDragRow() return buf def _deckRow(self, node, depth): name, did, due, lrn, new, children = node deck = self.mw.col.decks.get(did) collapsed = self.mw.col.decks.get(did)["collapsed"] if collapsed: prefix = "+" else: 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" return f'{cnt}' buf += "%s%s" % ( nonzeroColour(due, "review-count"), nonzeroColour(new, "new-count"), ) # options buf += ( "" "" % did ) # children if not collapsed: 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=buf, link_handler=self._linkHandler, web_context=DeckBrowserBottomBar(self), ) def _onShared(self): openLink(aqt.appShared + "decks/")