diff --git a/proto/backend.proto b/proto/backend.proto index f285feb83..6a4fb91dc 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -48,6 +48,7 @@ message BackendInput { OpenCollectionIn open_collection = 36; Empty close_collection = 37; int64 get_card = 38; + Card update_card = 39; } } @@ -79,6 +80,7 @@ message BackendOutput { Empty open_collection = 36; Empty close_collection = 37; GetCardOut get_card = 38; + Empty update_card = 39; BackendError error = 2047; } diff --git a/pylib/anki/cards.py b/pylib/anki/cards.py index e856fc0ba..823d8f1de 100644 --- a/pylib/anki/cards.py +++ b/pylib/anki/cards.py @@ -12,6 +12,7 @@ from anki import hooks from anki.consts import * from anki.models import NoteType, Template from anki.notes import Note +from anki.rsbackend import BackendCard from anki.sound import AVTag from anki.utils import intTime, joinFields, timestampID @@ -33,9 +34,7 @@ class Card: lastIvl: int ord: int - def __init__( - self, col: anki.storage._Collection, id: Optional[int] = None - ) -> None: + def __init__(self, col: anki.storage._Collection, id: Optional[int] = None) -> None: self.col = col.weakref() self.timerStarted = None self._render_output: Optional[anki.template.TemplateRenderOutput] = None @@ -98,30 +97,27 @@ class Card: def flush(self) -> None: self._preFlush() - self.col.db.execute( - """ -insert or replace into cards values -(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", - self.id, - self.nid, - self.did, - self.ord, - self.mod, - self.usn, - self.type, - self.queue, - self.due, - self.ivl, - self.factor, - self.reps, - self.lapses, - self.left, - self.odue, - self.odid, - self.flags, - self.data, + card = BackendCard( + id=self.id, + nid=self.nid, + did=self.did, + ord=self.ord, + mtime=self.mod, + usn=self.usn, + ctype=self.type, + queue=self.queue, + due=self.due, + ivl=self.ivl, + factor=self.factor, + reps=self.reps, + lapses=self.lapses, + left=self.left, + odue=self.odue, + odid=self.odid, + flags=self.flags, + data=self.data, ) - self.col.log(self) + self.col.backend.update_card(card) def question(self, reload: bool = False, browser: bool = False) -> str: return self.css() + self.render_output(reload, browser).question_text diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index d6bf3fab0..8a98b9be4 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -36,6 +36,7 @@ assert ankirspy.buildhash() == anki.buildinfo.buildhash SchedTimingToday = pb.SchedTimingTodayOut BuiltinSortKind = pb.BuiltinSortKind +BackendCard = pb.Card try: import orjson @@ -481,9 +482,11 @@ class RustBackend: ).search_notes.note_ids def get_card(self, cid: int) -> Optional[pb.Card]: - return self._run_command( - pb.BackendInput(get_card=cid) - ).get_card.card + return self._run_command(pb.BackendInput(get_card=cid)).get_card.card + + def update_card(self, card: BackendCard) -> None: + self._run_command(pb.BackendInput(update_card=card)) + def translate_string_in( key: TR, **kwargs: Union[str, int, float] diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index f3c862248..55c2065e6 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -5,8 +5,10 @@ use crate::backend::dbproxy::db_command_bytes; use crate::backend_proto::backend_input::Value; use crate::backend_proto::{BuiltinSortKind, Empty, RenderedTemplateReplacement, SyncMediaIn}; use crate::card::{Card, CardID}; +use crate::card::{CardQueue, CardType}; use crate::collection::{open_collection, Collection}; use crate::config::SortKind; +use crate::decks::DeckID; use crate::err::{AnkiError, NetworkErrorKind, Result, SyncErrorKind}; use crate::i18n::{tr_args, FString, I18n}; use crate::latex::{extract_latex, extract_latex_expanding_clozes, ExtractedLatex}; @@ -14,6 +16,7 @@ use crate::log::{default_logger, Logger}; use crate::media::check::MediaChecker; use crate::media::sync::MediaSyncProgress; use crate::media::MediaManager; +use crate::notes::NoteID; use crate::sched::cutoff::{local_minutes_west_for_stamp, sched_timing_today}; use crate::sched::timespan::{answer_button_time, learning_congrats, studied_today, time_span}; use crate::search::{search_cards, search_notes, SortMode}; @@ -22,10 +25,13 @@ use crate::template::{ RenderedNode, }; use crate::text::{extract_av_tags, strip_av_tags, AVTag}; +use crate::timestamp::TimestampSecs; +use crate::types::Usn; use crate::{backend_proto as pb, log}; use fluent::FluentValue; use prost::Message; use std::collections::{HashMap, HashSet}; +use std::convert::TryFrom; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use tokio::runtime::Runtime; @@ -250,6 +256,10 @@ impl Backend { Value::SearchCards(input) => OValue::SearchCards(self.search_cards(input)?), Value::SearchNotes(input) => OValue::SearchNotes(self.search_notes(input)?), Value::GetCard(cid) => OValue::GetCard(self.get_card(cid)?), + Value::UpdateCard(card) => { + self.update_card(card)?; + OValue::UpdateCard(pb::Empty {}) + } }) } @@ -625,6 +635,11 @@ impl Backend { card: card.map(card_to_pb), }) } + + fn update_card(&self, pbcard: pb::Card) -> Result<()> { + let card = pbcard_to_native(pbcard); + self.with_col(|col| col.with_ctx(|ctx| ctx.storage.update_card(&card))) + } } fn translate_arg_to_fluent_val(arg: &pb::TranslateArgValue) -> FluentValue { @@ -740,3 +755,26 @@ fn card_to_pb(c: Card) -> pb::Card { data: c.data, } } + +fn pbcard_to_native(c: pb::Card) -> Card { + Card { + id: CardID(c.id), + nid: NoteID(c.nid), + did: DeckID(c.did), + ord: c.ord as u16, + mtime: TimestampSecs(c.mtime), + usn: Usn(c.usn), + ctype: CardType::try_from(c.ctype as u8).unwrap_or(CardType::New), + queue: CardQueue::try_from(c.queue as i8).unwrap_or(CardQueue::New), + due: c.due, + ivl: c.ivl, + factor: c.factor as u16, + reps: c.reps, + lapses: c.lapses, + left: c.left, + odue: c.odue, + odid: DeckID(c.odid), + flags: c.flags, + data: c.data, + } +} diff --git a/rslib/src/storage/card.rs b/rslib/src/storage/card.rs index 28232b75e..e7a998121 100644 --- a/rslib/src/storage/card.rs +++ b/rslib/src/storage/card.rs @@ -3,7 +3,7 @@ use crate::cached_sql; use crate::card::{Card, CardID, CardQueue, CardType}; -use crate::err::Result; +use crate::err::{AnkiError, Result}; use rusqlite::params; use rusqlite::{ types::{FromSql, FromSqlError, ValueRef}, @@ -69,4 +69,48 @@ flags, data from cards where id=?" .optional() .map_err(Into::into) } + + pub(crate) fn update_card(&mut self, card: &Card) -> Result<()> { + if card.id.0 == 0 { + return Err(AnkiError::invalid_input("card id not set")); + } + self.flush_card(card) + } + + fn flush_card(&mut self, card: &Card) -> Result<()> { + let stmt = cached_sql!( + self.update_card_stmt, + self.db, + " +insert or replace into cards +(id, nid, did, ord, mod, usn, type, queue, due, ivl, factor, +reps, lapses, left, odue, odid, flags, data) +values +(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +" + ); + + stmt.execute(params![ + card.id, + card.nid, + card.did, + card.ord, + card.mtime, + card.usn, + card.ctype as u8, + card.queue as i8, + card.due, + card.ivl, + card.factor, + card.reps, + card.lapses, + card.left, + card.odue, + card.odid, + card.flags, + card.data, + ])?; + + Ok(()) + } } diff --git a/rslib/src/storage/sqlite.rs b/rslib/src/storage/sqlite.rs index 28d3de18b..a3bb4caf0 100644 --- a/rslib/src/storage/sqlite.rs +++ b/rslib/src/storage/sqlite.rs @@ -224,6 +224,7 @@ pub(crate) struct StorageContext<'a> { // cards pub(super) get_card_stmt: Option>, + pub(super) update_card_stmt: Option>, } impl StorageContext<'_> { @@ -234,6 +235,7 @@ impl StorageContext<'_> { usn: None, timing_today: None, get_card_stmt: None, + update_card_stmt: None, } }