mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
speed up browser load by rendering deck tree in Rust and skipping counts
This commit is contained in:
parent
5cefece264
commit
a88bc1e836
7 changed files with 153 additions and 30 deletions
|
@ -30,7 +30,7 @@ message I18nBackendInit {
|
|||
message BackendInput {
|
||||
oneof value {
|
||||
SchedTimingTodayIn sched_timing_today = 17;
|
||||
Empty deck_tree = 18;
|
||||
DeckTreeIn deck_tree = 18;
|
||||
SearchCardsIn search_cards = 19;
|
||||
SearchNotesIn search_notes = 20;
|
||||
RenderCardIn render_card = 21;
|
||||
|
@ -108,7 +108,7 @@ message BackendOutput {
|
|||
AllStockNotetypesOut all_stock_notetypes = 60;
|
||||
|
||||
// fallible commands
|
||||
DeckTreeOut deck_tree = 18;
|
||||
DeckTreeNode deck_tree = 18;
|
||||
SearchCardsOut search_cards = 19;
|
||||
SearchNotesOut search_notes = 20;
|
||||
RenderCardOut render_card = 21;
|
||||
|
@ -238,19 +238,20 @@ message SchedTimingTodayOut {
|
|||
int64 next_day_at = 2;
|
||||
}
|
||||
|
||||
message DeckTreeOut {
|
||||
DeckTreeNode top = 1;
|
||||
message DeckTreeIn {
|
||||
bool include_counts = 1;
|
||||
}
|
||||
|
||||
message DeckTreeNode {
|
||||
// the components of a deck, split on ::
|
||||
repeated string names = 1;
|
||||
int64 deck_id = 2;
|
||||
uint32 review_count = 3;
|
||||
uint32 learn_count = 4;
|
||||
uint32 new_count = 5;
|
||||
repeated DeckTreeNode children = 6;
|
||||
bool collapsed = 7;
|
||||
int64 deck_id = 1;
|
||||
string name = 2;
|
||||
repeated DeckTreeNode children = 3;
|
||||
uint32 level = 4;
|
||||
bool collapsed = 5;
|
||||
|
||||
uint32 review_count = 6;
|
||||
uint32 learn_count = 7;
|
||||
uint32 new_count = 8;
|
||||
}
|
||||
|
||||
message RenderCardIn {
|
||||
|
@ -705,3 +706,4 @@ message AddOrUpdateDeckLegacyIn {
|
|||
bytes deck = 1;
|
||||
bool preserve_usn_and_mtime = 2;
|
||||
}
|
||||
|
||||
|
|
|
@ -219,6 +219,9 @@ class DeckManager:
|
|||
except anki.rsbackend.ExistsError:
|
||||
raise DeckRenameError("deck already exists")
|
||||
|
||||
def deck_tree(self) -> pb.DeckTreeNode:
|
||||
return self.col.backend.deck_tree(include_counts=False)
|
||||
|
||||
def all(self, force_default: bool = True) -> List:
|
||||
"""A list of all decks.
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ BackendCard = pb.Card
|
|||
BackendNote = pb.Note
|
||||
TagUsnTuple = pb.TagUsnTuple
|
||||
NoteType = pb.NoteType
|
||||
DeckTreeNode = pb.DeckTreeNode
|
||||
|
||||
try:
|
||||
import orjson
|
||||
|
@ -729,6 +730,11 @@ class RustBackend:
|
|||
def remove_deck(self, did: int) -> None:
|
||||
self._run_command(pb.BackendInput(remove_deck=did))
|
||||
|
||||
def deck_tree(self, include_counts: bool) -> DeckTreeNode:
|
||||
return self._run_command(
|
||||
pb.BackendInput(deck_tree=pb.DeckTreeIn(include_counts=include_counts))
|
||||
).deck_tree
|
||||
|
||||
|
||||
def translate_string_in(
|
||||
key: TR, **kwargs: Union[str, int, float]
|
||||
|
|
|
@ -24,7 +24,7 @@ from anki.decks import DeckManager
|
|||
from anki.lang import _, ngettext
|
||||
from anki.models import NoteType
|
||||
from anki.notes import Note
|
||||
from anki.rsbackend import TR
|
||||
from anki.rsbackend import TR, DeckTreeNode
|
||||
from anki.utils import htmlToTextLine, ids2str, intTime, isMac, isWin
|
||||
from aqt import AnkiQt, gui_hooks
|
||||
from aqt.editor import Editor
|
||||
|
@ -1147,32 +1147,28 @@ QTableView {{ gridline-color: {grid} }}
|
|||
root.addChild(item)
|
||||
|
||||
def _decksTree(self, root) -> None:
|
||||
assert self.col
|
||||
grps = self.col.sched.deckDueTree()
|
||||
tree = self.col.decks.deck_tree()
|
||||
|
||||
def fillGroups(root, grps, head=""):
|
||||
for g in grps:
|
||||
baseName = g[0]
|
||||
did = g[1]
|
||||
children = g[5]
|
||||
if str(did) == "1" and not children:
|
||||
def fillGroups(root, nodes: List[DeckTreeNode], head=""):
|
||||
for node in nodes:
|
||||
if node.deck_id == 1 and not node.children:
|
||||
if not self.mw.col.decks.should_default_be_displayed(
|
||||
force_default=False, assume_no_child=True
|
||||
):
|
||||
|
||||
continue
|
||||
|
||||
item = SidebarItem(
|
||||
baseName,
|
||||
node.name,
|
||||
":/icons/deck.svg",
|
||||
lambda baseName=baseName: self.setFilter("deck", head + baseName),
|
||||
lambda expanded, did=did: self.mw.col.decks.collapseBrowser(did),
|
||||
not self.mw.col.decks.get(did).get("browserCollapsed", False),
|
||||
lambda baseName=node.name: self.setFilter("deck", head + baseName),
|
||||
lambda expanded, did=node.deck_id: self.mw.col.decks.collapseBrowser(did),
|
||||
not self.mw.col.decks.get(node.deck_id).get("browserCollapsed", False),
|
||||
)
|
||||
root.addChild(item)
|
||||
newhead = head + baseName + "::"
|
||||
fillGroups(item, children, newhead)
|
||||
newhead = head + node.name + "::"
|
||||
fillGroups(item, node.children, newhead)
|
||||
|
||||
fillGroups(root, grps)
|
||||
fillGroups(root, tree.children)
|
||||
|
||||
def _modelTree(self, root) -> None:
|
||||
assert self.col
|
||||
|
|
|
@ -217,7 +217,7 @@ impl Backend {
|
|||
Value::SchedTimingToday(input) => {
|
||||
OValue::SchedTimingToday(self.sched_timing_today(input))
|
||||
}
|
||||
Value::DeckTree(_) => todo!(),
|
||||
Value::DeckTree(input) => OValue::DeckTree(self.deck_tree(input)?),
|
||||
Value::RenderCard(input) => OValue::RenderCard(self.render_template(input)?),
|
||||
Value::LocalMinutesWest(stamp) => {
|
||||
OValue::LocalMinutesWest(local_minutes_west_for_stamp(stamp))
|
||||
|
@ -436,6 +436,16 @@ impl Backend {
|
|||
}
|
||||
}
|
||||
|
||||
fn deck_tree(&self, input: pb::DeckTreeIn) -> Result<pb::DeckTreeNode> {
|
||||
self.with_col(|col| {
|
||||
if input.include_counts {
|
||||
todo!()
|
||||
} else {
|
||||
col.deck_tree()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn render_template(&self, input: pb::RenderCardIn) -> Result<pb::RenderCardOut> {
|
||||
// convert string map to &str
|
||||
let fields: HashMap<_, _> = input
|
||||
|
|
|
@ -16,6 +16,7 @@ use crate::{
|
|||
types::Usn,
|
||||
};
|
||||
mod schema11;
|
||||
mod tree;
|
||||
pub use schema11::DeckSchema11;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
|
|
105
rslib/src/decks/tree.rs
Normal file
105
rslib/src/decks/tree.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::{backend_proto::DeckTreeNode, collection::Collection, decks::DeckID, err::Result};
|
||||
use std::iter::Peekable;
|
||||
|
||||
// fixme: handle mixed case of parents
|
||||
|
||||
fn deck_names_to_tree(names: Vec<(DeckID, String)>) -> DeckTreeNode {
|
||||
let mut top = DeckTreeNode::default();
|
||||
let mut it = names.into_iter().peekable();
|
||||
|
||||
add_child_nodes(&mut it, &mut top);
|
||||
|
||||
top
|
||||
}
|
||||
|
||||
fn add_child_nodes(
|
||||
names: &mut Peekable<impl Iterator<Item = (DeckID, String)>>,
|
||||
parent: &mut DeckTreeNode,
|
||||
) {
|
||||
while let Some((id, name)) = names.peek() {
|
||||
let split_name: Vec<_> = name.split("::").collect();
|
||||
match split_name.len() as u32 {
|
||||
l if l <= parent.level => {
|
||||
// next item is at a higher level
|
||||
return;
|
||||
}
|
||||
l if l == parent.level + 1 => {
|
||||
// next item is an immediate descendent of parent
|
||||
parent.children.push(DeckTreeNode {
|
||||
deck_id: id.0,
|
||||
name: (*split_name.last().unwrap()).into(),
|
||||
children: vec![],
|
||||
level: parent.level + 1,
|
||||
..Default::default()
|
||||
});
|
||||
names.next();
|
||||
}
|
||||
_ => {
|
||||
// next item is at a lower level
|
||||
if let Some(last_child) = parent.children.last_mut() {
|
||||
add_child_nodes(names, last_child)
|
||||
} else {
|
||||
// immediate parent is missing, skip the deck until a DB check is run
|
||||
names.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
pub fn deck_tree(&self) -> Result<DeckTreeNode> {
|
||||
let names = self.storage.get_all_deck_names()?;
|
||||
Ok(deck_names_to_tree(names))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{collection::open_test_collection, err::Result};
|
||||
|
||||
#[test]
|
||||
fn wellformed() -> Result<()> {
|
||||
let mut col = open_test_collection();
|
||||
|
||||
col.get_or_create_normal_deck("1")?;
|
||||
col.get_or_create_normal_deck("2")?;
|
||||
col.get_or_create_normal_deck("2::a")?;
|
||||
col.get_or_create_normal_deck("2::b")?;
|
||||
col.get_or_create_normal_deck("2::c")?;
|
||||
col.get_or_create_normal_deck("2::c::A")?;
|
||||
col.get_or_create_normal_deck("3")?;
|
||||
|
||||
let tree = col.deck_tree()?;
|
||||
|
||||
// 4 including default
|
||||
assert_eq!(tree.children.len(), 4);
|
||||
|
||||
assert_eq!(tree.children[1].name, "2");
|
||||
assert_eq!(tree.children[1].children[0].name, "a");
|
||||
assert_eq!(tree.children[1].children[2].name, "c");
|
||||
assert_eq!(tree.children[1].children[2].children[0].name, "A");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed() -> Result<()> {
|
||||
let mut col = open_test_collection();
|
||||
|
||||
col.get_or_create_normal_deck("1")?;
|
||||
col.get_or_create_normal_deck("2::3::4")?;
|
||||
|
||||
// remove the top parent and middle parent
|
||||
col.storage.remove_deck(col.get_deck_id("2")?.unwrap())?;
|
||||
col.storage.remove_deck(col.get_deck_id("2::3")?.unwrap())?;
|
||||
|
||||
let tree = col.deck_tree()?;
|
||||
assert_eq!(tree.children.len(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue