mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -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 {
|
message BackendInput {
|
||||||
oneof value {
|
oneof value {
|
||||||
SchedTimingTodayIn sched_timing_today = 17;
|
SchedTimingTodayIn sched_timing_today = 17;
|
||||||
Empty deck_tree = 18;
|
DeckTreeIn deck_tree = 18;
|
||||||
SearchCardsIn search_cards = 19;
|
SearchCardsIn search_cards = 19;
|
||||||
SearchNotesIn search_notes = 20;
|
SearchNotesIn search_notes = 20;
|
||||||
RenderCardIn render_card = 21;
|
RenderCardIn render_card = 21;
|
||||||
|
@ -108,7 +108,7 @@ message BackendOutput {
|
||||||
AllStockNotetypesOut all_stock_notetypes = 60;
|
AllStockNotetypesOut all_stock_notetypes = 60;
|
||||||
|
|
||||||
// fallible commands
|
// fallible commands
|
||||||
DeckTreeOut deck_tree = 18;
|
DeckTreeNode deck_tree = 18;
|
||||||
SearchCardsOut search_cards = 19;
|
SearchCardsOut search_cards = 19;
|
||||||
SearchNotesOut search_notes = 20;
|
SearchNotesOut search_notes = 20;
|
||||||
RenderCardOut render_card = 21;
|
RenderCardOut render_card = 21;
|
||||||
|
@ -238,19 +238,20 @@ message SchedTimingTodayOut {
|
||||||
int64 next_day_at = 2;
|
int64 next_day_at = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message DeckTreeOut {
|
message DeckTreeIn {
|
||||||
DeckTreeNode top = 1;
|
bool include_counts = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message DeckTreeNode {
|
message DeckTreeNode {
|
||||||
// the components of a deck, split on ::
|
int64 deck_id = 1;
|
||||||
repeated string names = 1;
|
string name = 2;
|
||||||
int64 deck_id = 2;
|
repeated DeckTreeNode children = 3;
|
||||||
uint32 review_count = 3;
|
uint32 level = 4;
|
||||||
uint32 learn_count = 4;
|
bool collapsed = 5;
|
||||||
uint32 new_count = 5;
|
|
||||||
repeated DeckTreeNode children = 6;
|
uint32 review_count = 6;
|
||||||
bool collapsed = 7;
|
uint32 learn_count = 7;
|
||||||
|
uint32 new_count = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RenderCardIn {
|
message RenderCardIn {
|
||||||
|
@ -705,3 +706,4 @@ message AddOrUpdateDeckLegacyIn {
|
||||||
bytes deck = 1;
|
bytes deck = 1;
|
||||||
bool preserve_usn_and_mtime = 2;
|
bool preserve_usn_and_mtime = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -219,6 +219,9 @@ class DeckManager:
|
||||||
except anki.rsbackend.ExistsError:
|
except anki.rsbackend.ExistsError:
|
||||||
raise DeckRenameError("deck already exists")
|
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:
|
def all(self, force_default: bool = True) -> List:
|
||||||
"""A list of all decks.
|
"""A list of all decks.
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,7 @@ BackendCard = pb.Card
|
||||||
BackendNote = pb.Note
|
BackendNote = pb.Note
|
||||||
TagUsnTuple = pb.TagUsnTuple
|
TagUsnTuple = pb.TagUsnTuple
|
||||||
NoteType = pb.NoteType
|
NoteType = pb.NoteType
|
||||||
|
DeckTreeNode = pb.DeckTreeNode
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import orjson
|
import orjson
|
||||||
|
@ -729,6 +730,11 @@ class RustBackend:
|
||||||
def remove_deck(self, did: int) -> None:
|
def remove_deck(self, did: int) -> None:
|
||||||
self._run_command(pb.BackendInput(remove_deck=did))
|
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(
|
def translate_string_in(
|
||||||
key: TR, **kwargs: Union[str, int, float]
|
key: TR, **kwargs: Union[str, int, float]
|
||||||
|
|
|
@ -24,7 +24,7 @@ from anki.decks import DeckManager
|
||||||
from anki.lang import _, ngettext
|
from anki.lang import _, ngettext
|
||||||
from anki.models import NoteType
|
from anki.models import NoteType
|
||||||
from anki.notes import Note
|
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 anki.utils import htmlToTextLine, ids2str, intTime, isMac, isWin
|
||||||
from aqt import AnkiQt, gui_hooks
|
from aqt import AnkiQt, gui_hooks
|
||||||
from aqt.editor import Editor
|
from aqt.editor import Editor
|
||||||
|
@ -1147,32 +1147,28 @@ QTableView {{ gridline-color: {grid} }}
|
||||||
root.addChild(item)
|
root.addChild(item)
|
||||||
|
|
||||||
def _decksTree(self, root) -> None:
|
def _decksTree(self, root) -> None:
|
||||||
assert self.col
|
tree = self.col.decks.deck_tree()
|
||||||
grps = self.col.sched.deckDueTree()
|
|
||||||
|
|
||||||
def fillGroups(root, grps, head=""):
|
def fillGroups(root, nodes: List[DeckTreeNode], head=""):
|
||||||
for g in grps:
|
for node in nodes:
|
||||||
baseName = g[0]
|
if node.deck_id == 1 and not node.children:
|
||||||
did = g[1]
|
|
||||||
children = g[5]
|
|
||||||
if str(did) == "1" and not children:
|
|
||||||
if not self.mw.col.decks.should_default_be_displayed(
|
if not self.mw.col.decks.should_default_be_displayed(
|
||||||
force_default=False, assume_no_child=True
|
force_default=False, assume_no_child=True
|
||||||
):
|
):
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
item = SidebarItem(
|
item = SidebarItem(
|
||||||
baseName,
|
node.name,
|
||||||
":/icons/deck.svg",
|
":/icons/deck.svg",
|
||||||
lambda baseName=baseName: self.setFilter("deck", head + baseName),
|
lambda baseName=node.name: self.setFilter("deck", head + baseName),
|
||||||
lambda expanded, did=did: self.mw.col.decks.collapseBrowser(did),
|
lambda expanded, did=node.deck_id: self.mw.col.decks.collapseBrowser(did),
|
||||||
not self.mw.col.decks.get(did).get("browserCollapsed", False),
|
not self.mw.col.decks.get(node.deck_id).get("browserCollapsed", False),
|
||||||
)
|
)
|
||||||
root.addChild(item)
|
root.addChild(item)
|
||||||
newhead = head + baseName + "::"
|
newhead = head + node.name + "::"
|
||||||
fillGroups(item, children, newhead)
|
fillGroups(item, node.children, newhead)
|
||||||
|
|
||||||
fillGroups(root, grps)
|
fillGroups(root, tree.children)
|
||||||
|
|
||||||
def _modelTree(self, root) -> None:
|
def _modelTree(self, root) -> None:
|
||||||
assert self.col
|
assert self.col
|
||||||
|
|
|
@ -217,7 +217,7 @@ impl Backend {
|
||||||
Value::SchedTimingToday(input) => {
|
Value::SchedTimingToday(input) => {
|
||||||
OValue::SchedTimingToday(self.sched_timing_today(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::RenderCard(input) => OValue::RenderCard(self.render_template(input)?),
|
||||||
Value::LocalMinutesWest(stamp) => {
|
Value::LocalMinutesWest(stamp) => {
|
||||||
OValue::LocalMinutesWest(local_minutes_west_for_stamp(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> {
|
fn render_template(&self, input: pb::RenderCardIn) -> Result<pb::RenderCardOut> {
|
||||||
// convert string map to &str
|
// convert string map to &str
|
||||||
let fields: HashMap<_, _> = input
|
let fields: HashMap<_, _> = input
|
||||||
|
|
|
@ -16,6 +16,7 @@ use crate::{
|
||||||
types::Usn,
|
types::Usn,
|
||||||
};
|
};
|
||||||
mod schema11;
|
mod schema11;
|
||||||
|
mod tree;
|
||||||
pub use schema11::DeckSchema11;
|
pub use schema11::DeckSchema11;
|
||||||
use std::{borrow::Cow, sync::Arc};
|
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