diff --git a/proto/backend.proto b/proto/backend.proto index 402fa1b54..3fcb56a3a 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -264,6 +264,8 @@ message DeckTreeNode { uint32 review_count = 6; uint32 learn_count = 7; uint32 new_count = 8; + + bool filtered = 16; } message RenderExistingCardIn { diff --git a/pylib/anki/schedv2.py b/pylib/anki/schedv2.py index 83e908f01..5913fd217 100644 --- a/pylib/anki/schedv2.py +++ b/pylib/anki/schedv2.py @@ -15,7 +15,7 @@ from anki import hooks from anki.cards import Card from anki.consts import * from anki.lang import _ -from anki.rsbackend import FormatTimeSpanContext, SchedTimingToday +from anki.rsbackend import DeckTreeNode, FormatTimeSpanContext, SchedTimingToday from anki.utils import ids2str, intTime # card types: 0=new, 1=lrn, 2=rev, 3=relrn @@ -232,6 +232,10 @@ order by due""" "List of (base name, did, rev, lrn, new, children)" return self.col.backend.legacy_deck_tree() + def deck_due_tree(self) -> DeckTreeNode: + "Returns a tree of decks with counts." + return self.col.backend.deck_tree(include_counts=True) + # Getting the next card ########################################################################## diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index 883e1c1d9..5470dad0c 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -6,12 +6,11 @@ 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.rsbackend import TR, DeckTreeNode from anki.utils import ids2str from aqt import AnkiQt, gui_hooks from aqt.qt import * @@ -39,8 +38,13 @@ class DeckBrowserContent: stats: str +@dataclass +class RenderDeckNodeContext: + current_deck_id: int + + class DeckBrowser: - _dueTree: Any + _dueTree: DeckTreeNode def __init__(self, mw: AnkiQt) -> None: self.mw = mw @@ -83,7 +87,7 @@ class DeckBrowser: draggedDeckDid, ontoDeckDid = arg.split(",") self._dragDeckOnto(draggedDeckDid, ontoDeckDid) elif cmd == "collapse": - self._collapse(arg) + self._collapse(int(arg)) return False def _selDeck(self, did): @@ -106,7 +110,7 @@ class DeckBrowser: def _renderPage(self, reuse=False): if not reuse: - self._dueTree = self.mw.col.sched.deckDueTree() + self._dueTree = self.mw.col.sched.deck_due_tree() self.__renderPage(None) return self.web.evalWithCallback("window.pageYOffset", self.__renderPage) @@ -143,53 +147,49 @@ where id > ?""", 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 = """ + def _renderDeckTree(self, top: DeckTreeNode) -> str: + 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() + _("Deck"), + tr(TR.STATISTICS_DUE_COUNT), + _("New"), + ) + 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) + 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: + def _render_deck_node(self, node: DeckTreeNode, ctx: RenderDeckNodeContext) -> str: + if node.collapsed: prefix = "+" else: prefix = "-" - due += lrn + + due = node.review_count + node.learn_count def indent(): - return " " * 6 * depth + return " " * 6 * (node.level - 1) - if did == self.mw.col.conf["curDeck"]: + if node.deck_id == ctx.current_deck_id: klass = "deck current" else: klass = "deck" - buf = "" % (klass, did) + + buf = "" % (klass, node.deck_id) # deck link - if children: + if node.children: collapse = ( "%s" - % (did, prefix) + % (node.deck_id, prefix) ) else: collapse = "" - if deck["dyn"]: + if node.filtered: extraclass = "filtered" else: extraclass = "" @@ -200,8 +200,8 @@ where id > ?""", indent(), collapse, extraclass, - did, - name, + node.deck_id, + node.name, ) # due counts def nonzeroColour(cnt, klass): @@ -211,16 +211,17 @@ where id > ?""", buf += "%s%s" % ( nonzeroColour(due, "review-count"), - nonzeroColour(new, "new-count"), + nonzeroColour(node.new_count, "new-count"), ) # options buf += ( "" - "" % did + "" % node.deck_id ) # children - if not collapsed: - buf += self._renderDeckTree(children, depth + 1) + if not node.collapsed: + for child in node.children: + buf += self._render_deck_node(child, ctx) return buf def _topLevelDragRow(self): @@ -265,10 +266,21 @@ where id > ?""", self.mw.col.decks.select(did) self.mw.onDeckConf() - def _collapse(self, did): + def _collapse(self, did: int) -> None: self.mw.col.decks.collapse(did) + self._toggle_collapsed_in_node(did, self._dueTree) self._renderPage(reuse=True) + def _toggle_collapsed_in_node(self, deck_id: int, node: DeckTreeNode) -> bool: + "Toggle collapsed on deck in tree. Returns true if found." + if node.deck_id == deck_id: + node.collapsed = not node.collapsed + return True + for child in node.children: + if self._toggle_collapsed_in_node(deck_id, child): + return True + return False + def _dragDeckOnto(self, draggedDeckDid, ontoDeckDid): try: self.mw.col.decks.renameForDragAndDrop(draggedDeckDid, ontoDeckDid) diff --git a/qt/mypy.ini b/qt/mypy.ini index 5f88a2a12..bdee644c0 100644 --- a/qt/mypy.ini +++ b/qt/mypy.ini @@ -62,3 +62,5 @@ check_untyped_defs=true check_untyped_defs=true [mypy-aqt.preferences] check_untyped_defs=true +[mypy-aqt.deckbrowser] +check_untyped_defs=true diff --git a/rslib/src/decks/tree.rs b/rslib/src/decks/tree.rs index ff7a0386d..d5daeb7e2 100644 --- a/rslib/src/decks/tree.rs +++ b/rslib/src/decks/tree.rs @@ -62,16 +62,21 @@ fn add_child_nodes( } } -fn add_collapsed(node: &mut DeckTreeNode, decks: &HashMap, browser: bool) { +fn add_collapsed_and_filtered( + node: &mut DeckTreeNode, + decks: &HashMap, + browser: bool, +) { if let Some(deck) = decks.get(&DeckID(node.deck_id)) { node.collapsed = if browser { deck.common.browser_collapsed } else { deck.common.study_collapsed }; + node.filtered = deck.is_filtered(); } for child in &mut node.children { - add_collapsed(child, decks, browser); + add_collapsed_and_filtered(child, decks, browser); } } @@ -201,7 +206,7 @@ impl Collection { .map(|d| (d.id, d)) .collect(); - add_collapsed(&mut tree, &decks_map, !counts); + add_collapsed_and_filtered(&mut tree, &decks_map, !counts); if self.default_deck_is_empty()? { hide_default_deck(&mut tree); }