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
|
2020-02-08 23:59:29 +01:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2021-01-05 11:43:37 +01:00
|
|
|
from concurrent.futures import Future
|
2019-12-20 10:19:03 +01:00
|
|
|
from copy import deepcopy
|
2020-02-17 16:26:21 +01:00
|
|
|
from dataclasses import dataclass
|
2021-01-30 11:37:29 +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
|
2021-01-07 07:20:02 +01:00
|
|
|
from anki.rsbackend import DeckTreeNode
|
2020-01-22 01:46:35 +01:00
|
|
|
from aqt import AnkiQt, gui_hooks
|
2019-12-20 10:19:03 +01:00
|
|
|
from aqt.qt import *
|
2020-01-20 11:10:38 +01:00
|
|
|
from aqt.sound import av_player
|
2020-01-22 01:46:35 +01:00
|
|
|
from aqt.toolbar import BottomBar
|
2020-11-17 08:42:43 +01:00
|
|
|
from aqt.utils import TR, askUser, getOnlyText, openLink, shortcut, showWarning, tr
|
2019-12-20 10:19:03 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-02-08 23:59:29 +01:00
|
|
|
class DeckBrowserBottomBar:
|
|
|
|
def __init__(self, deck_browser: DeckBrowser):
|
|
|
|
self.deck_browser = deck_browser
|
|
|
|
|
|
|
|
|
2020-02-17 16:26:21 +01:00
|
|
|
@dataclass
|
|
|
|
class DeckBrowserContent:
|
|
|
|
"""Stores sections of HTML content that the deck browser will be
|
|
|
|
populated with.
|
2020-08-31 05:29:28 +02:00
|
|
|
|
2020-02-17 16:26:21 +01:00
|
|
|
Attributes:
|
|
|
|
tree {str} -- HTML of the deck tree section
|
|
|
|
stats {str} -- HTML of the stats section
|
|
|
|
"""
|
|
|
|
|
|
|
|
tree: str
|
|
|
|
stats: str
|
2020-02-16 19:29:01 +01:00
|
|
|
|
|
|
|
|
2020-05-16 02:52:14 +02:00
|
|
|
@dataclass
|
|
|
|
class RenderDeckNodeContext:
|
|
|
|
current_deck_id: int
|
|
|
|
|
|
|
|
|
2017-02-06 23:21:33 +01:00
|
|
|
class DeckBrowser:
|
2020-05-16 02:52:14 +02:00
|
|
|
_dueTree: DeckTreeNode
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-01-22 01:46:35 +01:00
|
|
|
def __init__(self, mw: AnkiQt) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.mw = mw
|
|
|
|
self.web = mw.web
|
2020-01-22 01:46:35 +01:00
|
|
|
self.bottom = 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):
|
2020-01-20 11:10:38 +01:00
|
|
|
av_player.stop_and_clear_queue()
|
2020-02-08 23:59:29 +01:00
|
|
|
self.web.set_bridge_command(self._linkHandler, self)
|
2012-12-21 08:51:59 +01:00
|
|
|
self._renderPage()
|
2020-01-31 04:31:31 +01:00
|
|
|
# redraw top bar for theme change
|
2020-06-02 05:23:01 +02:00
|
|
|
self.mw.toolbar.redraw()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def refresh(self):
|
|
|
|
self._renderPage()
|
|
|
|
|
|
|
|
# Event handlers
|
|
|
|
##########################################################################
|
|
|
|
|
2021-01-30 11:37:29 +01:00
|
|
|
def _linkHandler(self, url: str) -> Any:
|
2012-12-21 08:51:59 +01:00
|
|
|
if ":" in url:
|
2021-01-25 16:33:18 +01:00
|
|
|
(cmd, arg) = url.split(":", 1)
|
2012-12-21 08:51:59 +01:00
|
|
|
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":
|
2020-11-17 08:42:43 +01:00
|
|
|
deck = getOnlyText(tr(TR.DECKS_NAME_FOR_DECK))
|
2012-12-21 08:51:59 +01:00
|
|
|
if deck:
|
|
|
|
self.mw.col.decks.id(deck)
|
2020-05-22 02:47:14 +02:00
|
|
|
gui_hooks.sidebar_should_refresh_decks()
|
2012-12-21 08:51:59 +01:00
|
|
|
self.refresh()
|
|
|
|
elif cmd == "drag":
|
2021-01-30 11:37:29 +01:00
|
|
|
source, target = arg.split(",")
|
|
|
|
self._handle_drag_and_drop(int(source), int(target or 0))
|
2012-12-21 08:51:59 +01:00
|
|
|
elif cmd == "collapse":
|
2020-05-16 02:52:14 +02:00
|
|
|
self._collapse(int(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
|
|
|
|
</center>
|
|
|
|
"""
|
|
|
|
|
|
|
|
def _renderPage(self, reuse=False):
|
|
|
|
if not reuse:
|
2020-05-16 02:52:14 +02:00
|
|
|
self._dueTree = self.mw.col.sched.deck_due_tree()
|
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):
|
2020-02-17 16:26:21 +01:00
|
|
|
content = DeckBrowserContent(
|
2020-08-31 05:29:28 +02:00
|
|
|
tree=self._renderDeckTree(self._dueTree),
|
|
|
|
stats=self._renderStats(),
|
2020-02-16 19:29:01 +01:00
|
|
|
)
|
2020-02-17 16:26:21 +01:00
|
|
|
gui_hooks.deck_browser_will_render_content(self, content)
|
2019-12-23 01:34:10 +01:00
|
|
|
self.web.stdHtml(
|
2020-02-17 16:26:21 +01:00
|
|
|
self._body % content.__dict__,
|
2020-11-01 05:26:58 +01:00
|
|
|
css=["css/deckbrowser.css"],
|
2020-12-21 04:20:55 +01:00
|
|
|
js=[
|
2020-12-28 14:18:07 +01:00
|
|
|
"js/vendor/jquery.min.js",
|
2020-12-30 12:07:02 +01:00
|
|
|
"js/vendor/jquery-ui.min.js",
|
2020-12-21 04:20:55 +01:00
|
|
|
"js/deckbrowser.js",
|
|
|
|
],
|
2020-02-12 22:00:13 +01:00
|
|
|
context=self,
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
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)
|
2020-04-09 05:40:19 +02:00
|
|
|
gui_hooks.deck_browser_did_render(self)
|
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):
|
2021-01-03 14:54:44 +01:00
|
|
|
return '<div id="studiedToday"><span>{}</span></div>'.format(
|
|
|
|
self.mw.col.studied_today(),
|
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-05-16 02:52:14 +02:00
|
|
|
def _renderDeckTree(self, top: DeckTreeNode) -> str:
|
|
|
|
buf = """
|
2020-07-22 03:00:39 +02:00
|
|
|
<tr><th colspan=5 align=start>%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>""" % (
|
2020-11-17 08:42:43 +01:00
|
|
|
tr(TR.DECKS_DECK),
|
2020-05-16 02:52:14 +02:00
|
|
|
tr(TR.STATISTICS_DUE_COUNT),
|
2020-11-17 08:42:43 +01:00
|
|
|
tr(TR.ACTIONS_NEW),
|
2020-05-16 02:52:14 +02:00
|
|
|
)
|
|
|
|
buf += self._topLevelDragRow()
|
|
|
|
|
|
|
|
ctx = RenderDeckNodeContext(current_deck_id=self.mw.col.conf["curDeck"])
|
|
|
|
|
|
|
|
for child in top.children:
|
|
|
|
buf += self._render_deck_node(child, ctx)
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
return buf
|
|
|
|
|
2020-05-16 02:52:14 +02:00
|
|
|
def _render_deck_node(self, node: DeckTreeNode, ctx: RenderDeckNodeContext) -> str:
|
|
|
|
if node.collapsed:
|
2012-12-21 08:51:59 +01:00
|
|
|
prefix = "+"
|
2020-05-16 02:02:08 +02:00
|
|
|
else:
|
|
|
|
prefix = "-"
|
2020-05-16 02:52:14 +02:00
|
|
|
|
|
|
|
due = node.review_count + node.learn_count
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
def indent():
|
2020-05-16 02:52:14 +02:00
|
|
|
return " " * 6 * (node.level - 1)
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2020-05-16 02:52:14 +02:00
|
|
|
if node.deck_id == ctx.current_deck_id:
|
2019-12-23 01:34:10 +01:00
|
|
|
klass = "deck current"
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
2019-12-23 01:34:10 +01:00
|
|
|
klass = "deck"
|
2020-05-16 02:52:14 +02:00
|
|
|
|
|
|
|
buf = "<tr class='%s' id='%d'>" % (klass, node.deck_id)
|
2012-12-21 08:51:59 +01:00
|
|
|
# deck link
|
2020-05-16 02:52:14 +02:00
|
|
|
if node.children:
|
2019-12-23 01:34:10 +01:00
|
|
|
collapse = (
|
|
|
|
"<a class=collapse href=# onclick='return pycmd(\"collapse:%d\")'>%s</a>"
|
2020-05-16 02:52:14 +02:00
|
|
|
% (node.deck_id, prefix)
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
else:
|
|
|
|
collapse = "<span class=collapse></span>"
|
2020-05-16 02:52:14 +02:00
|
|
|
if node.filtered:
|
2012-12-21 08:51:59 +01:00
|
|
|
extraclass = "filtered"
|
|
|
|
else:
|
|
|
|
extraclass = ""
|
|
|
|
buf += """
|
|
|
|
|
2016-05-31 10:51:40 +02:00
|
|
|
<td class=decktd colspan=5>%s%s<a class="deck %s"
|
2019-12-23 01:34:10 +01:00
|
|
|
href=# onclick="return pycmd('open:%d')">%s</a></td>""" % (
|
|
|
|
indent(),
|
|
|
|
collapse,
|
|
|
|
extraclass,
|
2020-05-16 02:52:14 +02:00
|
|
|
node.deck_id,
|
|
|
|
node.name,
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
# due counts
|
2020-01-23 06:08:10 +01:00
|
|
|
def nonzeroColour(cnt, klass):
|
2012-12-21 08:51:59 +01:00
|
|
|
if not cnt:
|
2020-01-23 06:08:10 +01:00
|
|
|
klass = "zero-count"
|
|
|
|
return f'<span class="{klass}">{cnt}</span>'
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
buf += "<td align=right>%s</td><td align=right>%s</td>" % (
|
2020-01-23 06:08:10 +01:00
|
|
|
nonzeroColour(due, "review-count"),
|
2020-05-16 02:52:14 +02:00
|
|
|
nonzeroColour(node.new_count, "new-count"),
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
# options
|
2019-12-23 01:34:10 +01:00
|
|
|
buf += (
|
|
|
|
"<td align=center class=opts><a onclick='return pycmd(\"opts:%d\");'>"
|
2020-05-16 02:52:14 +02:00
|
|
|
"<img src='/_anki/imgs/gears.svg' class=gears></a></td></tr>" % node.deck_id
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
# children
|
2020-05-16 02:52:14 +02:00
|
|
|
if not node.collapsed:
|
|
|
|
for child in node.children:
|
|
|
|
buf += self._render_deck_node(child, ctx)
|
2012-12-21 08:51:59 +01:00
|
|
|
return buf
|
|
|
|
|
|
|
|
def _topLevelDragRow(self):
|
|
|
|
return "<tr class='top-level-drag-row'><td colspan='6'> </td></tr>"
|
|
|
|
|
|
|
|
# Options
|
|
|
|
##########################################################################
|
|
|
|
|
2020-05-22 02:53:20 +02:00
|
|
|
def _showOptions(self, did: str) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
m = QMenu(self.mw)
|
2020-11-17 08:42:43 +01:00
|
|
|
a = m.addAction(tr(TR.ACTIONS_RENAME))
|
2020-05-22 02:53:20 +02:00
|
|
|
qconnect(a.triggered, lambda b, did=did: self._rename(int(did)))
|
2020-11-17 08:42:43 +01:00
|
|
|
a = m.addAction(tr(TR.ACTIONS_OPTIONS))
|
2020-01-15 22:41:23 +01:00
|
|
|
qconnect(a.triggered, lambda b, did=did: self._options(did))
|
2020-11-17 08:42:43 +01:00
|
|
|
a = m.addAction(tr(TR.ACTIONS_EXPORT))
|
2020-01-15 22:41:23 +01:00
|
|
|
qconnect(a.triggered, lambda b, did=did: self._export(did))
|
2020-11-17 08:42:43 +01:00
|
|
|
a = m.addAction(tr(TR.ACTIONS_DELETE))
|
2020-05-22 02:53:20 +02:00
|
|
|
qconnect(a.triggered, lambda b, did=did: self._delete(int(did)))
|
2020-05-22 03:27:40 +02:00
|
|
|
gui_hooks.deck_browser_will_show_options_menu(m, int(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)
|
|
|
|
|
2020-05-22 02:53:20 +02:00
|
|
|
def _rename(self, did: int) -> None:
|
2020-11-17 08:42:43 +01:00
|
|
|
self.mw.checkpoint(tr(TR.ACTIONS_RENAME_DECK))
|
2012-12-21 08:51:59 +01:00
|
|
|
deck = self.mw.col.decks.get(did)
|
2019-12-23 01:34:10 +01:00
|
|
|
oldName = deck["name"]
|
2020-11-17 08:42:43 +01:00
|
|
|
newName = getOnlyText(tr(TR.DECKS_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)
|
2020-05-22 02:47:14 +02:00
|
|
|
gui_hooks.sidebar_should_refresh_decks()
|
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()
|
|
|
|
|
2020-05-16 02:52:14 +02:00
|
|
|
def _collapse(self, did: int) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.mw.col.decks.collapse(did)
|
2020-05-16 05:05:20 +02:00
|
|
|
node = self.mw.col.decks.find_deck_in_tree(self._dueTree, did)
|
|
|
|
if node:
|
2020-05-16 02:52:14 +02:00
|
|
|
node.collapsed = not node.collapsed
|
2020-05-16 05:05:20 +02:00
|
|
|
self._renderPage(reuse=True)
|
2020-05-16 02:52:14 +02:00
|
|
|
|
2021-01-30 11:37:29 +01:00
|
|
|
def _handle_drag_and_drop(self, source: int, target: int) -> None:
|
|
|
|
self.mw.col.decks.drag_drop_decks([source], target)
|
|
|
|
gui_hooks.sidebar_should_refresh_decks()
|
2012-12-21 08:51:59 +01:00
|
|
|
self.show()
|
|
|
|
|
2021-01-05 11:28:19 +01:00
|
|
|
def ask_delete_deck(self, did: int) -> bool:
|
2012-12-21 08:51:59 +01:00
|
|
|
deck = self.mw.col.decks.get(did)
|
2021-01-05 11:50:54 +01:00
|
|
|
if deck["dyn"]:
|
|
|
|
return True
|
|
|
|
|
|
|
|
count = self.mw.col.decks.card_count(did, include_subdecks=True)
|
|
|
|
if not count:
|
|
|
|
return True
|
|
|
|
|
|
|
|
extra = tr(TR.DECKS_IT_HAS_CARD, count=count)
|
|
|
|
if askUser(
|
|
|
|
tr(TR.DECKS_ARE_YOU_SURE_YOU_WISH_TO, val=deck["name"]) + " " + extra
|
2019-12-23 01:34:10 +01:00
|
|
|
):
|
2021-01-05 11:28:19 +01:00
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
def _delete(self, did: int) -> None:
|
|
|
|
if self.ask_delete_deck(did):
|
2021-01-05 11:43:37 +01:00
|
|
|
|
|
|
|
def do_delete():
|
|
|
|
return self.mw.col.decks.rem(did, True)
|
|
|
|
|
|
|
|
def on_done(fut: Future):
|
|
|
|
self.show()
|
|
|
|
res = fut.result() # Required to check for errors
|
|
|
|
|
2021-01-05 11:28:19 +01:00
|
|
|
self.mw.checkpoint(tr(TR.DECKS_DELETE_DECK))
|
2021-01-05 11:43:37 +01:00
|
|
|
self.mw.taskman.with_progress(do_delete, on_done)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Top buttons
|
|
|
|
######################################################################
|
|
|
|
|
2017-07-15 15:39:01 +02:00
|
|
|
drawLinks = [
|
2020-11-17 08:42:43 +01:00
|
|
|
["", "shared", tr(TR.DECKS_GET_SHARED)],
|
|
|
|
["", "create", tr(TR.DECKS_CREATE_DECK)],
|
|
|
|
["Ctrl+Shift+I", "import", tr(TR.DECKS_IMPORT_FILE)],
|
2017-07-15 15:39:01 +02:00
|
|
|
]
|
|
|
|
|
|
|
|
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]:
|
2020-11-17 12:47:47 +01:00
|
|
|
b[0] = tr(TR.ACTIONS_SHORTCUT_KEY, val=shortcut(b[0]))
|
2012-12-21 08:51:59 +01:00
|
|
|
buf += """
|
2019-12-23 01:34:10 +01:00
|
|
|
<button title='%s' onclick='pycmd(\"%s\");'>%s</button>""" % tuple(
|
|
|
|
b
|
|
|
|
)
|
2020-02-12 22:00:13 +01:00
|
|
|
self.bottom.draw(
|
|
|
|
buf=buf,
|
|
|
|
link_handler=self._linkHandler,
|
|
|
|
web_context=DeckBrowserBottomBar(self),
|
2020-02-08 23:59:29 +01:00
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
def _onShared(self):
|
2019-12-23 01:34:10 +01:00
|
|
|
openLink(aqt.appShared + "decks/")
|