mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
186 lines
6 KiB
Rust
186 lines
6 KiB
Rust
// Copyright: Ankitects Pty Ltd and contributors
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use crate::{
|
|
card::{CardQueue, CardType},
|
|
config::SchedulerVersion,
|
|
prelude::*,
|
|
search::SortMode,
|
|
};
|
|
|
|
use super::cutoff::local_minutes_west_for_stamp;
|
|
|
|
struct V1FilteredDeckInfo {
|
|
/// True if the filtered deck had rescheduling enabled.
|
|
reschedule: bool,
|
|
/// If the filtered deck had custom steps enabled, `original_step_count`
|
|
/// contains the step count of the home deck, which will be used to ensure
|
|
/// the remaining steps of the card are not out of bounds.
|
|
original_step_count: Option<u32>,
|
|
}
|
|
|
|
impl Card {
|
|
/// Update relearning cards and cards in filtered decks.
|
|
/// `filtered_info` should be provided if card is in a filtered deck.
|
|
fn upgrade_to_v2(&mut self, filtered_info: Option<V1FilteredDeckInfo>) {
|
|
// relearning cards have their own type
|
|
if self.ctype == CardType::Review
|
|
&& matches!(self.queue, CardQueue::Learn | CardQueue::DayLearn)
|
|
{
|
|
self.ctype = CardType::Relearn;
|
|
}
|
|
|
|
// filtered deck handling
|
|
if let Some(info) = filtered_info {
|
|
// cap remaining count to home deck
|
|
if let Some(step_count) = info.original_step_count {
|
|
self.remaining_steps = self.remaining_steps.min(step_count);
|
|
}
|
|
|
|
if !info.reschedule {
|
|
// preview cards start in the review queue in v2
|
|
if self.queue == CardQueue::New {
|
|
self.queue = CardQueue::Review;
|
|
}
|
|
|
|
// to ensure learning cards are reset to new on exit, we must
|
|
// make them new now
|
|
if self.ctype == CardType::Learn {
|
|
self.queue = CardQueue::PreviewRepeat;
|
|
self.ctype = CardType::New;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn get_filter_info_for_card(
|
|
card: &Card,
|
|
decks: &HashMap<DeckID, Deck>,
|
|
configs: &HashMap<DeckConfID, DeckConf>,
|
|
) -> Option<V1FilteredDeckInfo> {
|
|
if card.original_deck_id.0 == 0 {
|
|
None
|
|
} else {
|
|
let (had_custom_steps, reschedule) = if let Some(deck) = decks.get(&card.deck_id) {
|
|
if let DeckKind::Filtered(filtered) = &deck.kind {
|
|
(!filtered.delays.is_empty(), filtered.reschedule)
|
|
} else {
|
|
// not a filtered deck, give up
|
|
return None;
|
|
}
|
|
} else {
|
|
// missing filtered deck, give up
|
|
return None;
|
|
};
|
|
|
|
let original_step_count = if had_custom_steps {
|
|
let home_conf_id = decks
|
|
.get(&card.original_deck_id)
|
|
.and_then(|deck| deck.config_id())
|
|
.unwrap_or(DeckConfID(1));
|
|
Some(
|
|
configs
|
|
.get(&home_conf_id)
|
|
.map(|config| {
|
|
if card.ctype == CardType::Review {
|
|
config.inner.relearn_steps.len()
|
|
} else {
|
|
config.inner.learn_steps.len()
|
|
}
|
|
})
|
|
.unwrap_or(0) as u32,
|
|
)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Some(V1FilteredDeckInfo {
|
|
reschedule,
|
|
original_step_count,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Collection {
|
|
/// Expects an existing transaction. No-op if already on v2.
|
|
pub(crate) fn upgrade_to_v2_scheduler(&mut self) -> Result<()> {
|
|
if self.scheduler_version() == SchedulerVersion::V2 {
|
|
// nothing to do
|
|
return Ok(());
|
|
}
|
|
|
|
self.storage.upgrade_revlog_to_v2()?;
|
|
self.upgrade_cards_to_v2()?;
|
|
self.set_scheduler_version_config_key(SchedulerVersion::V2)?;
|
|
|
|
// enable new timezone code by default
|
|
let created = self.storage.creation_stamp()?;
|
|
if self.get_creation_utc_offset().is_none() {
|
|
self.set_creation_utc_offset(Some(local_minutes_west_for_stamp(created.0)))?;
|
|
}
|
|
|
|
// force full sync
|
|
self.storage.set_schema_modified()
|
|
}
|
|
|
|
fn upgrade_cards_to_v2(&mut self) -> Result<()> {
|
|
let count = self.search_cards_into_table(
|
|
// can't add 'is:learn' here, as it matches on card type, not card queue
|
|
"deck:filtered OR is:review",
|
|
SortMode::NoOrder,
|
|
)?;
|
|
if count > 0 {
|
|
let decks = self.storage.get_decks_map()?;
|
|
let configs = self.storage.get_deck_config_map()?;
|
|
self.storage.for_each_card_in_search(|mut card| {
|
|
let filtered_info = get_filter_info_for_card(&card, &decks, &configs);
|
|
card.upgrade_to_v2(filtered_info);
|
|
self.storage.update_card(&card)
|
|
})?;
|
|
}
|
|
self.storage.clear_searched_cards_table()
|
|
}
|
|
}
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn v2_card() {
|
|
let mut c = Card::default();
|
|
|
|
// relearning cards should be reclassified
|
|
c.ctype = CardType::Review;
|
|
c.queue = CardQueue::DayLearn;
|
|
c.upgrade_to_v2(None);
|
|
assert_eq!(c.ctype, CardType::Relearn);
|
|
|
|
// check step capping
|
|
c.remaining_steps = 5005;
|
|
c.upgrade_to_v2(Some(V1FilteredDeckInfo {
|
|
reschedule: true,
|
|
original_step_count: Some(2),
|
|
}));
|
|
assert_eq!(c.remaining_steps, 2);
|
|
|
|
// with rescheduling off, relearning cards don't need changing
|
|
c.upgrade_to_v2(Some(V1FilteredDeckInfo {
|
|
reschedule: false,
|
|
original_step_count: None,
|
|
}));
|
|
assert_eq!(c.ctype, CardType::Relearn);
|
|
assert_eq!(c.queue, CardQueue::DayLearn);
|
|
|
|
// but learning cards are reset to new
|
|
c.ctype = CardType::Learn;
|
|
c.upgrade_to_v2(Some(V1FilteredDeckInfo {
|
|
reschedule: false,
|
|
original_step_count: None,
|
|
}));
|
|
assert_eq!(c.ctype, CardType::New);
|
|
assert_eq!(c.queue, CardQueue::PreviewRepeat);
|
|
}
|
|
}
|