diff --git a/proto/anki/cards.proto b/proto/anki/cards.proto index c16033c79..9b2d2192b 100644 --- a/proto/anki/cards.proto +++ b/proto/anki/cards.proto @@ -41,7 +41,7 @@ message Card { sint32 original_due = 15; int64 original_deck_id = 16; uint32 flags = 17; - string data = 18; + generic.UInt32 original_position = 18; } message UpdateCardsRequest { diff --git a/pylib/anki/cards.py b/pylib/anki/cards.py index 5f5aaf945..d13fe131c 100644 --- a/pylib/anki/cards.py +++ b/pylib/anki/cards.py @@ -11,7 +11,7 @@ import anki.collection import anki.decks import anki.notes import anki.template -from anki import cards_pb2, hooks +from anki import cards_pb2, generic_pb2, hooks from anki._legacy import DeprecatedNamesMixin, deprecated from anki.consts import * from anki.models import NotetypeDict, TemplateDict @@ -89,7 +89,9 @@ class Card(DeprecatedNamesMixin): self.odue = card.original_due self.odid = anki.decks.DeckId(card.original_deck_id) self.flags = card.flags - self.data = card.data + self.original_position = ( + card.original_position.val if card.HasField("original_position") else None + ) def _to_backend_card(self) -> cards_pb2.Card: # mtime & usn are set by backend @@ -109,7 +111,9 @@ class Card(DeprecatedNamesMixin): original_due=self.odue, original_deck_id=self.odid, flags=self.flags, - data=self.data, + original_position=generic_pb2.UInt32(val=self.original_position) + if self.original_position is not None + else None, ) def flush(self) -> None: diff --git a/rslib/src/backend/card.rs b/rslib/src/backend/card.rs index 5d1d43fe0..4246aba3f 100644 --- a/rslib/src/backend/card.rs +++ b/rslib/src/backend/card.rs @@ -86,7 +86,7 @@ impl TryFrom for Card { original_due: c.original_due, original_deck_id: DeckId(c.original_deck_id), flags: c.flags as u8, - data: c.data, + original_position: c.original_position.map(|pos| pos.val), }) } } @@ -111,7 +111,7 @@ impl From for pb::Card { original_due: c.original_due, original_deck_id: c.original_deck_id.0, flags: c.flags as u32, - data: c.data, + original_position: c.original_position.map(Into::into), } } } diff --git a/rslib/src/backend/generic.rs b/rslib/src/backend/generic.rs index 4e1ecea89..6813c141a 100644 --- a/rslib/src/backend/generic.rs +++ b/rslib/src/backend/generic.rs @@ -21,6 +21,12 @@ impl From for pb::Bool { } } +impl From for pb::Int32 { + fn from(val: i32) -> Self { + pb::Int32 { val } + } +} + impl From for pb::Int64 { fn from(val: i64) -> Self { pb::Int64 { val } diff --git a/rslib/src/card/mod.rs b/rslib/src/card/mod.rs index ae5cd56bd..7fc769165 100644 --- a/rslib/src/card/mod.rs +++ b/rslib/src/card/mod.rs @@ -77,7 +77,8 @@ pub struct Card { pub(crate) original_due: i32, pub(crate) original_deck_id: DeckId, pub(crate) flags: u8, - pub(crate) data: String, + /// The position in the new queue before leaving it. + pub(crate) original_position: Option, } impl Default for Card { @@ -100,7 +101,7 @@ impl Default for Card { original_due: 0, original_deck_id: DeckId(0), flags: 0, - data: "".to_string(), + original_position: None, } } } diff --git a/rslib/src/scheduler/answering/learning.rs b/rslib/src/scheduler/answering/learning.rs index 86a1b083e..1e2d16b57 100644 --- a/rslib/src/scheduler/answering/learning.rs +++ b/rslib/src/scheduler/answering/learning.rs @@ -19,6 +19,7 @@ impl CardStateUpdater { self.card.ctype = CardType::New; self.card.queue = CardQueue::New; self.card.due = next.position as i32; + self.card.original_position = None; RevlogEntryPartial::new(current, next.into(), 0.0, self.secs_until_rollover()) } @@ -30,6 +31,9 @@ impl CardStateUpdater { ) -> RevlogEntryPartial { self.card.remaining_steps = next.remaining_steps; self.card.ctype = CardType::Learn; + if let Some(position) = current.new_position() { + self.card.original_position = Some(position) + } let interval = next .interval_kind() diff --git a/rslib/src/scheduler/answering/relearning.rs b/rslib/src/scheduler/answering/relearning.rs index 7645d0ca0..01596fefb 100644 --- a/rslib/src/scheduler/answering/relearning.rs +++ b/rslib/src/scheduler/answering/relearning.rs @@ -18,6 +18,9 @@ impl CardStateUpdater { self.card.ctype = CardType::Relearn; self.card.lapses = next.review.lapses; self.card.ease_factor = (next.review.ease_factor * 1000.0).round() as u16; + if let Some(position) = current.new_position() { + self.card.original_position = Some(position) + } let interval = next .interval_kind() diff --git a/rslib/src/scheduler/answering/review.rs b/rslib/src/scheduler/answering/review.rs index 6d36812ef..f87e3288f 100644 --- a/rslib/src/scheduler/answering/review.rs +++ b/rslib/src/scheduler/answering/review.rs @@ -20,6 +20,9 @@ impl CardStateUpdater { self.card.ease_factor = (next.ease_factor * 1000.0).round() as u16; self.card.lapses = next.lapses; self.card.remaining_steps = 0; + if let Some(position) = current.new_position() { + self.card.original_position = Some(position) + } RevlogEntryPartial::new( current, diff --git a/rslib/src/scheduler/new.rs b/rslib/src/scheduler/new.rs index 48b99b138..5c39344cc 100644 --- a/rslib/src/scheduler/new.rs +++ b/rslib/src/scheduler/new.rs @@ -21,6 +21,7 @@ impl Card { self.queue = CardQueue::New; self.interval = 0; self.ease_factor = 0; + self.original_position = None; } /// If the card is new, change its position, and return true. diff --git a/rslib/src/scheduler/states/mod.rs b/rslib/src/scheduler/states/mod.rs index a410b93c8..d85784d8a 100644 --- a/rslib/src/scheduler/states/mod.rs +++ b/rslib/src/scheduler/states/mod.rs @@ -65,6 +65,17 @@ impl CardState { pub(crate) fn leeched(self) -> bool { self.review_state().map(|r| r.leeched).unwrap_or_default() } + + /// Returns the position if it's a [NewState]. + pub(super) fn new_position(&self) -> Option { + match self { + Self::Normal(NormalState::New(NewState { position })) + | Self::Filtered(FilteredState::Rescheduling(ReschedulingFilterState { + original_state: NormalState::New(NewState { position }), + })) => Some(*position), + _ => None, + } + } } /// Info required during state transitions. diff --git a/rslib/src/stats/card.rs b/rslib/src/stats/card.rs index 60bfb8a20..1a09b1597 100644 --- a/rslib/src/stats/card.rs +++ b/rslib/src/stats/card.rs @@ -25,7 +25,7 @@ impl Collection { let revlog = self.storage.get_revlog_entries_for_card(card.id)?; let (average_secs, total_secs) = average_and_total_secs_strings(&revlog); - let (due_date, due_position) = self.due_date_and_position_strings(&card)?; + let (due_date, due_position) = self.due_date_and_position(&card)?; Ok(pb::CardStatsResponse { card_id: card.id.into(), @@ -52,7 +52,7 @@ impl Collection { }) } - fn due_date_and_position_strings( + fn due_date_and_position( &mut self, card: &Card, ) -> Result<(Option, Option)> { @@ -67,7 +67,7 @@ impl Collection { Some(pb::generic::Int64 { val: TimestampSecs::now().0, }), - None, + card.original_position.map(|u| (u as i32).into()), ), CardQueue::Review | CardQueue::DayLearn => ( { @@ -81,7 +81,7 @@ impl Collection { Some(pb::generic::Int64 { val: due.0 }) } }, - None, + card.original_position.map(|u| (u as i32).into()), ), _ => (None, None), }) diff --git a/rslib/src/storage/card/data.rs b/rslib/src/storage/card/data.rs new file mode 100644 index 000000000..5f7338497 --- /dev/null +++ b/rslib/src/storage/card/data.rs @@ -0,0 +1,60 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use rusqlite::{ + types::{FromSql, FromSqlError, ToSqlOutput, ValueRef}, + ToSql, +}; +use serde_derive::{Deserialize, Serialize}; + +use crate::{prelude::*, serde::default_on_invalid}; + +/// Helper for serdeing the card data column. +#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] +#[serde(default)] +pub(super) struct CardData { + #[serde( + skip_serializing_if = "Option::is_none", + rename = "pos", + deserialize_with = "default_on_invalid" + )] + pub(crate) original_position: Option, +} + +impl CardData { + pub(super) fn from_card(card: &Card) -> Self { + Self { + original_position: card.original_position, + } + } +} + +impl FromSql for CardData { + /// Infallible; invalid/missing data results in the default value. + fn column_result(value: ValueRef<'_>) -> std::result::Result { + if let ValueRef::Text(s) = value { + Ok(serde_json::from_slice(s).unwrap_or_default()) + } else { + Ok(Self::default()) + } + } +} + +impl ToSql for CardData { + fn to_sql(&self) -> Result, rusqlite::Error> { + Ok(ToSqlOutput::Owned( + serde_json::to_string(self).unwrap().into(), + )) + } +} + +/// Serialize the JSON `data` for a card. +pub(crate) fn card_data_string(card: &Card) -> String { + serde_json::to_string(&CardData::from_card(card)).unwrap() +} + +/// Extract original position from JSON `data`. +pub(crate) fn original_position_from_card_data(card_data: &str) -> Option { + let data: CardData = serde_json::from_str(card_data).unwrap_or_default(); + data.original_position +} diff --git a/rslib/src/storage/card/mod.rs b/rslib/src/storage/card/mod.rs index 1910fd39e..8d07d61e6 100644 --- a/rslib/src/storage/card/mod.rs +++ b/rslib/src/storage/card/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(crate) mod data; pub(crate) mod filtered; use std::{collections::HashSet, convert::TryFrom, result}; @@ -11,6 +12,7 @@ use rusqlite::{ OptionalExtension, Row, }; +use self::data::CardData; use super::ids_to_string; use crate::{ card::{Card, CardId, CardQueue, CardType}, @@ -47,6 +49,7 @@ impl FromSql for CardQueue { } fn row_to_card(row: &Row) -> result::Result { + let data: CardData = row.get(17)?; Ok(Card { id: row.get(0)?, note_id: row.get(1)?, @@ -65,7 +68,7 @@ fn row_to_card(row: &Row) -> result::Result { original_due: row.get(14).ok().unwrap_or_default(), original_deck_id: row.get(15)?, flags: row.get(16)?, - data: row.get(17)?, + original_position: data.original_position, }) } @@ -109,7 +112,7 @@ impl super::SqliteStorage { card.original_due, card.original_deck_id, card.flags, - card.data, + CardData::from_card(card), card.id, ])?; Ok(()) @@ -136,7 +139,7 @@ impl super::SqliteStorage { card.original_due, card.original_deck_id, card.flags, - card.data, + CardData::from_card(card), ])?; card.id = CardId(self.db.last_insert_rowid()); Ok(()) @@ -163,7 +166,7 @@ impl super::SqliteStorage { card.original_due, card.original_deck_id, card.flags, - card.data, + CardData::from_card(card), ])?; Ok(()) diff --git a/rslib/src/sync/mod.rs b/rslib/src/sync/mod.rs index 8d77fa1ed..9d36d351c 100644 --- a/rslib/src/sync/mod.rs +++ b/rslib/src/sync/mod.rs @@ -26,7 +26,10 @@ use crate::{ prelude::*, revlog::RevlogEntry, serde::{default_on_invalid, deserialize_int_from_number}, - storage::open_and_check_sqlite_file, + storage::{ + card::data::{card_data_string, original_position_from_card_data}, + open_and_check_sqlite_file, + }, tags::{join_tags, split_tags, Tag}, }; @@ -1097,7 +1100,7 @@ impl From for Card { original_due: e.odue, original_deck_id: e.odid, flags: e.flags, - data: e.data, + original_position: original_position_from_card_data(&e.data), } } } @@ -1122,7 +1125,7 @@ impl From for CardEntry { odue: e.original_due, odid: e.original_deck_id, flags: e.flags, - data: e.data, + data: card_data_string(&e), } } }