From 1823c0dda4c71198ac3f711d586ee630b140220a Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 20 Mar 2021 11:59:45 +0100 Subject: [PATCH 01/18] Add get_note_without_fields() from storage --- rslib/src/storage/note/get_without_fields.sql | 10 ++++++++++ rslib/src/storage/note/mod.rs | 11 +++++++++++ 2 files changed, 21 insertions(+) create mode 100644 rslib/src/storage/note/get_without_fields.sql diff --git a/rslib/src/storage/note/get_without_fields.sql b/rslib/src/storage/note/get_without_fields.sql new file mode 100644 index 000000000..61fe1702c --- /dev/null +++ b/rslib/src/storage/note/get_without_fields.sql @@ -0,0 +1,10 @@ +SELECT id, + guid, + mid, + mod, + usn, + tags, + "", + cast(sfld AS text), + csum +FROM notes \ No newline at end of file diff --git a/rslib/src/storage/note/mod.rs b/rslib/src/storage/note/mod.rs index d8a28b630..5d6e680b5 100644 --- a/rslib/src/storage/note/mod.rs +++ b/rslib/src/storage/note/mod.rs @@ -29,6 +29,17 @@ impl super::SqliteStorage { .transpose() } + pub fn get_note_without_fields(&self, nid: NoteID) -> Result> { + self.db + .prepare_cached(concat!( + include_str!("get_without_fields.sql"), + " where id = ?" + ))? + .query_and_then(params![nid], row_to_note)? + .next() + .transpose() + } + /// If fields have been modified, caller must call note.prepare_for_update() prior to calling this. pub(crate) fn update_note(&self, note: &Note) -> Result<()> { assert!(note.id.0 != 0); From e931a429b36b95e4d99bb3405979b40f8ed67570 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 20 Mar 2021 12:00:45 +0100 Subject: [PATCH 02/18] Add html_to_text_line() on backend --- rslib/src/text.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/rslib/src/text.rs b/rslib/src/text.rs index e4ed3e8d2..85a713539 100644 --- a/rslib/src/text.rs +++ b/rslib/src/text.rs @@ -10,6 +10,26 @@ use unicode_normalization::{ char::is_combining_mark, is_nfc, is_nfkd_quick, IsNormalized, UnicodeNormalization, }; +pub trait Trimming { + fn trim(self) -> Self; +} + +impl Trimming for Cow<'_, str> { + fn trim(self) -> Self { + match self { + Cow::Borrowed(text) => text.trim().into(), + Cow::Owned(text) => { + let trimmed = text.as_str().trim(); + if trimmed.len() == text.len() { + text.into() + } else { + trimmed.to_string().into() + } + } + } + } +} + #[derive(Debug, PartialEq)] pub enum AVTag { SoundOrVideo(String), @@ -72,6 +92,29 @@ lazy_static! { (.*?) # 3 - field text \[/anki:tts\] "#).unwrap(); + + static ref PERSISTENT_HTML_SPACERS: Regex = Regex::new("
|
|
|\n").unwrap(); + + static ref UNPRINTABLE_TAGS: Regex = Regex::new( + r"(?xs) + \[sound:[^]]+\] + | + \[\[type:[^]]+\]\] + ").unwrap(); +} + +pub fn html_to_text_line(html: &str) -> Cow { + let mut out: Cow = html.into(); + if let Cow::Owned(o) = PERSISTENT_HTML_SPACERS.replace_all(&out, " ") { + out = o.into(); + } + if let Cow::Owned(o) = UNPRINTABLE_TAGS.replace_all(&out, "") { + out = o.into(); + } + if let Cow::Owned(o) = strip_html_preserving_media_filenames(&out) { + out = o.into(); + } + out.trim() } pub fn strip_html(html: &str) -> Cow { From 436269b701bf937349dd2f0de0b9a7a9076a72cc Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 20 Mar 2021 12:01:53 +0100 Subject: [PATCH 03/18] Add backend mod for browser rows --- rslib/src/notetype/render.rs | 2 +- rslib/src/search/browser.rs | 335 +++++++++++++++++++++++++++++++++++ rslib/src/search/mod.rs | 1 + 3 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 rslib/src/search/browser.rs diff --git a/rslib/src/notetype/render.rs b/rslib/src/notetype/render.rs index f4683f10f..a480183ff 100644 --- a/rslib/src/notetype/render.rs +++ b/rslib/src/notetype/render.rs @@ -82,7 +82,7 @@ impl Collection { }) } - fn render_card_inner( + pub fn render_card_inner( &mut self, note: &Note, card: &Card, diff --git a/rslib/src/search/browser.rs b/rslib/src/search/browser.rs new file mode 100644 index 000000000..9f4d6bc9a --- /dev/null +++ b/rslib/src/search/browser.rs @@ -0,0 +1,335 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use std::sync::Arc; + +use chrono::prelude::*; +use itertools::Itertools; + +use crate::err::{AnkiError, Result}; +use crate::i18n::{tr_args, I18n, TR}; +use crate::{ + card::{Card, CardID, CardQueue, CardType}, + collection::Collection, + decks::Deck, + notes::Note, + notetype::{CardTemplate, NoteType, NoteTypeKind}, + scheduler::{timespan::time_span, timing::SchedTimingToday}, + template::RenderedNode, + text::{extract_av_tags, html_to_text_line}, + timestamp::{TimestampMillis, TimestampSecs}, +}; + +#[derive(Debug, PartialEq)] +pub struct Row { + pub cells: Vec, + pub color: RowColor, + pub font: Font, +} + +#[derive(Debug, PartialEq)] +pub struct Cell { + pub text: String, + pub is_rtl: bool, +} + +#[derive(Debug, PartialEq)] +pub enum RowColor { + Default, + Marked, + Suspended, + FlagRed, + FlagOrange, + FlagGreen, + FlagBlue, +} + +#[derive(Debug, PartialEq)] +pub struct Font { + pub name: String, + pub size: u32, +} + +struct RowContext<'a> { + col: &'a Collection, + card: Card, + note: Note, + notetype: Arc, + deck: Option, + original_deck: Option>, + i18n: &'a I18n, + offset: FixedOffset, + timing: SchedTimingToday, + question_nodes: Option>, + answer_nodes: Option>, +} + +impl RowContext<'_> { + fn template(&self) -> Result<&CardTemplate> { + self.notetype.get_template(self.card.template_idx) + } + + fn deck(&mut self) -> Result<&Deck> { + if self.deck.is_none() { + self.deck = Some( + self.col + .storage + .get_deck(self.card.deck_id)? + .ok_or(AnkiError::NotFound)?, + ); + } + Ok(self.deck.as_ref().unwrap()) + } + + fn original_deck(&mut self) -> Result<&Option> { + if self.original_deck.is_none() { + self.original_deck = Some(self.col.storage.get_deck(self.card.original_deck_id)?); + } + Ok(self.original_deck.as_ref().unwrap()) + } +} + +fn row_context_from_cid<'a>( + col: &'a mut Collection, + id: CardID, + with_card_render: bool, +) -> Result> { + let card = col.storage.get_card(id)?.ok_or(AnkiError::NotFound)?; + let note = if with_card_render { + col.storage.get_note(card.note_id)? + } else { + col.storage.get_note_without_fields(card.note_id)? + } + .ok_or(AnkiError::NotFound)?; + let notetype = col + .get_notetype(note.notetype_id)? + .ok_or(AnkiError::NotFound)?; + let offset = col.local_utc_offset_for_user()?; + let timing = col.timing_today()?; + let (question_nodes, answer_nodes) = if with_card_render { + let render = col.render_card_inner( + ¬e, + &card, + ¬etype, + notetype.get_template(card.template_idx)?, + true, + )?; + (Some(render.qnodes), Some(render.anodes)) + } else { + (None, None) + }; + + Ok(RowContext { + col, + card, + note, + notetype, + deck: None, + original_deck: None, + i18n: &col.i18n, + offset, + timing, + question_nodes, + answer_nodes, + }) +} + +impl Collection { + pub fn browser_row_for_card(&mut self, id: CardID) -> Result { + let columns: Vec = self.get_config_optional("activeCols").unwrap_or_else(|| { + vec![ + "noteFld".to_string(), + "template".to_string(), + "cardDue".to_string(), + "deck".to_string(), + ] + }); + let mut context = row_context_from_cid(self, id, note_fields_required(&columns))?; + Ok(Row { + cells: columns + .iter() + .map(|column| get_cell(column, &mut context)) + .collect::>()?, + color: get_row_color(&context), + font: get_row_font(&context)?, + }) + } +} + +fn get_cell(column: &str, context: &mut RowContext) -> Result { + Ok(Cell { + text: get_cell_text(column, context)?, + is_rtl: get_is_rtl(column, context), + }) +} + +fn get_cell_text(column: &str, context: &mut RowContext) -> Result { + Ok(match column { + "answer" => answer_str(context)?, + "cardDue" => card_due_str(context)?, + "cardEase" => card_ease_str(context), + "cardIvl" => card_interval_str(context), + "cardLapses" => context.card.lapses.to_string(), + "cardMod" => context.card.mtime.date_string(context.offset), + "cardReps" => context.card.reps.to_string(), + "deck" => deck_str(context)?, + "note" => context.notetype.name.to_owned(), + "noteCrt" => note_creation_str(context), + "noteFld" => note_field_str(context), + "noteMod" => context.note.mtime.date_string(context.offset), + "noteTags" => context.note.tags.join(" "), + "question" => question_str(context)?, + "template" => template_str(context)?, + _ => "".to_string(), + }) +} + +fn answer_str(context: &RowContext) -> Result { + let text = context + .answer_nodes + .as_ref() + .unwrap() + .iter() + .map(|node| match node { + RenderedNode::Text { text } => text, + RenderedNode::Replacement { + field_name: _, + current_text, + filters: _, + } => current_text, + }) + .join(""); + Ok(html_to_text_line(&extract_av_tags(&text, false).0).to_string()) +} + +fn card_due_str(context: &mut RowContext) -> Result { + Ok(if context.original_deck()?.is_some() { + context.i18n.tr(TR::BrowsingFiltered).into() + } else if context.card.queue == CardQueue::New || context.card.ctype == CardType::New { + context.i18n.trn( + TR::StatisticsDueForNewCard, + tr_args!["number"=>context.card.due], + ) + } else { + let date = match context.card.queue { + CardQueue::Learn => TimestampSecs(context.card.due as i64), + CardQueue::DayLearn | CardQueue::Review => TimestampSecs::now().adding_secs( + ((context.card.due - context.timing.days_elapsed as i32) * 86400) as i64, + ), + _ => return Ok("".into()), + }; + date.date_string(context.offset) + }) +} + +fn card_ease_str(context: &RowContext) -> String { + match context.card.ctype { + CardType::New => context.i18n.tr(TR::BrowsingNew).into(), + _ => format!("{}%", context.card.ease_factor / 10), + } +} + +fn card_interval_str(context: &RowContext) -> String { + match context.card.ctype { + CardType::New => context.i18n.tr(TR::BrowsingNew).into(), + CardType::Learn => context.i18n.tr(TR::BrowsingLearning).into(), + _ => time_span((context.card.interval * 86400) as f32, context.i18n, false), + } +} + +fn deck_str(context: &mut RowContext) -> Result { + let deck_name = context.deck()?.human_name(); + Ok(if let Some(original_deck) = context.original_deck()? { + format!("{} ({})", &deck_name, &original_deck.human_name()) + } else { + deck_name + }) +} + +fn note_creation_str(context: &RowContext) -> String { + TimestampMillis(context.note.id.into()) + .as_secs() + .date_string(context.offset) +} + +fn note_field_str(context: &RowContext) -> String { + if let Some(field) = &context.note.sort_field { + field.to_owned() + } else { + "".to_string() + } +} + +fn template_str(context: &RowContext) -> Result { + let name = &context.template()?.name; + Ok(match context.notetype.config.kind() { + NoteTypeKind::Normal => name.to_owned(), + NoteTypeKind::Cloze => format!("{} {}", name, context.card.template_idx + 1), + }) +} + +fn question_str(context: &RowContext) -> Result { + let text = context + .question_nodes + .as_ref() + .unwrap() + .iter() + .map(|node| match node { + RenderedNode::Text { text } => text, + RenderedNode::Replacement { + field_name: _, + current_text, + filters: _, + } => current_text, + }) + .join(""); + Ok(html_to_text_line(&extract_av_tags(&text, true).0).to_string()) +} + +fn get_is_rtl(column: &str, context: &RowContext) -> bool { + match column { + "noteFld" => { + let index = context.notetype.config.sort_field_idx as usize; + context.notetype.fields[index].config.rtl + } + _ => false, + } +} + +fn get_row_color(context: &RowContext) -> RowColor { + match context.card.flags { + 1 => RowColor::FlagRed, + 2 => RowColor::FlagOrange, + 3 => RowColor::FlagGreen, + 4 => RowColor::FlagBlue, + _ => { + if context + .note + .tags + .iter() + .any(|tag| tag.eq_ignore_ascii_case("marked")) + { + RowColor::Marked + } else if context.card.queue == CardQueue::Suspended { + RowColor::Suspended + } else { + RowColor::Default + } + } + } +} + +fn get_row_font(context: &RowContext) -> Result { + Ok(Font { + name: context.template()?.config.browser_font_name.to_owned(), + size: context.template()?.config.browser_font_size, + }) +} + +const FIELDS_REQUIRING_COLUMNS: [&str; 2] = ["question", "answer"]; + +fn note_fields_required(columns: &[String]) -> bool { + columns + .iter() + .any(|c| FIELDS_REQUIRING_COLUMNS.contains(&c.as_str())) +} diff --git a/rslib/src/search/mod.rs b/rslib/src/search/mod.rs index b469542df..fa929cfe8 100644 --- a/rslib/src/search/mod.rs +++ b/rslib/src/search/mod.rs @@ -1,6 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +pub mod browser; mod cards; mod notes; mod parser; From c68a6131e0201a2370722d26aab21bfbe5d4bb84 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 20 Mar 2021 12:02:51 +0100 Subject: [PATCH 04/18] Add backend routine for browser rows --- rslib/backend.proto | 21 ++++++++++++++++++ rslib/src/backend/search.rs | 43 +++++++++++++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/rslib/backend.proto b/rslib/backend.proto index 443a59972..00e2c6671 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -234,6 +234,7 @@ service SearchService { rpc JoinSearchNodes(JoinSearchNodesIn) returns (String); rpc ReplaceSearchNode(ReplaceSearchNodeIn) returns (String); rpc FindAndReplace(FindAndReplaceIn) returns (OpChangesWithCount); + rpc BrowserRowForCard(CardID) returns (BrowserRow); } service StatsService { @@ -1038,6 +1039,26 @@ message FindAndReplaceIn { string field_name = 6; } +message BrowserRow { + message Cell { + string text = 1; + bool is_rtl = 2; + } + enum Color { + COLOR_DEFAULT = 0; + COLOR_MARKED = 1; + COLOR_SUSPENDED = 2; + COLOR_FLAG_RED = 3; + COLOR_FLAG_ORANGE = 4; + COLOR_FLAG_GREEN = 5; + COLOR_FLAG_BLUE = 6; + } + repeated Cell cells = 1; + Color color = 2; + string font_name = 3; + uint32 font_size = 4; +} + message AfterNoteUpdatesIn { repeated int64 nids = 1; bool mark_notes_modified = 2; diff --git a/rslib/src/backend/search.rs b/rslib/src/backend/search.rs index ed4c7d497..b6ffde8e1 100644 --- a/rslib/src/backend/search.rs +++ b/rslib/src/backend/search.rs @@ -13,8 +13,9 @@ use crate::{ config::SortKind, prelude::*, search::{ - concatenate_searches, parse_search, replace_search_node, write_nodes, BoolSeparator, Node, - PropertyKind, RatingKind, SearchNode, SortMode, StateKind, TemplateKind, + browser, concatenate_searches, parse_search, replace_search_node, write_nodes, + BoolSeparator, Node, PropertyKind, RatingKind, SearchNode, SortMode, StateKind, + TemplateKind, }, text::escape_anki_wildcards, }; @@ -89,6 +90,10 @@ impl SearchService for Backend { .map(Into::into) }) } + + fn browser_row_for_card(&self, input: pb::CardId) -> Result { + self.with_col(|col| col.browser_row_for_card(input.cid.into()).map(Into::into)) + } } impl TryFrom for Node { @@ -264,3 +269,37 @@ impl From> for SortMode { } } } + +impl From for pb::BrowserRow { + fn from(row: browser::Row) -> Self { + pb::BrowserRow { + cells: row.cells.into_iter().map(Into::into).collect(), + color: row.color.into(), + font_name: row.font.name, + font_size: row.font.size, + } + } +} + +impl From for pb::browser_row::Cell { + fn from(cell: browser::Cell) -> Self { + pb::browser_row::Cell { + text: cell.text, + is_rtl: cell.is_rtl, + } + } +} + +impl From for i32 { + fn from(color: browser::RowColor) -> Self { + match color { + browser::RowColor::Default => pb::browser_row::Color::Default as i32, + browser::RowColor::Marked => pb::browser_row::Color::Marked as i32, + browser::RowColor::Suspended => pb::browser_row::Color::Suspended as i32, + browser::RowColor::FlagRed => pb::browser_row::Color::FlagRed as i32, + browser::RowColor::FlagOrange => pb::browser_row::Color::FlagOrange as i32, + browser::RowColor::FlagGreen => pb::browser_row::Color::FlagGreen as i32, + browser::RowColor::FlagBlue => pb::browser_row::Color::FlagBlue as i32, + } + } +} From 922fccee581a01cd9343381452e02ac02916242e Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 20 Mar 2021 12:03:26 +0100 Subject: [PATCH 05/18] Use backend rows in browser.py --- pylib/anki/collection.py | 17 +- qt/aqt/browser.py | 344 ++++++++++++--------------------------- 2 files changed, 116 insertions(+), 245 deletions(-) diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 01b0fb3c4..684b90bdb 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -3,7 +3,7 @@ from __future__ import annotations -from typing import Any, List, Literal, Optional, Sequence, Tuple, Union +from typing import Any, Generator, List, Literal, Optional, Sequence, Tuple, Union import anki._backend.backend_pb2 as _pb @@ -18,6 +18,7 @@ UndoStatus = _pb.UndoStatus OpChanges = _pb.OpChanges OpChangesWithCount = _pb.OpChangesWithCount DefaultsForAdding = _pb.DeckAndNotetype +BrowserRow = _pb.BrowserRow import copy import os @@ -682,6 +683,20 @@ class Collection: else: return SearchNode.Group.Joiner.OR + # Browser rows + ########################################################################## + + def browser_row_for_card( + self, cid: int + ) -> Tuple[Generator[Tuple[str, bool], None, None], BrowserRow.Color, str, int]: + row = self._backend.browser_row_for_card(cid) + return ( + ((cell.text, cell.is_rtl) for cell in row.cells), + row.color, + row.font_name, + row.font_size, + ) + # Config ########################################################################## diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index d4f25c774..2c03b23ce 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -5,21 +5,32 @@ from __future__ import annotations import html import time -from dataclasses import dataclass, field +from dataclasses import dataclass from operator import itemgetter -from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union, cast +from typing import ( + Any, + Callable, + Dict, + Generator, + List, + Optional, + Sequence, + Tuple, + Union, + cast, +) import aqt import aqt.forms from anki.cards import Card -from anki.collection import Collection, Config, OpChanges, SearchNode +from anki.collection import BrowserRow, Collection, Config, OpChanges, SearchNode from anki.consts import * from anki.errors import InvalidInput, NotFoundError from anki.lang import without_unicode_isolation from anki.models import NoteType from anki.stats import CardStats from anki.tags import MARKED_TAG -from anki.utils import htmlToTextLine, ids2str, isMac, isWin +from anki.utils import ids2str, isMac, isWin from aqt import AnkiQt, colors, gui_hooks from aqt.card_ops import set_card_deck, set_card_flag from aqt.editor import Editor @@ -91,25 +102,63 @@ class SearchContext: # Data model ########################################################################## -# temporary cache to avoid hitting the database on redraw + @dataclass class Cell: - text: str = "" - font: Optional[Tuple[str, int]] = None - is_rtl: bool = False + text: str + is_rtl: bool -@dataclass class CellRow: - columns: List[Cell] - refreshed_at: float = field(default_factory=time.time) - card_flag: int = 0 - marked: bool = False - suspended: bool = False + def __init__( + self, + cells: Generator[Tuple[str, bool], None, None], + color: BrowserRow.Color.V, + font_name: str, + font_size: int, + ) -> None: + self.refreshed_at: float = time.time() + self.cells: Tuple[Cell, ...] = tuple(Cell(*cell) for cell in cells) + self.color: Optional[Tuple[str, str]] = backend_color_to_aqt_color(color) + self.font_name: str = font_name or "arial" + self.font_size: int = font_size if font_size > 0 else 12 def is_stale(self, threshold: float) -> bool: return self.refreshed_at < threshold + @staticmethod + def generic(length: int, cell_text: str) -> CellRow: + return CellRow( + ((cell_text, False) for cell in range(length)), + BrowserRow.COLOR_DEFAULT, + "arial", + 12, + ) + + @staticmethod + def placeholder(length: int) -> CellRow: + return CellRow.generic(length, "...") + + @staticmethod + def deleted(length: int) -> CellRow: + return CellRow.generic(length, tr(TR.BROWSING_ROW_DELETED)) + + +def backend_color_to_aqt_color(color: BrowserRow.Color.V) -> Optional[Tuple[str, str]]: + if color == BrowserRow.COLOR_MARKED: + return colors.MARKED_BG + if color == BrowserRow.COLOR_SUSPENDED: + return colors.SUSPENDED_BG + if color == BrowserRow.COLOR_FLAG_RED: + return colors.FLAG1_BG + if color == BrowserRow.COLOR_FLAG_ORANGE: + return colors.FLAG2_BG + if color == BrowserRow.COLOR_FLAG_GREEN: + return colors.FLAG3_BG + if color == BrowserRow.COLOR_FLAG_BLUE: + return colors.FLAG4_BG + return None + class DataModel(QAbstractTableModel): def __init__(self, browser: Browser) -> None: @@ -121,73 +170,38 @@ class DataModel(QAbstractTableModel): "activeCols", ["noteFld", "template", "cardDue", "deck"] ) self.cards: Sequence[int] = [] - self.cardObjs: Dict[int, Card] = {} - self._row_cache: Dict[int, CellRow] = {} + self._rows: Dict[int, CellRow] = {} self._last_refresh = 0.0 # serve stale content to avoid hitting the DB? self.block_updates = False - def getCard(self, index: QModelIndex) -> Optional[Card]: - return self._get_card_by_row(index.row()) - - def _get_card_by_row(self, row: int) -> Optional[Card]: - "None if card is not in DB." - id = self.cards[row] - if not id in self.cardObjs: - try: - card = self.col.getCard(id) - except NotFoundError: - # deleted - card = None - self.cardObjs[id] = card - return self.cardObjs[id] - - # Card and cell data cache - ###################################################################### - # Stopgap until we can fetch this data a row at a time from Rust. + def get_id(self, index: QModelIndex) -> int: + return self.cards[index.row()] def get_cell(self, index: QModelIndex) -> Cell: - row = self.get_row(index.row()) - return row.columns[index.column()] + return self.get_row(index).cells[index.column()] - def get_row(self, row: int) -> CellRow: - if entry := self._row_cache.get(row): - if not self.block_updates and entry.is_stale(self._last_refresh): + def get_row(self, index: QModelIndex) -> CellRow: + cid = self.get_id(index) + if row := self._rows.get(cid): + if not self.block_updates and row.is_stale(self._last_refresh): # need to refresh - entry = self._build_cell_row(row) - self._row_cache[row] = entry - return entry - else: - # return entry, even if it's stale - return entry - elif self.block_updates: - # blank entry until we unblock - return CellRow(columns=[Cell(text="...")] * len(self.activeCols)) - else: - # missing entry, need to build - entry = self._build_cell_row(row) - self._row_cache[row] = entry - return entry + self._rows[cid] = self._fetch_row_from_backend(cid) + return self._rows[cid] + # return row, even if it's stale + return row + if self.block_updates: + # blank row until we unblock + return CellRow.placeholder(len(self.activeCols)) + # missing row, need to build + self._rows[cid] = self._fetch_row_from_backend(cid) + return self._rows[cid] - def _build_cell_row(self, row: int) -> CellRow: - if not (card := self._get_card_by_row(row)): - cell = Cell(text=tr(TR.BROWSING_ROW_DELETED)) - return CellRow(columns=[cell] * len(self.activeCols)) - - return CellRow( - columns=[ - Cell( - text=self._column_data(card, column_type), - font=self._font(card, column_type), - is_rtl=self._is_rtl(card, column_type), - ) - for column_type in self.activeCols - ], - # should probably make these an enum instead? - card_flag=card.user_flag(), - marked=card.note().has_tag(MARKED_TAG), - suspended=card.queue == QUEUE_TYPE_SUSPENDED, - ) + def _fetch_row_from_backend(self, cid: int) -> CellRow: + try: + return CellRow(*self.col.browser_row_for_card(cid)) + except: + return CellRow.deleted(len(self.activeCols)) # Model interface ###################################################################### @@ -206,13 +220,13 @@ class DataModel(QAbstractTableModel): if not index.isValid(): return if role == Qt.FontRole: - if font := self.get_cell(index).font: - qfont = QFont() - qfont.setFamily(font[0]) - qfont.setPixelSize(font[1]) - return qfont - else: - return None + if self.activeCols[index.column()] not in ("question", "answer", "noteFld"): + return + qfont = QFont() + row = self.get_row(index) + qfont.setFamily(row.font_name) + qfont.setPixelSize(row.font_size) + return qfont elif role == Qt.TextAlignmentRole: align: Union[Qt.AlignmentFlag, int] = Qt.AlignVCenter @@ -238,7 +252,7 @@ class DataModel(QAbstractTableModel): if orientation == Qt.Vertical: return None elif role == Qt.DisplayRole and section < len(self.activeCols): - type = self.columnType(section) + type = self.activeCols[section] txt = None for stype, name in self.browser.columns: if type == stype: @@ -291,8 +305,7 @@ class DataModel(QAbstractTableModel): self.browser.mw.progress.start() self.saveSelection() self.beginResetModel() - self.cardObjs = {} - self._row_cache = {} + self._rows = {} def endReset(self) -> None: self.endResetModel() @@ -366,8 +379,7 @@ class DataModel(QAbstractTableModel): def op_executed(self, op: OpChanges, focused: bool) -> None: print("op executed") if op.card or op.note or op.deck or op.notetype: - # clear card cache - self.cardObjs = {} + self._rows = {} if focused: self.redraw_cells() @@ -378,148 +390,6 @@ class DataModel(QAbstractTableModel): self.block_updates = False self.redraw_cells() - # Column data - ###################################################################### - - def columnType(self, column: int) -> str: - return self.activeCols[column] - - def time_format(self) -> str: - return "%Y-%m-%d" - - def _font(self, card: Card, column_type: str) -> Optional[Tuple[str, int]]: - if column_type not in ("question", "answer", "noteFld"): - return None - - template = card.template() - if not template.get("bfont"): - return None - - return ( - cast(str, template.get("bfont", "arial")), - cast(int, template.get("bsize", 12)), - ) - - # legacy - def columnData(self, index: QModelIndex) -> str: - col = index.column() - type = self.columnType(col) - c = self.getCard(index) - if not c: - return tr(TR.BROWSING_ROW_DELETED) - else: - return self._column_data(c, type) - - def _column_data(self, card: Card, column_type: str) -> str: - type = column_type - if type == "question": - return self.question(card) - elif type == "answer": - return self.answer(card) - elif type == "noteFld": - f = card.note() - return htmlToTextLine(f.fields[self.col.models.sortIdx(f.model())]) - elif type == "template": - t = card.template()["name"] - if card.model()["type"] == MODEL_CLOZE: - t = f"{t} {card.ord + 1}" - return cast(str, t) - elif type == "cardDue": - # catch invalid dates - try: - t = self._next_due(card) - except: - t = "" - if card.queue < 0: - t = f"({t})" - return t - elif type == "noteCrt": - return time.strftime( - self.time_format(), time.localtime(card.note().id / 1000) - ) - elif type == "noteMod": - return time.strftime(self.time_format(), time.localtime(card.note().mod)) - elif type == "cardMod": - return time.strftime(self.time_format(), time.localtime(card.mod)) - elif type == "cardReps": - return str(card.reps) - elif type == "cardLapses": - return str(card.lapses) - elif type == "noteTags": - return " ".join(card.note().tags) - elif type == "note": - return card.model()["name"] - elif type == "cardIvl": - if card.type == CARD_TYPE_NEW: - return tr(TR.BROWSING_NEW) - elif card.type == CARD_TYPE_LRN: - return tr(TR.BROWSING_LEARNING) - return self.col.format_timespan(card.ivl * 86400) - elif type == "cardEase": - if card.type == CARD_TYPE_NEW: - return tr(TR.BROWSING_NEW) - return "%d%%" % (card.factor / 10) - elif type == "deck": - if card.odid: - # in a cram deck - return "%s (%s)" % ( - self.browser.mw.col.decks.name(card.did), - self.browser.mw.col.decks.name(card.odid), - ) - # normal deck - return self.browser.mw.col.decks.name(card.did) - else: - return "" - - def question(self, c: Card) -> str: - return htmlToTextLine(c.q(browser=True)) - - def answer(self, c: Card) -> str: - if c.template().get("bafmt"): - # they have provided a template, use it verbatim - c.q(browser=True) - return htmlToTextLine(c.a()) - # need to strip question from answer - q = self.question(c) - a = htmlToTextLine(c.a()) - if a.startswith(q): - return a[len(q) :].strip() - return a - - # legacy - def nextDue(self, c: Card, index: QModelIndex) -> str: - return self._next_due(c) - - def _next_due(self, card: Card) -> str: - date: float - if card.odid: - return tr(TR.BROWSING_FILTERED) - elif card.queue == QUEUE_TYPE_LRN: - date = card.due - elif card.queue == QUEUE_TYPE_NEW or card.type == CARD_TYPE_NEW: - return tr(TR.STATISTICS_DUE_FOR_NEW_CARD, number=card.due) - elif card.queue in (QUEUE_TYPE_REV, QUEUE_TYPE_DAY_LEARN_RELEARN) or ( - card.type == CARD_TYPE_REV and card.queue < 0 - ): - date = time.time() + ((card.due - self.col.sched.today) * 86400) - else: - return "" - return time.strftime(self.time_format(), time.localtime(date)) - - # legacy - def isRTL(self, index: QModelIndex) -> bool: - col = index.column() - type = self.columnType(col) - c = self.getCard(index) - return self._is_rtl(c, type) - - def _is_rtl(self, card: Card, column_type: str) -> bool: - if column_type != "noteFld": - return False - - nt = card.note().model() - return nt["flds"][self.col.models.sortIdx(nt)]["rtl"] - # Line painter ###################################################################### @@ -534,27 +404,13 @@ class StatusDelegate(QItemDelegate): def paint( self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex ) -> None: - row = self.model.get_row(index.row()) - cell = row.columns[index.column()] - - if cell.is_rtl: + if self.model.get_cell(index).is_rtl: option.direction = Qt.RightToLeft - - if row.card_flag: - color = getattr(colors, f"FLAG{row.card_flag}_BG") - elif row.marked: - color = colors.MARKED_BG - elif row.suspended: - color = colors.SUSPENDED_BG - else: - color = None - - if color: - brush = QBrush(theme_manager.qcolor(color)) + if row_color := self.model.get_row(index).color: + brush = QBrush(theme_manager.qcolor(row_color)) painter.save() painter.fillRect(option.rect, brush) painter.restore() - return QItemDelegate.paint(self, painter, option, index) @@ -962,7 +818,7 @@ QTableView {{ gridline-color: {grid} }} show = self.model.cards and update == 1 idx = self.form.tableView.selectionModel().currentIndex() if idx.isValid(): - self.card = self.model.getCard(idx) + self.card = self.col.getCard(self.model.get_id(idx)) show = show and self.card is not None self.form.splitter.widget(1).setVisible(bool(show)) From a5be72742cb66ddf3e6c0c5a81763f12aa1cd110 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 20 Mar 2021 16:06:26 +0100 Subject: [PATCH 06/18] Add BrowserRow to ignored classes --- pylib/.pylintrc | 1 + pylib/anki/collection.py | 2 +- qt/.pylintrc | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pylib/.pylintrc b/pylib/.pylintrc index 53b937e12..e8b5122bd 100644 --- a/pylib/.pylintrc +++ b/pylib/.pylintrc @@ -4,6 +4,7 @@ persistent = no [TYPECHECK] ignored-classes= + BrowserRow, FormatTimespanIn, AnswerCardIn, UnburyCardsInCurrentDeckIn, diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 684b90bdb..7940e3adf 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -688,7 +688,7 @@ class Collection: def browser_row_for_card( self, cid: int - ) -> Tuple[Generator[Tuple[str, bool], None, None], BrowserRow.Color, str, int]: + ) -> Tuple[Generator[Tuple[str, bool], None, None], BrowserRow.Color.V, str, int]: row = self._backend.browser_row_for_card(cid) return ( ((cell.text, cell.is_rtl) for cell in row.cells), diff --git a/qt/.pylintrc b/qt/.pylintrc index 4a701207b..b4809e76d 100644 --- a/qt/.pylintrc +++ b/qt/.pylintrc @@ -6,6 +6,7 @@ ignore = forms,hooks_gen.py [TYPECHECK] ignored-modules=win32file,pywintypes,socket,win32pipe,winrt,pyaudio ignored-classes= + BrowserRow, SearchNode, Config, OpChanges From 7425aa6b5883e964851ff7158acf96c4481f6ef0 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 20 Mar 2021 17:20:49 +0100 Subject: [PATCH 07/18] Refactor search/browser.rs to browser_rows.rs --- rslib/backend.proto | 6 ++- rslib/src/backend/browser_rows.rs | 46 +++++++++++++++++++ rslib/src/backend/mod.rs | 5 ++ rslib/src/backend/search.rs | 43 +---------------- .../{search/browser.rs => browser_rows.rs} | 0 rslib/src/lib.rs | 1 + rslib/src/search/mod.rs | 1 - 7 files changed, 59 insertions(+), 43 deletions(-) create mode 100644 rslib/src/backend/browser_rows.rs rename rslib/src/{search/browser.rs => browser_rows.rs} (100%) diff --git a/rslib/backend.proto b/rslib/backend.proto index 00e2c6671..bc754c581 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -103,6 +103,7 @@ enum ServiceIndex { SERVICE_INDEX_I18N = 12; SERVICE_INDEX_COLLECTION = 13; SERVICE_INDEX_CARDS = 14; + SERVICE_INDEX_BROWSER_ROWS = 15; } service SchedulingService { @@ -234,7 +235,6 @@ service SearchService { rpc JoinSearchNodes(JoinSearchNodesIn) returns (String); rpc ReplaceSearchNode(ReplaceSearchNodeIn) returns (String); rpc FindAndReplace(FindAndReplaceIn) returns (OpChangesWithCount); - rpc BrowserRowForCard(CardID) returns (BrowserRow); } service StatsService { @@ -277,6 +277,10 @@ service CardsService { rpc SetFlag(SetFlagIn) returns (OpChanges); } +service BrowserRowsService { + rpc BrowserRowForCard(CardID) returns (BrowserRow); +} + // Protobuf stored in .anki2 files // These should be moved to a separate file in the future /////////////////////////////////////////////////////////// diff --git a/rslib/src/backend/browser_rows.rs b/rslib/src/backend/browser_rows.rs new file mode 100644 index 000000000..3c88c1398 --- /dev/null +++ b/rslib/src/backend/browser_rows.rs @@ -0,0 +1,46 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use super::Backend; +use crate::{backend_proto as pb, browser_rows, prelude::*}; +pub(super) use pb::browserrows_service::Service as BrowserRowsService; + +impl BrowserRowsService for Backend { + fn browser_row_for_card(&self, input: pb::CardId) -> Result { + self.with_col(|col| col.browser_row_for_card(input.cid.into()).map(Into::into)) + } +} + +impl From for pb::BrowserRow { + fn from(row: browser_rows::Row) -> Self { + pb::BrowserRow { + cells: row.cells.into_iter().map(Into::into).collect(), + color: row.color.into(), + font_name: row.font.name, + font_size: row.font.size, + } + } +} + +impl From for pb::browser_row::Cell { + fn from(cell: browser_rows::Cell) -> Self { + pb::browser_row::Cell { + text: cell.text, + is_rtl: cell.is_rtl, + } + } +} + +impl From for i32 { + fn from(color: browser_rows::RowColor) -> Self { + match color { + browser_rows::RowColor::Default => pb::browser_row::Color::Default as i32, + browser_rows::RowColor::Marked => pb::browser_row::Color::Marked as i32, + browser_rows::RowColor::Suspended => pb::browser_row::Color::Suspended as i32, + browser_rows::RowColor::FlagRed => pb::browser_row::Color::FlagRed as i32, + browser_rows::RowColor::FlagOrange => pb::browser_row::Color::FlagOrange as i32, + browser_rows::RowColor::FlagGreen => pb::browser_row::Color::FlagGreen as i32, + browser_rows::RowColor::FlagBlue => pb::browser_row::Color::FlagBlue as i32, + } + } +} diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 7c1521260..aacdbc8d1 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -2,6 +2,7 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html mod adding; +mod browser_rows; mod card; mod cardrendering; mod collection; @@ -24,6 +25,7 @@ mod sync; mod tags; use self::{ + browser_rows::BrowserRowsService, card::CardsService, cardrendering::CardRenderingService, collection::CollectionService, @@ -137,6 +139,9 @@ impl Backend { pb::ServiceIndex::I18n => I18nService::run_method(self, method, input), pb::ServiceIndex::Collection => CollectionService::run_method(self, method, input), pb::ServiceIndex::Cards => CardsService::run_method(self, method, input), + pb::ServiceIndex::BrowserRows => { + BrowserRowsService::run_method(self, method, input) + } }) .map_err(|err| { let backend_err = anki_error_to_proto_error(err, &self.i18n); diff --git a/rslib/src/backend/search.rs b/rslib/src/backend/search.rs index b6ffde8e1..ed4c7d497 100644 --- a/rslib/src/backend/search.rs +++ b/rslib/src/backend/search.rs @@ -13,9 +13,8 @@ use crate::{ config::SortKind, prelude::*, search::{ - browser, concatenate_searches, parse_search, replace_search_node, write_nodes, - BoolSeparator, Node, PropertyKind, RatingKind, SearchNode, SortMode, StateKind, - TemplateKind, + concatenate_searches, parse_search, replace_search_node, write_nodes, BoolSeparator, Node, + PropertyKind, RatingKind, SearchNode, SortMode, StateKind, TemplateKind, }, text::escape_anki_wildcards, }; @@ -90,10 +89,6 @@ impl SearchService for Backend { .map(Into::into) }) } - - fn browser_row_for_card(&self, input: pb::CardId) -> Result { - self.with_col(|col| col.browser_row_for_card(input.cid.into()).map(Into::into)) - } } impl TryFrom for Node { @@ -269,37 +264,3 @@ impl From> for SortMode { } } } - -impl From for pb::BrowserRow { - fn from(row: browser::Row) -> Self { - pb::BrowserRow { - cells: row.cells.into_iter().map(Into::into).collect(), - color: row.color.into(), - font_name: row.font.name, - font_size: row.font.size, - } - } -} - -impl From for pb::browser_row::Cell { - fn from(cell: browser::Cell) -> Self { - pb::browser_row::Cell { - text: cell.text, - is_rtl: cell.is_rtl, - } - } -} - -impl From for i32 { - fn from(color: browser::RowColor) -> Self { - match color { - browser::RowColor::Default => pb::browser_row::Color::Default as i32, - browser::RowColor::Marked => pb::browser_row::Color::Marked as i32, - browser::RowColor::Suspended => pb::browser_row::Color::Suspended as i32, - browser::RowColor::FlagRed => pb::browser_row::Color::FlagRed as i32, - browser::RowColor::FlagOrange => pb::browser_row::Color::FlagOrange as i32, - browser::RowColor::FlagGreen => pb::browser_row::Color::FlagGreen as i32, - browser::RowColor::FlagBlue => pb::browser_row::Color::FlagBlue as i32, - } - } -} diff --git a/rslib/src/search/browser.rs b/rslib/src/browser_rows.rs similarity index 100% rename from rslib/src/search/browser.rs rename to rslib/src/browser_rows.rs diff --git a/rslib/src/lib.rs b/rslib/src/lib.rs index bcbf847f2..8994c8b7b 100644 --- a/rslib/src/lib.rs +++ b/rslib/src/lib.rs @@ -6,6 +6,7 @@ pub mod adding; pub mod backend; mod backend_proto; +pub mod browser_rows; pub mod card; pub mod cloze; pub mod collection; diff --git a/rslib/src/search/mod.rs b/rslib/src/search/mod.rs index fa929cfe8..b469542df 100644 --- a/rslib/src/search/mod.rs +++ b/rslib/src/search/mod.rs @@ -1,7 +1,6 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -pub mod browser; mod cards; mod notes; mod parser; From b86d683f175e4a0c27af86fb79005abc32c55f79 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 20 Mar 2021 17:26:30 +0100 Subject: [PATCH 08/18] Rename render_card_inner() to render_card() --- rslib/src/browser_rows.rs | 2 +- rslib/src/notetype/render.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rslib/src/browser_rows.rs b/rslib/src/browser_rows.rs index 9f4d6bc9a..3d9833381 100644 --- a/rslib/src/browser_rows.rs +++ b/rslib/src/browser_rows.rs @@ -107,7 +107,7 @@ fn row_context_from_cid<'a>( let offset = col.local_utc_offset_for_user()?; let timing = col.timing_today()?; let (question_nodes, answer_nodes) = if with_card_render { - let render = col.render_card_inner( + let render = col.render_card( ¬e, &card, ¬etype, diff --git a/rslib/src/notetype/render.rs b/rslib/src/notetype/render.rs index a480183ff..c73611e5c 100644 --- a/rslib/src/notetype/render.rs +++ b/rslib/src/notetype/render.rs @@ -37,7 +37,7 @@ impl Collection { } .ok_or_else(|| AnkiError::invalid_input("missing template"))?; - self.render_card_inner(¬e, &card, &nt, template, browser) + self.render_card(¬e, &card, &nt, template, browser) } /// Render a card that may not yet have been added. @@ -59,7 +59,7 @@ impl Collection { fill_empty_fields(note, &template.config.q_format, &nt, &self.i18n); } - self.render_card_inner(note, &card, &nt, template, false) + self.render_card(note, &card, &nt, template, false) } fn existing_or_synthesized_card( @@ -82,7 +82,7 @@ impl Collection { }) } - pub fn render_card_inner( + pub fn render_card( &mut self, note: &Note, card: &Card, From 04ae6f727b9a9c73f7a2180d78c5d159a9e71b31 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 20 Mar 2021 17:31:16 +0100 Subject: [PATCH 09/18] Rename browser_rows/RowColor to Color --- rslib/src/backend/browser_rows.rs | 18 +++++++++--------- rslib/src/browser_rows.rs | 20 ++++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/rslib/src/backend/browser_rows.rs b/rslib/src/backend/browser_rows.rs index 3c88c1398..42ddb0967 100644 --- a/rslib/src/backend/browser_rows.rs +++ b/rslib/src/backend/browser_rows.rs @@ -31,16 +31,16 @@ impl From for pb::browser_row::Cell { } } -impl From for i32 { - fn from(color: browser_rows::RowColor) -> Self { +impl From for i32 { + fn from(color: browser_rows::Color) -> Self { match color { - browser_rows::RowColor::Default => pb::browser_row::Color::Default as i32, - browser_rows::RowColor::Marked => pb::browser_row::Color::Marked as i32, - browser_rows::RowColor::Suspended => pb::browser_row::Color::Suspended as i32, - browser_rows::RowColor::FlagRed => pb::browser_row::Color::FlagRed as i32, - browser_rows::RowColor::FlagOrange => pb::browser_row::Color::FlagOrange as i32, - browser_rows::RowColor::FlagGreen => pb::browser_row::Color::FlagGreen as i32, - browser_rows::RowColor::FlagBlue => pb::browser_row::Color::FlagBlue as i32, + browser_rows::Color::Default => pb::browser_row::Color::Default as i32, + browser_rows::Color::Marked => pb::browser_row::Color::Marked as i32, + browser_rows::Color::Suspended => pb::browser_row::Color::Suspended as i32, + browser_rows::Color::FlagRed => pb::browser_row::Color::FlagRed as i32, + browser_rows::Color::FlagOrange => pb::browser_row::Color::FlagOrange as i32, + browser_rows::Color::FlagGreen => pb::browser_row::Color::FlagGreen as i32, + browser_rows::Color::FlagBlue => pb::browser_row::Color::FlagBlue as i32, } } } diff --git a/rslib/src/browser_rows.rs b/rslib/src/browser_rows.rs index 3d9833381..c6f3e0e58 100644 --- a/rslib/src/browser_rows.rs +++ b/rslib/src/browser_rows.rs @@ -23,7 +23,7 @@ use crate::{ #[derive(Debug, PartialEq)] pub struct Row { pub cells: Vec, - pub color: RowColor, + pub color: Color, pub font: Font, } @@ -34,7 +34,7 @@ pub struct Cell { } #[derive(Debug, PartialEq)] -pub enum RowColor { +pub enum Color { Default, Marked, Suspended, @@ -296,12 +296,12 @@ fn get_is_rtl(column: &str, context: &RowContext) -> bool { } } -fn get_row_color(context: &RowContext) -> RowColor { +fn get_row_color(context: &RowContext) -> Color { match context.card.flags { - 1 => RowColor::FlagRed, - 2 => RowColor::FlagOrange, - 3 => RowColor::FlagGreen, - 4 => RowColor::FlagBlue, + 1 => Color::FlagRed, + 2 => Color::FlagOrange, + 3 => Color::FlagGreen, + 4 => Color::FlagBlue, _ => { if context .note @@ -309,11 +309,11 @@ fn get_row_color(context: &RowContext) -> RowColor { .iter() .any(|tag| tag.eq_ignore_ascii_case("marked")) { - RowColor::Marked + Color::Marked } else if context.card.queue == CardQueue::Suspended { - RowColor::Suspended + Color::Suspended } else { - RowColor::Default + Color::Default } } } From 1ab0d4dff8a756f1fdfa2c6baff6a1f0e25f7023 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 20 Mar 2021 18:02:41 +0100 Subject: [PATCH 10/18] Refactor browser_rows.rs * Make RowContext taking functions methods * Make RowContext constructor a method * Rename 'with_fields' to 'card_render' --- rslib/src/browser_rows.rs | 453 +++++++++++++++++++------------------- 1 file changed, 225 insertions(+), 228 deletions(-) diff --git a/rslib/src/browser_rows.rs b/rslib/src/browser_rows.rs index c6f3e0e58..d526e0041 100644 --- a/rslib/src/browser_rows.rs +++ b/rslib/src/browser_rows.rs @@ -20,6 +20,8 @@ use crate::{ timestamp::{TimestampMillis, TimestampSecs}, }; +const CARD_RENDER_COLUMNS: [&str; 2] = ["question", "answer"]; + #[derive(Debug, PartialEq)] pub struct Row { pub cells: Vec, @@ -64,7 +66,77 @@ struct RowContext<'a> { answer_nodes: Option>, } -impl RowContext<'_> { +fn card_render_required(columns: &[String]) -> bool { + columns + .iter() + .any(|c| CARD_RENDER_COLUMNS.contains(&c.as_str())) +} + +impl Collection { + pub fn browser_row_for_card(&mut self, id: CardID) -> Result { + let columns: Vec = self.get_config_optional("activeCols").unwrap_or_else(|| { + vec![ + "noteFld".to_string(), + "template".to_string(), + "cardDue".to_string(), + "deck".to_string(), + ] + }); + let mut context = RowContext::new(self, id, card_render_required(&columns))?; + + Ok(Row { + cells: columns + .iter() + .map(|column| context.get_cell(column)) + .collect::>()?, + color: context.get_row_color(), + font: context.get_row_font()?, + }) + } +} + +impl<'a> RowContext<'a> { + fn new(col: &'a mut Collection, id: CardID, with_card_render: bool) -> Result { + let card = col.storage.get_card(id)?.ok_or(AnkiError::NotFound)?; + let note = if with_card_render { + col.storage.get_note(card.note_id)? + } else { + col.storage.get_note_without_fields(card.note_id)? + } + .ok_or(AnkiError::NotFound)?; + let notetype = col + .get_notetype(note.notetype_id)? + .ok_or(AnkiError::NotFound)?; + let offset = col.local_utc_offset_for_user()?; + let timing = col.timing_today()?; + let (question_nodes, answer_nodes) = if with_card_render { + let render = col.render_card( + ¬e, + &card, + ¬etype, + notetype.get_template(card.template_idx)?, + true, + )?; + (Some(render.qnodes), Some(render.anodes)) + } else { + (None, None) + }; + + Ok(RowContext { + col, + card, + note, + notetype, + deck: None, + original_deck: None, + i18n: &col.i18n, + offset, + timing, + question_nodes, + answer_nodes, + }) + } + fn template(&self) -> Result<&CardTemplate> { self.notetype.get_template(self.card.template_idx) } @@ -87,249 +159,174 @@ impl RowContext<'_> { } Ok(self.original_deck.as_ref().unwrap()) } -} -fn row_context_from_cid<'a>( - col: &'a mut Collection, - id: CardID, - with_card_render: bool, -) -> Result> { - let card = col.storage.get_card(id)?.ok_or(AnkiError::NotFound)?; - let note = if with_card_render { - col.storage.get_note(card.note_id)? - } else { - col.storage.get_note_without_fields(card.note_id)? - } - .ok_or(AnkiError::NotFound)?; - let notetype = col - .get_notetype(note.notetype_id)? - .ok_or(AnkiError::NotFound)?; - let offset = col.local_utc_offset_for_user()?; - let timing = col.timing_today()?; - let (question_nodes, answer_nodes) = if with_card_render { - let render = col.render_card( - ¬e, - &card, - ¬etype, - notetype.get_template(card.template_idx)?, - true, - )?; - (Some(render.qnodes), Some(render.anodes)) - } else { - (None, None) - }; - - Ok(RowContext { - col, - card, - note, - notetype, - deck: None, - original_deck: None, - i18n: &col.i18n, - offset, - timing, - question_nodes, - answer_nodes, - }) -} - -impl Collection { - pub fn browser_row_for_card(&mut self, id: CardID) -> Result { - let columns: Vec = self.get_config_optional("activeCols").unwrap_or_else(|| { - vec![ - "noteFld".to_string(), - "template".to_string(), - "cardDue".to_string(), - "deck".to_string(), - ] - }); - let mut context = row_context_from_cid(self, id, note_fields_required(&columns))?; - Ok(Row { - cells: columns - .iter() - .map(|column| get_cell(column, &mut context)) - .collect::>()?, - color: get_row_color(&context), - font: get_row_font(&context)?, + fn get_cell(&mut self, column: &str) -> Result { + Ok(Cell { + text: self.get_cell_text(column)?, + is_rtl: self.get_is_rtl(column), }) } -} -fn get_cell(column: &str, context: &mut RowContext) -> Result { - Ok(Cell { - text: get_cell_text(column, context)?, - is_rtl: get_is_rtl(column, context), - }) -} - -fn get_cell_text(column: &str, context: &mut RowContext) -> Result { - Ok(match column { - "answer" => answer_str(context)?, - "cardDue" => card_due_str(context)?, - "cardEase" => card_ease_str(context), - "cardIvl" => card_interval_str(context), - "cardLapses" => context.card.lapses.to_string(), - "cardMod" => context.card.mtime.date_string(context.offset), - "cardReps" => context.card.reps.to_string(), - "deck" => deck_str(context)?, - "note" => context.notetype.name.to_owned(), - "noteCrt" => note_creation_str(context), - "noteFld" => note_field_str(context), - "noteMod" => context.note.mtime.date_string(context.offset), - "noteTags" => context.note.tags.join(" "), - "question" => question_str(context)?, - "template" => template_str(context)?, - _ => "".to_string(), - }) -} - -fn answer_str(context: &RowContext) -> Result { - let text = context - .answer_nodes - .as_ref() - .unwrap() - .iter() - .map(|node| match node { - RenderedNode::Text { text } => text, - RenderedNode::Replacement { - field_name: _, - current_text, - filters: _, - } => current_text, + fn get_cell_text(&mut self, column: &str) -> Result { + Ok(match column { + "answer" => self.answer_str()?, + "cardDue" => self.card_due_str()?, + "cardEase" => self.card_ease_str(), + "cardIvl" => self.card_interval_str(), + "cardLapses" => self.card.lapses.to_string(), + "cardMod" => self.card.mtime.date_string(self.offset), + "cardReps" => self.card.reps.to_string(), + "deck" => self.deck_str()?, + "note" => self.notetype.name.to_owned(), + "noteCrt" => self.note_creation_str(), + "noteFld" => self.note_field_str(), + "noteMod" => self.note.mtime.date_string(self.offset), + "noteTags" => self.note.tags.join(" "), + "question" => self.question_str()?, + "template" => self.template_str()?, + _ => "".to_string(), }) - .join(""); - Ok(html_to_text_line(&extract_av_tags(&text, false).0).to_string()) -} - -fn card_due_str(context: &mut RowContext) -> Result { - Ok(if context.original_deck()?.is_some() { - context.i18n.tr(TR::BrowsingFiltered).into() - } else if context.card.queue == CardQueue::New || context.card.ctype == CardType::New { - context.i18n.trn( - TR::StatisticsDueForNewCard, - tr_args!["number"=>context.card.due], - ) - } else { - let date = match context.card.queue { - CardQueue::Learn => TimestampSecs(context.card.due as i64), - CardQueue::DayLearn | CardQueue::Review => TimestampSecs::now().adding_secs( - ((context.card.due - context.timing.days_elapsed as i32) * 86400) as i64, - ), - _ => return Ok("".into()), - }; - date.date_string(context.offset) - }) -} - -fn card_ease_str(context: &RowContext) -> String { - match context.card.ctype { - CardType::New => context.i18n.tr(TR::BrowsingNew).into(), - _ => format!("{}%", context.card.ease_factor / 10), } -} -fn card_interval_str(context: &RowContext) -> String { - match context.card.ctype { - CardType::New => context.i18n.tr(TR::BrowsingNew).into(), - CardType::Learn => context.i18n.tr(TR::BrowsingLearning).into(), - _ => time_span((context.card.interval * 86400) as f32, context.i18n, false), + fn answer_str(&self) -> Result { + let text = self + .answer_nodes + .as_ref() + .unwrap() + .iter() + .map(|node| match node { + RenderedNode::Text { text } => text, + RenderedNode::Replacement { + field_name: _, + current_text, + filters: _, + } => current_text, + }) + .join(""); + Ok(html_to_text_line(&extract_av_tags(&text, false).0).to_string()) } -} -fn deck_str(context: &mut RowContext) -> Result { - let deck_name = context.deck()?.human_name(); - Ok(if let Some(original_deck) = context.original_deck()? { - format!("{} ({})", &deck_name, &original_deck.human_name()) - } else { - deck_name - }) -} - -fn note_creation_str(context: &RowContext) -> String { - TimestampMillis(context.note.id.into()) - .as_secs() - .date_string(context.offset) -} - -fn note_field_str(context: &RowContext) -> String { - if let Some(field) = &context.note.sort_field { - field.to_owned() - } else { - "".to_string() - } -} - -fn template_str(context: &RowContext) -> Result { - let name = &context.template()?.name; - Ok(match context.notetype.config.kind() { - NoteTypeKind::Normal => name.to_owned(), - NoteTypeKind::Cloze => format!("{} {}", name, context.card.template_idx + 1), - }) -} - -fn question_str(context: &RowContext) -> Result { - let text = context - .question_nodes - .as_ref() - .unwrap() - .iter() - .map(|node| match node { - RenderedNode::Text { text } => text, - RenderedNode::Replacement { - field_name: _, - current_text, - filters: _, - } => current_text, + fn card_due_str(&mut self) -> Result { + Ok(if self.original_deck()?.is_some() { + self.i18n.tr(TR::BrowsingFiltered).into() + } else if self.card.queue == CardQueue::New || self.card.ctype == CardType::New { + self.i18n.trn( + TR::StatisticsDueForNewCard, + tr_args!["number"=>self.card.due], + ) + } else { + let date = match self.card.queue { + CardQueue::Learn => TimestampSecs(self.card.due as i64), + CardQueue::DayLearn | CardQueue::Review => TimestampSecs::now().adding_secs( + ((self.card.due - self.timing.days_elapsed as i32) * 86400) as i64, + ), + _ => return Ok("".into()), + }; + date.date_string(self.offset) }) - .join(""); - Ok(html_to_text_line(&extract_av_tags(&text, true).0).to_string()) -} + } -fn get_is_rtl(column: &str, context: &RowContext) -> bool { - match column { - "noteFld" => { - let index = context.notetype.config.sort_field_idx as usize; - context.notetype.fields[index].config.rtl + fn card_ease_str(&self) -> String { + match self.card.ctype { + CardType::New => self.i18n.tr(TR::BrowsingNew).into(), + _ => format!("{}%", self.card.ease_factor / 10), } - _ => false, } -} -fn get_row_color(context: &RowContext) -> Color { - match context.card.flags { - 1 => Color::FlagRed, - 2 => Color::FlagOrange, - 3 => Color::FlagGreen, - 4 => Color::FlagBlue, - _ => { - if context - .note - .tags - .iter() - .any(|tag| tag.eq_ignore_ascii_case("marked")) - { - Color::Marked - } else if context.card.queue == CardQueue::Suspended { - Color::Suspended - } else { - Color::Default + fn card_interval_str(&self) -> String { + match self.card.ctype { + CardType::New => self.i18n.tr(TR::BrowsingNew).into(), + CardType::Learn => self.i18n.tr(TR::BrowsingLearning).into(), + _ => time_span((self.card.interval * 86400) as f32, self.i18n, false), + } + } + + fn deck_str(&mut self) -> Result { + let deck_name = self.deck()?.human_name(); + Ok(if let Some(original_deck) = self.original_deck()? { + format!("{} ({})", &deck_name, &original_deck.human_name()) + } else { + deck_name + }) + } + + fn note_creation_str(&self) -> String { + TimestampMillis(self.note.id.into()) + .as_secs() + .date_string(self.offset) + } + + fn note_field_str(&self) -> String { + if let Some(field) = &self.note.sort_field { + field.to_owned() + } else { + "".to_string() + } + } + + fn template_str(&self) -> Result { + let name = &self.template()?.name; + Ok(match self.notetype.config.kind() { + NoteTypeKind::Normal => name.to_owned(), + NoteTypeKind::Cloze => format!("{} {}", name, self.card.template_idx + 1), + }) + } + + fn question_str(&self) -> Result { + let text = self + .question_nodes + .as_ref() + .unwrap() + .iter() + .map(|node| match node { + RenderedNode::Text { text } => text, + RenderedNode::Replacement { + field_name: _, + current_text, + filters: _, + } => current_text, + }) + .join(""); + Ok(html_to_text_line(&extract_av_tags(&text, true).0).to_string()) + } + + fn get_is_rtl(&self, column: &str) -> bool { + match column { + "noteFld" => { + let index = self.notetype.config.sort_field_idx as usize; + self.notetype.fields[index].config.rtl + } + _ => false, + } + } + + fn get_row_color(&self) -> Color { + match self.card.flags { + 1 => Color::FlagRed, + 2 => Color::FlagOrange, + 3 => Color::FlagGreen, + 4 => Color::FlagBlue, + _ => { + if self + .note + .tags + .iter() + .any(|tag| tag.eq_ignore_ascii_case("marked")) + { + Color::Marked + } else if self.card.queue == CardQueue::Suspended { + Color::Suspended + } else { + Color::Default + } } } } -} -fn get_row_font(context: &RowContext) -> Result { - Ok(Font { - name: context.template()?.config.browser_font_name.to_owned(), - size: context.template()?.config.browser_font_size, - }) -} - -const FIELDS_REQUIRING_COLUMNS: [&str; 2] = ["question", "answer"]; - -fn note_fields_required(columns: &[String]) -> bool { - columns - .iter() - .any(|c| FIELDS_REQUIRING_COLUMNS.contains(&c.as_str())) + fn get_row_font(&self) -> Result { + Ok(Font { + name: self.template()?.config.browser_font_name.to_owned(), + size: self.template()?.config.browser_font_size, + }) + } } From af90bbf8799a34bfc5ad9b59bb0c1a99747c6157 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sat, 20 Mar 2021 18:12:00 +0100 Subject: [PATCH 11/18] Check original_deck_id rather than original_deck() in card_due_str() as we don't necessarily have to load that deck. --- rslib/src/browser_rows.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rslib/src/browser_rows.rs b/rslib/src/browser_rows.rs index d526e0041..623f076fd 100644 --- a/rslib/src/browser_rows.rs +++ b/rslib/src/browser_rows.rs @@ -11,7 +11,7 @@ use crate::i18n::{tr_args, I18n, TR}; use crate::{ card::{Card, CardID, CardQueue, CardType}, collection::Collection, - decks::Deck, + decks::{Deck, DeckID}, notes::Note, notetype::{CardTemplate, NoteType, NoteTypeKind}, scheduler::{timespan::time_span, timing::SchedTimingToday}, @@ -207,7 +207,7 @@ impl<'a> RowContext<'a> { } fn card_due_str(&mut self) -> Result { - Ok(if self.original_deck()?.is_some() { + Ok(if self.card.original_deck_id != DeckID(0) { self.i18n.tr(TR::BrowsingFiltered).into() } else if self.card.queue == CardQueue::New || self.card.ctype == CardType::New { self.i18n.trn( From a27308dc9c0ad1c8a7f079d5b01d8ba70231d852 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sun, 21 Mar 2021 18:44:31 +0100 Subject: [PATCH 12/18] Readd browser.model.getCard() Actually, the new model has no truck with card objects, but since it may hold an invalid id, it takes responsibility for catching the exception. --- qt/aqt/browser.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 2c03b23ce..1ffe53979 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -203,6 +203,13 @@ class DataModel(QAbstractTableModel): except: return CellRow.deleted(len(self.activeCols)) + def getCard(self, index: QModelIndex) -> Optional[Card]: + """Try to return the indicated, possibly deleted card.""" + try: + return self.col.getCard(self.get_id(index)) + except: + return None + # Model interface ###################################################################### @@ -818,7 +825,7 @@ QTableView {{ gridline-color: {grid} }} show = self.model.cards and update == 1 idx = self.form.tableView.selectionModel().currentIndex() if idx.isValid(): - self.card = self.col.getCard(self.model.get_id(idx)) + self.card = self.model.getCard(idx) show = show and self.card is not None self.form.splitter.widget(1).setVisible(bool(show)) From c91182b248e6be34183d7893e3a541cc64c99bd2 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sun, 21 Mar 2021 18:46:04 +0100 Subject: [PATCH 13/18] Strip question from answer string --- rslib/src/browser_rows.rs | 94 ++++++++++++++++++++++++--------------- 1 file changed, 58 insertions(+), 36 deletions(-) diff --git a/rslib/src/browser_rows.rs b/rslib/src/browser_rows.rs index 623f076fd..fddc41919 100644 --- a/rslib/src/browser_rows.rs +++ b/rslib/src/browser_rows.rs @@ -62,8 +62,14 @@ struct RowContext<'a> { i18n: &'a I18n, offset: FixedOffset, timing: SchedTimingToday, - question_nodes: Option>, - answer_nodes: Option>, + render_context: Option, +} + +/// The answer string needs the question string but not the other way around, so only build the +/// answer string when needed. +struct RenderContext { + question: String, + answer_nodes: Vec, } fn card_render_required(columns: &[String]) -> bool { @@ -95,6 +101,36 @@ impl Collection { } } +impl RenderContext { + fn new(col: &mut Collection, card: &Card, note: &Note, notetype: &NoteType) -> Result { + let render = col.render_card( + note, + card, + notetype, + notetype.get_template(card.template_idx)?, + true, + )?; + let qnodes_text = render + .qnodes + .iter() + .map(|node| match node { + RenderedNode::Text { text } => text, + RenderedNode::Replacement { + field_name: _, + current_text, + filters: _, + } => current_text, + }) + .join(""); + let question = extract_av_tags(&qnodes_text, true).0.to_string(); + + Ok(RenderContext { + question, + answer_nodes: render.anodes, + }) + } +} + impl<'a> RowContext<'a> { fn new(col: &'a mut Collection, id: CardID, with_card_render: bool) -> Result { let card = col.storage.get_card(id)?.ok_or(AnkiError::NotFound)?; @@ -109,17 +145,10 @@ impl<'a> RowContext<'a> { .ok_or(AnkiError::NotFound)?; let offset = col.local_utc_offset_for_user()?; let timing = col.timing_today()?; - let (question_nodes, answer_nodes) = if with_card_render { - let render = col.render_card( - ¬e, - &card, - ¬etype, - notetype.get_template(card.template_idx)?, - true, - )?; - (Some(render.qnodes), Some(render.anodes)) + let render_context = if with_card_render { + Some(RenderContext::new(col, &card, ¬e, ¬etype)?) } else { - (None, None) + None }; Ok(RowContext { @@ -132,8 +161,7 @@ impl<'a> RowContext<'a> { i18n: &col.i18n, offset, timing, - question_nodes, - answer_nodes, + render_context, }) } @@ -169,6 +197,7 @@ impl<'a> RowContext<'a> { fn get_cell_text(&mut self, column: &str) -> Result { Ok(match column { + "answer" => self.answer_str(), "answer" => self.answer_str()?, "cardDue" => self.card_due_str()?, "cardEase" => self.card_ease_str(), @@ -182,17 +211,16 @@ impl<'a> RowContext<'a> { "noteFld" => self.note_field_str(), "noteMod" => self.note.mtime.date_string(self.offset), "noteTags" => self.note.tags.join(" "), - "question" => self.question_str()?, + "question" => self.question_str(), "template" => self.template_str()?, _ => "".to_string(), }) } - fn answer_str(&self) -> Result { - let text = self + fn answer_str(&self) -> String { + let render_context = self.render_context.as_ref().unwrap(); + let answer = render_context .answer_nodes - .as_ref() - .unwrap() .iter() .map(|node| match node { RenderedNode::Text { text } => text, @@ -203,7 +231,15 @@ impl<'a> RowContext<'a> { } => current_text, }) .join(""); - Ok(html_to_text_line(&extract_av_tags(&text, false).0).to_string()) + let answer = extract_av_tags(&answer, false).0; + html_to_text_line( + if let Some(stripped) = answer.strip_prefix(&render_context.question) { + stripped + } else { + &answer + }, + ) + .to_string() } fn card_due_str(&mut self) -> Result { @@ -272,22 +308,8 @@ impl<'a> RowContext<'a> { }) } - fn question_str(&self) -> Result { - let text = self - .question_nodes - .as_ref() - .unwrap() - .iter() - .map(|node| match node { - RenderedNode::Text { text } => text, - RenderedNode::Replacement { - field_name: _, - current_text, - filters: _, - } => current_text, - }) - .join(""); - Ok(html_to_text_line(&extract_av_tags(&text, true).0).to_string()) + fn question_str(&self) -> String { + html_to_text_line(&self.render_context.as_ref().unwrap().question).to_string() } fn get_is_rtl(&self, column: &str) -> bool { From 255daad820194e44ca59ee6373860c49cfc937cf Mon Sep 17 00:00:00 2001 From: RumovZ Date: Sun, 21 Mar 2021 21:18:56 +0100 Subject: [PATCH 14/18] Fix card_due_str() --- rslib/src/browser_rows.rs | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/rslib/src/browser_rows.rs b/rslib/src/browser_rows.rs index fddc41919..fb941f021 100644 --- a/rslib/src/browser_rows.rs +++ b/rslib/src/browser_rows.rs @@ -198,8 +198,7 @@ impl<'a> RowContext<'a> { fn get_cell_text(&mut self, column: &str) -> Result { Ok(match column { "answer" => self.answer_str(), - "answer" => self.answer_str()?, - "cardDue" => self.card_due_str()?, + "cardDue" => self.card_due_str(), "cardEase" => self.card_ease_str(), "cardIvl" => self.card_interval_str(), "cardLapses" => self.card.lapses.to_string(), @@ -242,8 +241,8 @@ impl<'a> RowContext<'a> { .to_string() } - fn card_due_str(&mut self) -> Result { - Ok(if self.card.original_deck_id != DeckID(0) { + fn card_due_str(&mut self) -> String { + let due = if self.card.original_deck_id != DeckID(0) { self.i18n.tr(TR::BrowsingFiltered).into() } else if self.card.queue == CardQueue::New || self.card.ctype == CardType::New { self.i18n.trn( @@ -251,15 +250,24 @@ impl<'a> RowContext<'a> { tr_args!["number"=>self.card.due], ) } else { - let date = match self.card.queue { - CardQueue::Learn => TimestampSecs(self.card.due as i64), - CardQueue::DayLearn | CardQueue::Review => TimestampSecs::now().adding_secs( - ((self.card.due - self.timing.days_elapsed as i32) * 86400) as i64, - ), - _ => return Ok("".into()), + let date = if self.card.queue == CardQueue::Learn { + TimestampSecs(self.card.due as i64) + } else if self.card.queue == CardQueue::DayLearn + || self.card.queue == CardQueue::Review + || (self.card.ctype == CardType::Review && (self.card.queue as i8) < 0) + { + TimestampSecs::now() + .adding_secs(((self.card.due - self.timing.days_elapsed as i32) * 86400) as i64) + } else { + return "".into(); }; date.date_string(self.offset) - }) + }; + if (self.card.queue as i8) < 0 { + format!("({})", due) + } else { + due + } } fn card_ease_str(&self) -> String { From de73232f0df7ff516f96621ec11385a24fb8d19e Mon Sep 17 00:00:00 2001 From: RumovZ Date: Mon, 22 Mar 2021 08:50:54 +0100 Subject: [PATCH 15/18] Fix date_string using FixedOffset instead of Local --- rslib/src/browser_rows.rs | 14 ++++---------- rslib/src/stats/card.rs | 18 +++++++----------- rslib/src/timestamp.rs | 4 ++-- 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/rslib/src/browser_rows.rs b/rslib/src/browser_rows.rs index fb941f021..9d597a81d 100644 --- a/rslib/src/browser_rows.rs +++ b/rslib/src/browser_rows.rs @@ -3,7 +3,6 @@ use std::sync::Arc; -use chrono::prelude::*; use itertools::Itertools; use crate::err::{AnkiError, Result}; @@ -60,7 +59,6 @@ struct RowContext<'a> { deck: Option, original_deck: Option>, i18n: &'a I18n, - offset: FixedOffset, timing: SchedTimingToday, render_context: Option, } @@ -143,7 +141,6 @@ impl<'a> RowContext<'a> { let notetype = col .get_notetype(note.notetype_id)? .ok_or(AnkiError::NotFound)?; - let offset = col.local_utc_offset_for_user()?; let timing = col.timing_today()?; let render_context = if with_card_render { Some(RenderContext::new(col, &card, ¬e, ¬etype)?) @@ -159,7 +156,6 @@ impl<'a> RowContext<'a> { deck: None, original_deck: None, i18n: &col.i18n, - offset, timing, render_context, }) @@ -202,13 +198,13 @@ impl<'a> RowContext<'a> { "cardEase" => self.card_ease_str(), "cardIvl" => self.card_interval_str(), "cardLapses" => self.card.lapses.to_string(), - "cardMod" => self.card.mtime.date_string(self.offset), + "cardMod" => self.card.mtime.date_string(), "cardReps" => self.card.reps.to_string(), "deck" => self.deck_str()?, "note" => self.notetype.name.to_owned(), "noteCrt" => self.note_creation_str(), "noteFld" => self.note_field_str(), - "noteMod" => self.note.mtime.date_string(self.offset), + "noteMod" => self.note.mtime.date_string(), "noteTags" => self.note.tags.join(" "), "question" => self.question_str(), "template" => self.template_str()?, @@ -261,7 +257,7 @@ impl<'a> RowContext<'a> { } else { return "".into(); }; - date.date_string(self.offset) + date.date_string() }; if (self.card.queue as i8) < 0 { format!("({})", due) @@ -295,9 +291,7 @@ impl<'a> RowContext<'a> { } fn note_creation_str(&self) -> String { - TimestampMillis(self.note.id.into()) - .as_secs() - .date_string(self.offset) + TimestampMillis(self.note.id.into()).as_secs().date_string() } fn note_field_str(&self) -> String { diff --git a/rslib/src/stats/card.rs b/rslib/src/stats/card.rs index f6d58e544..ec19157a9 100644 --- a/rslib/src/stats/card.rs +++ b/rslib/src/stats/card.rs @@ -127,32 +127,28 @@ impl Collection { } fn card_stats_to_string(&mut self, cs: CardStats) -> Result { - let offset = self.local_utc_offset_for_user()?; let i18n = &self.i18n; let mut stats = vec![( i18n.tr(TR::CardStatsAdded).to_string(), - cs.added.date_string(offset), + cs.added.date_string(), )]; if let Some(first) = cs.first_review { stats.push(( i18n.tr(TR::CardStatsFirstReview).into(), - first.date_string(offset), + first.date_string(), )) } if let Some(last) = cs.latest_review { stats.push(( i18n.tr(TR::CardStatsLatestReview).into(), - last.date_string(offset), + last.date_string(), )) } match cs.due { Due::Time(secs) => { - stats.push(( - i18n.tr(TR::StatisticsDueDate).into(), - secs.date_string(offset), - )); + stats.push((i18n.tr(TR::StatisticsDueDate).into(), secs.date_string())); } Due::Position(pos) => { stats.push(( @@ -203,7 +199,7 @@ impl Collection { .revlog .into_iter() .rev() - .map(|e| revlog_to_text(e, i18n, offset)) + .map(|e| revlog_to_text(e, i18n)) .collect(); let revlog_titles = RevlogText { time: i18n.tr(TR::CardStatsReviewLogDate).into(), @@ -226,8 +222,8 @@ impl Collection { } } -fn revlog_to_text(e: RevlogEntry, i18n: &I18n, offset: FixedOffset) -> RevlogText { - let dt = offset.timestamp(e.id.as_secs().0, 0); +fn revlog_to_text(e: RevlogEntry, i18n: &I18n) -> RevlogText { + let dt = Local.timestamp(e.id.as_secs().0, 0); let time = dt.format("%Y-%m-%d @ %H:%M").to_string(); let kind = match e.review_kind { RevlogReviewKind::Learning => i18n.tr(TR::CardStatsReviewLogTypeLearn).into(), diff --git a/rslib/src/timestamp.rs b/rslib/src/timestamp.rs index f8aadd1df..60563920d 100644 --- a/rslib/src/timestamp.rs +++ b/rslib/src/timestamp.rs @@ -24,8 +24,8 @@ impl TimestampSecs { } /// YYYY-mm-dd - pub(crate) fn date_string(self, offset: FixedOffset) -> String { - offset.timestamp(self.0, 0).format("%Y-%m-%d").to_string() + pub(crate) fn date_string(self) -> String { + Local.timestamp(self.0, 0).format("%Y-%m-%d").to_string() } pub fn local_utc_offset(self) -> FixedOffset { From a6fb72780a74fc5e1c88a2cdf4c7fd083ef30c50 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Mon, 22 Mar 2021 09:31:07 +0100 Subject: [PATCH 16/18] Show tooltip on browser cells Oftentimes, a cell's text is too long to be fully displayed inside the table, so show it as a tooltip. --- qt/aqt/browser.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 1ffe53979..76b7a7e18 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -225,17 +225,16 @@ class DataModel(QAbstractTableModel): def data(self, index: QModelIndex = QModelIndex(), role: int = 0) -> Any: if not index.isValid(): - return + return QVariant() if role == Qt.FontRole: if self.activeCols[index.column()] not in ("question", "answer", "noteFld"): - return + return QVariant() qfont = QFont() row = self.get_row(index) qfont.setFamily(row.font_name) qfont.setPixelSize(row.font_size) return qfont - - elif role == Qt.TextAlignmentRole: + if role == Qt.TextAlignmentRole: align: Union[Qt.AlignmentFlag, int] = Qt.AlignVCenter if self.activeCols[index.column()] not in ( "question", @@ -248,10 +247,9 @@ class DataModel(QAbstractTableModel): ): align |= Qt.AlignHCenter return align - elif role == Qt.DisplayRole or role == Qt.EditRole: + if role in (Qt.DisplayRole, Qt.ToolTipRole): return self.get_cell(index).text - else: - return + return QVariant() def headerData( self, section: int, orientation: Qt.Orientation, role: int = 0 From 9151bfb53e75fa2f3a44ff46e17704894ab22476 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Mon, 22 Mar 2021 12:08:22 +0100 Subject: [PATCH 17/18] Return input if decode_entities() encounters error --- rslib/src/text.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/rslib/src/text.rs b/rslib/src/text.rs index 85a713539..7af9635a1 100644 --- a/rslib/src/text.rs +++ b/rslib/src/text.rs @@ -138,10 +138,9 @@ pub fn strip_html_preserving_entities(html: &str) -> Cow { pub fn decode_entities(html: &str) -> Cow { if html.contains('&') { match htmlescape::decode_html(html) { - Ok(text) => text.replace('\u{a0}', " "), - Err(e) => format!("{:?}", e), + Ok(text) => text.replace('\u{a0}', " ").into(), + Err(_) => html.into(), } - .into() } else { // nothing to do html.into() From 03b96677894d335748c7221861fbd111a8582582 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Mon, 22 Mar 2021 12:12:52 +0100 Subject: [PATCH 18/18] Use raw sort field text in note_field_str() ... ... instead of the preprocessed note.sort_field. That means we always have to load the note with fields. --- rslib/src/browser_rows.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rslib/src/browser_rows.rs b/rslib/src/browser_rows.rs index 9d597a81d..903700bb4 100644 --- a/rslib/src/browser_rows.rs +++ b/rslib/src/browser_rows.rs @@ -132,7 +132,10 @@ impl RenderContext { impl<'a> RowContext<'a> { fn new(col: &'a mut Collection, id: CardID, with_card_render: bool) -> Result { let card = col.storage.get_card(id)?.ok_or(AnkiError::NotFound)?; - let note = if with_card_render { + // todo: After note.sort_field has been modified so it can be displayed in the browser, + // we can update note_field_str() and only load the note with fields if a card render is + // necessary (see #1082). + let note = if true { col.storage.get_note(card.note_id)? } else { col.storage.get_note_without_fields(card.note_id)? @@ -295,11 +298,8 @@ impl<'a> RowContext<'a> { } fn note_field_str(&self) -> String { - if let Some(field) = &self.note.sort_field { - field.to_owned() - } else { - "".to_string() - } + let index = self.notetype.config.sort_field_idx as usize; + html_to_text_line(&self.note.fields()[index]).into() } fn template_str(&self) -> Result {