rework various aspects of the test scheduler

- Daily limits are no longer inherited - each deck limits its own
cards, and the selected deck enforces a maximum limit.
- Fetching of review cards now uses a single query, and sorts in advance.
In collections with a large number of overdue cards and decks, this is
faster than iterating over each deck in turn.
- Include interday learning count in review count & review limit, and
allow them to be buried.
- Warn when parent review limit is lower than child deck in deck options.
- Cap the new card limit to the review limit.
- Add option to control whether new card fetching short-circuits.
This commit is contained in:
Damien Elmes 2021-05-15 20:09:50 +10:00
parent 35063316d3
commit 13519a929c
30 changed files with 551 additions and 592 deletions

View File

@ -21,9 +21,9 @@ deck-config-review-limit-tooltip =
The maximum number of review cards to show in a day, The maximum number of review cards to show in a day,
if cards are ready for review. if cards are ready for review.
## Learning/Relearning section ## Learning section
deck-config-learning-relearning-title = Learning/Relearning deck-config-learning-title = Learning
deck-config-learning-steps = Learning steps deck-config-learning-steps = Learning steps
# Please don't translate '5m' or '2d' # Please don't translate '5m' or '2d'
-deck-config-delay-hint = Delays can be in minutes (eg "5m"), or days (eg "2d"). -deck-config-delay-hint = Delays can be in minutes (eg "5m"), or days (eg "2d").
@ -33,12 +33,6 @@ deck-config-learning-steps-tooltip =
The Good button will advance to the next step, which is 10 minutes by default. The Good button will advance to the next step, which is 10 minutes by default.
Once all steps have been passed, the card will become a review card, and Once all steps have been passed, the card will become a review card, and
will appear on a different day. { -deck-config-delay-hint } will appear on a different day. { -deck-config-delay-hint }
deck-config-relearning-steps = Relearning steps
deck-config-relearning-steps-tooltip =
Zero or more delays, separated by spaces. By default, pressing the Again
button on a review card will show it again 10 minutes later. If no delays
are provided, the card will have its interval changed, without entering
relearning. { -deck-config-delay-hint }
deck-config-interday-step-priority = Interday step priority deck-config-interday-step-priority = Interday step priority
deck-config-interday-step-priority-tooltip = When to show (re)learning cards that cross a day boundary. deck-config-interday-step-priority-tooltip = When to show (re)learning cards that cross a day boundary.
deck-config-review-mix-mix-with-reviews = Mix with reviews deck-config-review-mix-mix-with-reviews = Mix with reviews
@ -56,33 +50,56 @@ deck-config-easy-interval-tooltip =
deck-config-new-insertion-order = Insertion order deck-config-new-insertion-order = Insertion order
deck-config-new-insertion-order-tooltip = deck-config-new-insertion-order-tooltip =
Controls the position (due #) new cards are assigned when you add new cards. Controls the position (due #) new cards are assigned when you add new cards.
Cards with a lower due # will be shown first when reviewing. Changing this Cards with a lower due # will be shown first when studying. Changing this
option will automatically update the existing position of new cards. option will automatically update the existing position of new cards.
deck-config-new-insertion-order-sequential = Sequential (show oldest cards first) deck-config-new-insertion-order-sequential = Sequential (oldest cards first)
deck-config-new-insertion-order-random = Random deck-config-new-insertion-order-random = Random
deck-config-new-gather-priority = Gather priority
deck-config-new-gather-priority-tooltip =
<b>Deck</b> gathers cards from each subdeck in order, and stops when the
limit of the selected deck has been exceeded. This is faster, and allows you
to prioritize subdecks that are closer to the top. <b>Position</b> gathers
cards from all decks before they are sorted. This ensures the oldest cards
will be shown first, even if the parent limit is not high enough to see
cards from all decks.
deck-config-new-gather-priority-deck = Deck
deck-config-new-gather-priority-position = Position
deck-config-sort-order = Sort order deck-config-sort-order = Sort order
deck-config-sort-order-tooltip = deck-config-sort-order-tooltip =
After today's new cards have been gathered, this option controls the order This option controls how cards are sorted after they have been gathered.
they are then presented in. The default is to sort by card template first, to avoid By default, Anki sorts by card template first, to avoid multiple cards of
multiple cards of the same note from being shown in succession. the same note from being shown in succession.
deck-config-sort-order-card-template-then-position = Card template, then position deck-config-sort-order-card-template-then-position = Card template, then position
deck-config-sort-order-card-template-then-random = Card template, then random deck-config-sort-order-card-template-then-random = Card template, then random
deck-config-sort-order-position = Position (siblings together) deck-config-sort-order-position = Position (siblings together)
deck-config-sort-order-random = Random deck-config-sort-order-random = Random
deck-config-new-priority = Priority deck-config-review-priority = Review priority
deck-config-new-priority-tooltip = When to show new cards in a study session. deck-config-review-priority-tooltip = When to show these cards in relation to review cards.
## Review section ## Review section
deck-config-review-sort-order-tooltip = deck-config-review-sort-order-tooltip =
After today's review cards have been gathered, this option controls the order The default order fetches cards from each subdeck in turn, stopping when the limit
they are then presented in. The default is to sort by due date, then shuffle, so of the selected deck has been reached. The gathered cards are then shuffled together,
that if you have a backlog of reviews, the cards that have been waiting longest and shown in due date order. Because gathering stops when the parent limit has been
will be shown first. The other choices can be useful when you have a large backlog reached, your child decks should have smaller limits if you wish to see cards from
and want to tackle it in a different way. multiple decks at once.
The other sort options are mainly useful when catching up from a large backlog.
Because they have to sort all the cards first, they can be considerably slower
than the default sort order when many cards are due.
deck-config-sort-order-due-date-then-random = Due date, then random deck-config-sort-order-due-date-then-random = Due date, then random
deck-config-sort-order-ascending-intervals = Ascending intervals deck-config-sort-order-ascending-intervals = Ascending intervals
deck-config-sort-order-descending-intervals = Descending intervals deck-config-sort-order-descending-intervals = Descending intervals
## Lapses section
deck-config-relearning-steps = Relearning steps
deck-config-relearning-steps-tooltip =
Zero or more delays, separated by spaces. By default, pressing the Again
button on a review card will show it again 10 minutes later. If no delays
are provided, the card will have its interval changed, without entering
relearning. { -deck-config-delay-hint }
deck-config-leech-threshold-tooltip = deck-config-leech-threshold-tooltip =
The number of times Again needs to be pressed on a review card before it is The number of times Again needs to be pressed on a review card before it is
marked as a leech. Leeches are cards that consume a lot of your time, and marked as a leech. Leeches are cards that consume a lot of your time, and
@ -185,3 +202,4 @@ deck-config-reviews-too-low =
}, your review limit should be at least { $expected }. }, your review limit should be at least { $expected }.
deck-config-learning-step-above-graduating-interval = The graduating interval should be at least as long as your final learning step. deck-config-learning-step-above-graduating-interval = The graduating interval should be at least as long as your final learning step.
deck-config-good-above-easy = The easy interval should be at least as long as the graduating interval. deck-config-good-above-easy = The easy interval should be at least as long as the graduating interval.
deck-config-relearning-steps-above-minimum-interval = The minimum lapse interval should be at least as long as your final relearning step.

View File

@ -475,8 +475,6 @@ def review_limits_setup() -> Tuple[anki.collection.Collection, Dict]:
def test_review_limits(): def test_review_limits():
if is_2021():
pytest.skip("old sched only")
col, child = review_limits_setup() col, child = review_limits_setup()
tree = col.sched.deck_due_tree().children tree = col.sched.deck_due_tree().children
@ -499,30 +497,6 @@ def test_review_limits():
assert tree[0].children[0].review_count == 9 # child assert tree[0].children[0].review_count == 9 # child
def test_review_limits_new():
if not is_2021():
pytest.skip("new sched only")
col, child = review_limits_setup()
tree = col.sched.deck_due_tree().children
assert tree[0].review_count == 5 # parent
assert tree[0].children[0].review_count == 5 # child capped by parent
# child .counts() are bound by parents
col.decks.select(child["id"])
col.sched.reset()
assert col.sched.counts() == (0, 0, 5)
# answering a card in the child should decrement both child and parent count
c = col.sched.getCard()
col.sched.answerCard(c, 3)
assert col.sched.counts() == (0, 0, 4)
tree = col.sched.deck_due_tree().children
assert tree[0].review_count == 4 # parent
assert tree[0].children[0].review_count == 4 # child
def test_button_spacing(): def test_button_spacing():
col = getEmptyCol() col = getEmptyCol()
note = col.newNote() note = col.newNote()

View File

@ -309,9 +309,13 @@ service CardsService {
message DeckConfig { message DeckConfig {
message Config { message Config {
enum NewCardFetchOrder { enum NewCardInsertOrder {
NEW_CARD_FETCH_ORDER_DUE = 0; NEW_CARD_INSERT_ORDER_DUE = 0;
NEW_CARD_FETCH_ORDER_RANDOM = 1; NEW_CARD_INSERT_ORDER_RANDOM = 1;
}
enum NewCardGatherPriority {
NEW_CARD_GATHER_PRIORITY_DECK = 0;
NEW_CARD_GATHER_PRIORITY_POSITION = 1;
} }
enum NewCardSortOrder { enum NewCardSortOrder {
NEW_CARD_SORT_ORDER_TEMPLATE_THEN_DUE = 0; NEW_CARD_SORT_ORDER_TEMPLATE_THEN_DUE = 0;
@ -356,11 +360,13 @@ message DeckConfig {
uint32 graduating_interval_good = 18; uint32 graduating_interval_good = 18;
uint32 graduating_interval_easy = 19; uint32 graduating_interval_easy = 19;
NewCardFetchOrder new_card_fetch_order = 20; NewCardInsertOrder new_card_insert_order = 20;
NewCardGatherPriority new_card_gather_priority = 34;
NewCardSortOrder new_card_sort_order = 32; NewCardSortOrder new_card_sort_order = 32;
ReviewMix new_mix = 30;
ReviewCardOrder review_order = 33; ReviewCardOrder review_order = 33;
ReviewMix new_mix = 30;
ReviewMix interday_learning_mix = 31; ReviewMix interday_learning_mix = 31;
LeechAction leech_action = 21; LeechAction leech_action = 21;
@ -659,7 +665,6 @@ message DeckTreeIn {
message DeckTreeNode { message DeckTreeNode {
int64 deck_id = 1; int64 deck_id = 1;
string name = 2; string name = 2;
repeated DeckTreeNode children = 3;
uint32 level = 4; uint32 level = 4;
bool collapsed = 5; bool collapsed = 5;
@ -668,6 +673,10 @@ message DeckTreeNode {
uint32 new_count = 8; uint32 new_count = 8;
bool filtered = 16; bool filtered = 16;
// low index so key can be packed into a byte, but at bottom
// to make debug output easier to read
repeated DeckTreeNode children = 3;
} }
message RenderExistingCardIn { message RenderExistingCardIn {

View File

@ -9,7 +9,10 @@ pub use schema11::{DeckConfSchema11, NewCardOrderSchema11};
pub use update::UpdateDeckConfigsIn; pub use update::UpdateDeckConfigsIn;
pub use crate::backend_proto::deck_config::{ pub use crate::backend_proto::deck_config::{
config::{LeechAction, NewCardFetchOrder, NewCardSortOrder, ReviewCardOrder, ReviewMix}, config::{
LeechAction, NewCardGatherPriority, NewCardInsertOrder, NewCardSortOrder, ReviewCardOrder,
ReviewMix,
},
Config as DeckConfigInner, Config as DeckConfigInner,
}; };
@ -58,7 +61,8 @@ impl Default for DeckConfig {
minimum_lapse_interval: 1, minimum_lapse_interval: 1,
graduating_interval_good: 1, graduating_interval_good: 1,
graduating_interval_easy: 4, graduating_interval_easy: 4,
new_card_fetch_order: NewCardFetchOrder::Due as i32, new_card_insert_order: NewCardInsertOrder::Due as i32,
new_card_gather_priority: NewCardGatherPriority::Deck as i32,
new_card_sort_order: NewCardSortOrder::TemplateThenDue as i32, new_card_sort_order: NewCardSortOrder::TemplateThenDue as i32,
review_order: ReviewCardOrder::DayThenRandom as i32, review_order: ReviewCardOrder::DayThenRandom as i32,
new_mix: ReviewMix::MixWithReviews as i32, new_mix: ReviewMix::MixWithReviews as i32,

View File

@ -10,7 +10,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr};
use serde_tuple::Serialize_tuple; use serde_tuple::Serialize_tuple;
use super::{ use super::{
DeckConfig, DeckConfigId, DeckConfigInner, NewCardFetchOrder, INITIAL_EASE_FACTOR_THOUSANDS, DeckConfig, DeckConfigId, DeckConfigInner, NewCardInsertOrder, INITIAL_EASE_FACTOR_THOUSANDS,
}; };
use crate::{serde::default_on_invalid, timestamp::TimestampSecs, types::Usn}; use crate::{serde::default_on_invalid, timestamp::TimestampSecs, types::Usn};
@ -48,6 +48,8 @@ pub struct DeckConfSchema11 {
review_order: i32, review_order: i32,
#[serde(default)] #[serde(default)]
new_sort_order: i32, new_sort_order: i32,
#[serde(default)]
new_gather_priority: i32,
#[serde(flatten)] #[serde(flatten)]
other: HashMap<String, Value>, other: HashMap<String, Value>,
@ -213,6 +215,7 @@ impl Default for DeckConfSchema11 {
interday_learning_mix: 0, interday_learning_mix: 0,
review_order: 0, review_order: 0,
new_sort_order: 0, new_sort_order: 0,
new_gather_priority: 0,
} }
} }
} }
@ -263,10 +266,11 @@ impl From<DeckConfSchema11> for DeckConfig {
minimum_lapse_interval: c.lapse.min_int, minimum_lapse_interval: c.lapse.min_int,
graduating_interval_good: c.new.ints.good as u32, graduating_interval_good: c.new.ints.good as u32,
graduating_interval_easy: c.new.ints.easy as u32, graduating_interval_easy: c.new.ints.easy as u32,
new_card_fetch_order: match c.new.order { new_card_insert_order: match c.new.order {
NewCardOrderSchema11::Random => NewCardFetchOrder::Random, NewCardOrderSchema11::Random => NewCardInsertOrder::Random,
NewCardOrderSchema11::Due => NewCardFetchOrder::Due, NewCardOrderSchema11::Due => NewCardInsertOrder::Due,
} as i32, } as i32,
new_card_gather_priority: c.new_gather_priority,
new_card_sort_order: c.new_sort_order, new_card_sort_order: c.new_sort_order,
review_order: c.review_order, review_order: c.review_order,
new_mix: c.new_mix, new_mix: c.new_mix,
@ -312,7 +316,7 @@ impl From<DeckConfig> for DeckConfSchema11 {
} }
} }
let i = c.inner; let i = c.inner;
let new_order = i.new_card_fetch_order(); let new_order = i.new_card_insert_order();
DeckConfSchema11 { DeckConfSchema11 {
id: c.id, id: c.id,
mtime: c.mtime_secs, mtime: c.mtime_secs,
@ -333,8 +337,8 @@ impl From<DeckConfig> for DeckConfSchema11 {
_unused: 0, _unused: 0,
}, },
order: match new_order { order: match new_order {
NewCardFetchOrder::Random => NewCardOrderSchema11::Random, NewCardInsertOrder::Random => NewCardOrderSchema11::Random,
NewCardFetchOrder::Due => NewCardOrderSchema11::Due, NewCardInsertOrder::Due => NewCardOrderSchema11::Due,
}, },
per_day: i.new_per_day, per_day: i.new_per_day,
other: new_other, other: new_other,
@ -365,6 +369,7 @@ impl From<DeckConfig> for DeckConfSchema11 {
interday_learning_mix: i.interday_learning_mix, interday_learning_mix: i.interday_learning_mix,
review_order: i.review_order, review_order: i.review_order,
new_sort_order: i.new_card_sort_order, new_sort_order: i.new_card_sort_order,
new_gather_priority: i.new_card_gather_priority,
} }
} }
} }
@ -383,6 +388,7 @@ fn clear_other_duplicates(top_other: &mut HashMap<String, Value>) {
"interdayLearningMix", "interdayLearningMix",
"reviewOrder", "reviewOrder",
"newSortOrder", "newSortOrder",
"newGatherPriority",
] { ] {
top_other.remove(*key); top_other.remove(*key);
} }

View File

@ -151,7 +151,7 @@ impl Collection {
let previous_config_id = DeckConfigId(normal.config_id); let previous_config_id = DeckConfigId(normal.config_id);
let previous_order = configs_before_update let previous_order = configs_before_update
.get(&previous_config_id) .get(&previous_config_id)
.map(|c| c.inner.new_card_fetch_order()) .map(|c| c.inner.new_card_insert_order())
.unwrap_or_default(); .unwrap_or_default();
// if a selected (sub)deck, or its old config was removed, update deck to point to new config // if a selected (sub)deck, or its old config was removed, update deck to point to new config
@ -169,7 +169,7 @@ impl Collection {
// if new order differs, deck needs re-sorting // if new order differs, deck needs re-sorting
let current_order = configs_after_update let current_order = configs_after_update
.get(&current_config_id) .get(&current_config_id)
.map(|c| c.inner.new_card_fetch_order()) .map(|c| c.inner.new_card_insert_order())
.unwrap_or_default(); .unwrap_or_default();
if previous_order != current_order { if previous_order != current_order {
self.sort_deck(deck_id, current_order, usn)?; self.sort_deck(deck_id, current_order, usn)?;
@ -184,7 +184,7 @@ impl Collection {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::{collection::open_test_collection, deckconfig::NewCardFetchOrder}; use crate::{collection::open_test_collection, deckconfig::NewCardInsertOrder};
#[test] #[test]
fn updating() -> Result<()> { fn updating() -> Result<()> {
@ -258,7 +258,7 @@ mod test {
assert_eq!(card1_pos(&mut col), 1); assert_eq!(card1_pos(&mut col), 1);
reset_card1_pos(&mut col); reset_card1_pos(&mut col);
assert_eq!(card1_pos(&mut col), 0); assert_eq!(card1_pos(&mut col), 0);
input.configs[1].inner.new_card_fetch_order = NewCardFetchOrder::Random as i32; input.configs[1].inner.new_card_insert_order = NewCardInsertOrder::Random as i32;
assert_eq!( assert_eq!(
col.update_deck_configs(input.clone())?.changes.changes.card, col.update_deck_configs(input.clone())?.changes.changes.card,
true true

View File

@ -30,12 +30,14 @@ impl Collection {
days_elapsed: u32, days_elapsed: u32,
learn_cutoff: u32, learn_cutoff: u32,
limit_to: Option<&str>, limit_to: Option<&str>,
v3: bool,
) -> Result<HashMap<DeckId, DueCounts>> { ) -> Result<HashMap<DeckId, DueCounts>> {
self.storage.due_counts( self.storage.due_counts(
self.scheduler_version(), self.scheduler_version(),
days_elapsed, days_elapsed,
learn_cutoff, learn_cutoff,
limit_to, limit_to,
v3,
) )
} }

59
rslib/src/decks/limits.rs Normal file
View File

@ -0,0 +1,59 @@
// 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 super::Deck;
use crate::{
deckconfig::{DeckConfig, DeckConfigId},
prelude::*,
};
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct RemainingLimits {
pub review: u32,
pub new: u32,
}
impl RemainingLimits {
pub(crate) fn new(deck: &Deck, config: Option<&DeckConfig>, today: u32) -> Self {
config
.map(|config| {
let (new_today, rev_today) = deck.new_rev_counts(today);
RemainingLimits {
review: ((config.inner.reviews_per_day as i32) - rev_today).max(0) as u32,
new: ((config.inner.new_per_day as i32) - new_today).max(0) as u32,
}
})
.unwrap_or_default()
}
pub(crate) fn cap_to(&mut self, limits: RemainingLimits) {
self.review = self.review.min(limits.review);
self.new = self.new.min(limits.new);
}
}
impl Default for RemainingLimits {
fn default() -> Self {
RemainingLimits {
review: 9999,
new: 9999,
}
}
}
pub(crate) fn remaining_limits_map<'a>(
decks: impl Iterator<Item = &'a Deck>,
config: &'a HashMap<DeckConfigId, DeckConfig>,
today: u32,
) -> HashMap<DeckId, RemainingLimits> {
decks
.map(|deck| {
(
deck.id,
RemainingLimits::new(deck, deck.config_id().and_then(|id| config.get(&id)), today),
)
})
.collect()
}

View File

@ -5,6 +5,7 @@ mod addupdate;
mod counts; mod counts;
mod current; mod current;
mod filtered; mod filtered;
pub(crate) mod limits;
mod name; mod name;
mod remove; mod remove;
mod reparent; mod reparent;

View File

@ -9,15 +9,18 @@ use std::{
use serde_tuple::Serialize_tuple; use serde_tuple::Serialize_tuple;
use unicase::UniCase; use unicase::UniCase;
use super::DueCounts; use super::{
limits::{remaining_limits_map, RemainingLimits},
DueCounts,
};
pub use crate::backend_proto::set_deck_collapsed_in::Scope as DeckCollapseScope; pub use crate::backend_proto::set_deck_collapsed_in::Scope as DeckCollapseScope;
use crate::{ use crate::{
backend_proto::DeckTreeNode, config::SchedulerVersion, ops::OpOutput, prelude::*, undo::Op, backend_proto::DeckTreeNode, config::SchedulerVersion, ops::OpOutput, prelude::*, undo::Op,
}; };
fn deck_names_to_tree(names: Vec<(DeckId, String)>) -> DeckTreeNode { fn deck_names_to_tree(names: impl Iterator<Item = (DeckId, String)>) -> DeckTreeNode {
let mut top = DeckTreeNode::default(); let mut top = DeckTreeNode::default();
let mut it = names.into_iter().peekable(); let mut it = names.peekable();
add_child_nodes(&mut it, &mut top); add_child_nodes(&mut it, &mut top);
@ -89,26 +92,22 @@ fn add_counts(node: &mut DeckTreeNode, counts: &HashMap<DeckId, DueCounts>) {
} }
/// Apply parent limits to children, and add child counts to parents. /// Apply parent limits to children, and add child counts to parents.
/// Counts are (new, review). fn apply_limits_v1(
fn apply_limits(
node: &mut DeckTreeNode, node: &mut DeckTreeNode,
today: u32, limits: &HashMap<DeckId, RemainingLimits>,
decks: &HashMap<DeckId, Deck>, parent_limits: RemainingLimits,
dconf: &HashMap<DeckConfigId, DeckConfig>,
parent_limits: (u32, u32),
) { ) {
let (mut remaining_new, mut remaining_rev) = let mut remaining = limits
remaining_counts_for_deck(DeckId(node.deck_id), today, decks, dconf); .get(&DeckId(node.deck_id))
.copied()
// cap remaining to parent limits .unwrap_or_default();
remaining_new = remaining_new.min(parent_limits.0); remaining.cap_to(parent_limits);
remaining_rev = remaining_rev.min(parent_limits.1);
// apply our limit to children and tally their counts // apply our limit to children and tally their counts
let mut child_new_total = 0; let mut child_new_total = 0;
let mut child_rev_total = 0; let mut child_rev_total = 0;
for child in &mut node.children { for child in &mut node.children {
apply_limits(child, today, decks, dconf, (remaining_new, remaining_rev)); apply_limits_v1(child, limits, remaining);
child_new_total += child.new_count; child_new_total += child.new_count;
child_rev_total += child.review_count; child_rev_total += child.review_count;
// no limit on learning cards // no limit on learning cards
@ -116,82 +115,87 @@ fn apply_limits(
} }
// add child counts to our count, capped to remaining limit // add child counts to our count, capped to remaining limit
node.new_count = (node.new_count + child_new_total).min(remaining_new); node.new_count = (node.new_count + child_new_total).min(remaining.new);
node.review_count = (node.review_count + child_rev_total).min(remaining_rev); node.review_count = (node.review_count + child_rev_total).min(remaining.review);
} }
/// Apply parent new limits to children, and add child counts to parents. Unlike /// Apply parent new limits to children, and add child counts to parents. Unlike
/// v1 and the 2021 scheduler, reviews are not capped by their parents, and we /// v1, reviews are not capped by their parents, and we
/// return the uncapped review amount to add to the parent. /// return the uncapped review amount to add to the parent.
/// Counts are (new, review). fn apply_limits_v2(
fn apply_limits_v2_old(
node: &mut DeckTreeNode, node: &mut DeckTreeNode,
today: u32, limits: &HashMap<DeckId, RemainingLimits>,
decks: &HashMap<DeckId, Deck>, parent_limits: RemainingLimits,
dconf: &HashMap<DeckConfigId, DeckConfig>,
parent_limits: (u32, u32),
) -> u32 { ) -> u32 {
let original_rev_count = node.review_count; let original_rev_count = node.review_count;
let mut remaining = limits
let (mut remaining_new, remaining_rev) = .get(&DeckId(node.deck_id))
remaining_counts_for_deck(DeckId(node.deck_id), today, decks, dconf); .copied()
.unwrap_or_default();
// cap remaining to parent limits remaining.new = remaining.new.min(parent_limits.new);
remaining_new = remaining_new.min(parent_limits.0);
// apply our limit to children and tally their counts // apply our limit to children and tally their counts
let mut child_new_total = 0; let mut child_new_total = 0;
let mut child_rev_total = 0; let mut child_rev_total = 0;
for child in &mut node.children { for child in &mut node.children {
child_rev_total += child_rev_total += apply_limits_v2(child, limits, remaining);
apply_limits_v2_old(child, today, decks, dconf, (remaining_new, remaining_rev));
child_new_total += child.new_count; child_new_total += child.new_count;
// no limit on learning cards // no limit on learning cards
node.learn_count += child.learn_count; node.learn_count += child.learn_count;
} }
// add child counts to our count, capped to remaining limit // add child counts to our count, capped to remaining limit
node.new_count = (node.new_count + child_new_total).min(remaining_new); node.new_count = (node.new_count + child_new_total).min(remaining.new);
node.review_count = (node.review_count + child_rev_total).min(remaining_rev); node.review_count = (node.review_count + child_rev_total).min(remaining.review);
original_rev_count + child_rev_total original_rev_count + child_rev_total
} }
fn remaining_counts_for_deck( /// Add child counts, then limit to remaining limit. The v3 scheduler does not
did: DeckId, /// propagate limits down the tree. Limits for a deck affect only the amount
today: u32, /// that deck itself will gather.
decks: &HashMap<DeckId, Deck>, /// The v3 scheduler also caps the new limit to the remaining review limit,
dconf: &HashMap<DeckConfigId, DeckConfig>, /// so no new cards will be introduced when there is a backlog that exceeds
/// the review limits.
fn apply_limits_v3(
node: &mut DeckTreeNode,
limits: &HashMap<DeckId, RemainingLimits>,
) -> (u32, u32) { ) -> (u32, u32) {
if let Some(deck) = decks.get(&did) { let mut remaining = limits
match &deck.kind { .get(&DeckId(node.deck_id))
DeckKind::Normal(norm) => { .copied()
let (new_today, rev_today) = deck.new_rev_counts(today); .unwrap_or_default();
if let Some(conf) = dconf
.get(&DeckConfigId(norm.config_id)) // recurse into children, tallying their counts
.or_else(|| dconf.get(&DeckConfigId(1))) let mut child_new_total = 0;
{ let mut child_rev_total = 0;
let new = (conf.inner.new_per_day as i32) for child in &mut node.children {
.saturating_sub(new_today) let child_counts = apply_limits_v3(child, limits);
.max(0); child_new_total += child_counts.0;
let rev = (conf.inner.reviews_per_day as i32) child_rev_total += child_counts.1;
.saturating_sub(rev_today) // no limit on learning cards
.max(0); node.learn_count += child.learn_count;
(new as u32, rev as u32)
} else {
// missing dconf and fallback
(0, 0)
}
}
DeckKind::Filtered(_) => {
// filtered decks have no limit
(std::u32::MAX, std::u32::MAX)
}
}
} else {
// top level deck with id 0
(std::u32::MAX, std::u32::MAX)
} }
// new limits capped to review limits
remaining.new = remaining.new.min(
remaining
.review
.saturating_sub(node.review_count)
.saturating_sub(child_rev_total),
);
// parents want the child total without caps
let out = (
node.new_count.min(remaining.new) + child_new_total,
node.review_count.min(remaining.review) + child_rev_total,
);
// but the current node needs to cap after adding children
node.new_count = (node.new_count + child_new_total).min(remaining.new);
node.review_count = (node.review_count + child_rev_total).min(remaining.review);
out
} }
fn hide_default_deck(node: &mut DeckTreeNode) { fn hide_default_deck(node: &mut DeckTreeNode) {
@ -257,7 +261,7 @@ impl Collection {
top_deck_id: Option<DeckId>, top_deck_id: Option<DeckId>,
) -> Result<DeckTreeNode> { ) -> Result<DeckTreeNode> {
let names = self.storage.get_all_deck_names()?; let names = self.storage.get_all_deck_names()?;
let mut tree = deck_names_to_tree(names); let mut tree = deck_names_to_tree(names.into_iter());
let decks_map = self.storage.get_decks_map()?; let decks_map = self.storage.get_decks_map()?;
@ -276,27 +280,20 @@ impl Collection {
}); });
let days_elapsed = self.timing_for_timestamp(now)?.days_elapsed; let days_elapsed = self.timing_for_timestamp(now)?.days_elapsed;
let learn_cutoff = (now.0 as u32) + self.learn_ahead_secs(); let learn_cutoff = (now.0 as u32) + self.learn_ahead_secs();
let counts = self.due_counts(days_elapsed, learn_cutoff, limit)?; let sched_ver = self.scheduler_version();
let v3 = self.get_bool(BoolKey::Sched2021);
let counts = self.due_counts(days_elapsed, learn_cutoff, limit, v3)?;
let dconf = self.storage.get_deck_config_map()?; let dconf = self.storage.get_deck_config_map()?;
add_counts(&mut tree, &counts); add_counts(&mut tree, &counts);
if self.scheduler_version() == SchedulerVersion::V2 let limits = remaining_limits_map(decks_map.values(), &dconf, days_elapsed);
&& !self.get_bool(BoolKey::Sched2021) if sched_ver == SchedulerVersion::V2 {
{ if v3 {
apply_limits_v2_old( apply_limits_v3(&mut tree, &limits);
&mut tree, } else {
days_elapsed, apply_limits_v2(&mut tree, &limits, RemainingLimits::default());
&decks_map, }
&dconf,
(std::u32::MAX, std::u32::MAX),
);
} else { } else {
apply_limits( apply_limits_v1(&mut tree, &limits, RemainingLimits::default());
&mut tree,
days_elapsed,
&decks_map,
&dconf,
(std::u32::MAX, std::u32::MAX),
);
} }
} }
@ -328,7 +325,9 @@ impl Collection {
Ok(()) Ok(())
}) })
} }
}
impl Collection {
pub(crate) fn legacy_deck_tree(&mut self) -> Result<LegacyDueCounts> { pub(crate) fn legacy_deck_tree(&mut self) -> Result<LegacyDueCounts> {
let tree = self.deck_tree(Some(TimestampSecs::now()), None)?; let tree = self.deck_tree(Some(TimestampSecs::now()), None)?;
Ok(LegacyDueCounts::from(tree)) Ok(LegacyDueCounts::from(tree))

View File

@ -311,10 +311,10 @@ impl Collection {
.get(&did) .get(&did)
.unwrap() .unwrap()
.inner .inner
.new_card_fetch_order() .new_card_insert_order()
{ {
crate::deckconfig::NewCardFetchOrder::Random => Ok(random_position(next_pos)), crate::deckconfig::NewCardInsertOrder::Random => Ok(random_position(next_pos)),
crate::deckconfig::NewCardFetchOrder::Due => Ok(next_pos), crate::deckconfig::NewCardInsertOrder::Due => Ok(next_pos),
} }
} }

View File

@ -261,7 +261,7 @@ impl Collection {
if let Some(revlog_partial) = updater.apply_study_state(current_state, answer.new_state)? { if let Some(revlog_partial) = updater.apply_study_state(current_state, answer.new_state)? {
self.add_partial_revlog(revlog_partial, usn, &answer)?; self.add_partial_revlog(revlog_partial, usn, &answer)?;
} }
self.update_deck_stats_from_answer(usn, &answer, &updater)?; self.update_deck_stats_from_answer(usn, &answer, &updater, original.queue)?;
self.maybe_bury_siblings(&original, &updater.config)?; self.maybe_bury_siblings(&original, &updater.config)?;
let timing = updater.timing; let timing = updater.timing;
let mut card = updater.into_card(); let mut card = updater.into_card();
@ -308,26 +308,22 @@ impl Collection {
usn: Usn, usn: Usn,
answer: &CardAnswer, answer: &CardAnswer,
updater: &CardStateUpdater, updater: &CardStateUpdater,
from_queue: CardQueue,
) -> Result<()> { ) -> Result<()> {
let mut new_delta = 0;
let mut review_delta = 0;
match from_queue {
CardQueue::New => new_delta += 1,
CardQueue::Review => review_delta += 1,
_ => {}
}
self.update_deck_stats( self.update_deck_stats(
updater.timing.days_elapsed, updater.timing.days_elapsed,
usn, usn,
backend_proto::UpdateStatsIn { backend_proto::UpdateStatsIn {
deck_id: updater.deck.id.0, deck_id: updater.deck.id.0,
new_delta: if matches!(answer.current_state, CardState::Normal(NormalState::New(_))) new_delta,
{ review_delta,
1
} else {
0
},
review_delta: if matches!(
answer.current_state,
CardState::Normal(NormalState::Review(_))
) {
1
} else {
0
},
millisecond_delta: answer.milliseconds_taken as i32, millisecond_delta: answer.milliseconds_taken as i32,
}, },
) )

View File

@ -7,7 +7,7 @@ use rand::seq::SliceRandom;
use crate::{ use crate::{
card::{CardQueue, CardType}, card::{CardQueue, CardType},
deckconfig::NewCardFetchOrder, deckconfig::NewCardInsertOrder,
prelude::*, prelude::*,
search::{SortMode, StateKind}, search::{SortMode, StateKind},
}; };
@ -171,9 +171,9 @@ impl Collection {
col.sort_deck( col.sort_deck(
deck, deck,
if random { if random {
NewCardFetchOrder::Random NewCardInsertOrder::Random
} else { } else {
NewCardFetchOrder::Due NewCardInsertOrder::Due
}, },
col.usn()?, col.usn()?,
) )
@ -183,7 +183,7 @@ impl Collection {
pub(crate) fn sort_deck( pub(crate) fn sort_deck(
&mut self, &mut self,
deck: DeckId, deck: DeckId,
order: NewCardFetchOrder, order: NewCardInsertOrder,
usn: Usn, usn: Usn,
) -> Result<usize> { ) -> Result<usize> {
let cids = self.search_cards(match_all![deck, StateKind::New], SortMode::NoOrder)?; let cids = self.search_cards(match_all![deck, StateKind::New], SortMode::NoOrder)?;
@ -241,11 +241,11 @@ mod test {
} }
} }
impl From<NewCardFetchOrder> for NewCardDueOrder { impl From<NewCardInsertOrder> for NewCardDueOrder {
fn from(o: NewCardFetchOrder) -> Self { fn from(o: NewCardInsertOrder) -> Self {
match o { match o {
NewCardFetchOrder::Due => NewCardDueOrder::NoteId, NewCardInsertOrder::Due => NewCardDueOrder::NoteId,
NewCardFetchOrder::Random => NewCardDueOrder::Random, NewCardInsertOrder::Random => NewCardDueOrder::Random,
} }
} }
} }

View File

@ -1,62 +1,58 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use super::{super::limits::RemainingLimits, BuryMode, DueCard, NewCard, QueueBuilder}; use super::{BuryMode, DueCard, NewCard, QueueBuilder};
use crate::{card::CardQueue, prelude::*}; use crate::{card::CardQueue, prelude::*};
impl QueueBuilder { impl QueueBuilder {
/// Assumes cards will arrive sorted in (queue, due) order, so learning pub(in super::super) fn add_intraday_learning_card(
/// cards come first, and reviews come before day-learning and preview cards. &mut self,
card: DueCard,
bury_mode: BuryMode,
) {
self.get_and_update_bury_mode_for_note(card.note_id, bury_mode);
self.learning.push(card);
}
/// True if limit should be decremented.
pub(in super::super) fn add_due_card( pub(in super::super) fn add_due_card(
&mut self, &mut self,
limit: &mut RemainingLimits,
queue: CardQueue, queue: CardQueue,
card: DueCard, card: DueCard,
bury_mode: BuryMode, bury_mode: BuryMode,
) -> bool { ) -> bool {
let bury_reviews = self let bury_this_card = self
.get_and_update_bury_mode_for_note(card.note_id, bury_mode) .get_and_update_bury_mode_for_note(card.note_id, bury_mode)
.map(|mode| mode.bury_reviews) .map(|mode| mode.bury_reviews)
.unwrap_or_default(); .unwrap_or_default();
if bury_this_card {
match queue { false
CardQueue::Learn | CardQueue::PreviewRepeat => self.learning.push(card), } else {
CardQueue::DayLearn => { match queue {
self.day_learning.push(card); CardQueue::DayLearn => {
} self.day_learning.push(card);
CardQueue::Review => {
if !bury_reviews {
self.review.push(card);
limit.review -= 1;
} }
CardQueue::Review => {
self.review.push(card);
}
_ => unreachable!(),
} }
CardQueue::New
| CardQueue::Suspended
| CardQueue::SchedBuried
| CardQueue::UserBuried => {
unreachable!()
}
}
limit.review != 0 true
}
} }
pub(in super::super) fn add_new_card( // True if limit should be decremented.
&mut self, pub(in super::super) fn add_new_card(&mut self, card: NewCard, bury_mode: BuryMode) -> bool {
limit: &mut RemainingLimits,
card: NewCard,
bury_mode: BuryMode,
) -> bool {
let previous_bury_mode = self let previous_bury_mode = self
.get_and_update_bury_mode_for_note(card.note_id, bury_mode) .get_and_update_bury_mode_for_note(card.note_id, bury_mode)
.map(|mode| mode.bury_new); .map(|mode| mode.bury_new);
// no previous siblings seen? // no previous siblings seen?
if previous_bury_mode.is_none() { if previous_bury_mode.is_none() {
self.new.push(card); self.new.push(card);
limit.new -= 1; return true;
return limit.new != 0;
} }
let bury_new = previous_bury_mode.unwrap(); let bury_this_card = previous_bury_mode.unwrap();
// Cards will be arriving in (due, card_id) order, with all // Cards will be arriving in (due, card_id) order, with all
// siblings sharing the same due number by default. In the // siblings sharing the same due number by default. In the
@ -76,10 +72,12 @@ impl QueueBuilder {
.unwrap_or(false); .unwrap_or(false);
if previous_card_was_sibling_with_higher_ordinal { if previous_card_was_sibling_with_higher_ordinal {
if bury_new { if bury_this_card {
// When burying is enabled, we replace the existing sibling // When burying is enabled, we replace the existing sibling
// with the lower ordinal one. // with the lower ordinal one, and skip decrementing the limit.
*self.new.last_mut().unwrap() = card; *self.new.last_mut().unwrap() = card;
false
} else { } else {
// When burying disabled, we'll want to add this card as well, but we // When burying disabled, we'll want to add this card as well, but we
// need to insert it in front of the later-ordinal card(s). // need to insert it in front of the later-ordinal card(s).
@ -100,17 +98,19 @@ impl QueueBuilder {
.next() .next()
.unwrap_or(0); .unwrap_or(0);
self.new.insert(target_idx, card); self.new.insert(target_idx, card);
limit.new -= 1;
true
} }
} else { } else {
// card has arrived in expected order - add if burying disabled // card has arrived in expected order - add if burying disabled
if !bury_new { if bury_this_card {
false
} else {
self.new.push(card); self.new.push(card);
limit.new -= 1;
true
} }
} }
limit.new != 0
} }
/// If burying is enabled in `new_settings`, existing entry will be updated. /// If burying is enabled in `new_settings`, existing entry will be updated.
@ -142,11 +142,6 @@ mod test {
#[test] #[test]
fn new_siblings() { fn new_siblings() {
let mut builder = QueueBuilder::default(); let mut builder = QueueBuilder::default();
let mut limits = RemainingLimits {
review: 0,
new: 100,
};
let cards = vec![ let cards = vec![
NewCard { NewCard {
id: CardId(1), id: CardId(1),
@ -177,7 +172,6 @@ mod test {
for card in &cards { for card in &cards {
builder.add_new_card( builder.add_new_card(
&mut limits,
card.clone(), card.clone(),
BuryMode { BuryMode {
bury_new: true, bury_new: true,
@ -194,7 +188,7 @@ mod test {
let mut builder = QueueBuilder::default(); let mut builder = QueueBuilder::default();
for card in &cards { for card in &cards {
builder.add_new_card(&mut limits, card.clone(), Default::default()); builder.add_new_card(card.clone(), Default::default());
} }
assert_eq!(builder.new[0].id, CardId(1)); assert_eq!(builder.new[0].id, CardId(1));

View File

@ -11,12 +11,10 @@ use std::collections::{HashMap, VecDeque};
use intersperser::Intersperser; use intersperser::Intersperser;
use sized_chain::SizedChain; use sized_chain::SizedChain;
use super::{ use super::{CardQueues, Counts, LearningQueueEntry, MainQueueEntry, MainQueueEntryKind};
limits::{remaining_limits_capped_to_parents, RemainingLimits},
CardQueues, Counts, LearningQueueEntry, MainQueueEntry, MainQueueEntryKind,
};
use crate::{ use crate::{
deckconfig::{NewCardSortOrder, ReviewCardOrder, ReviewMix}, deckconfig::{NewCardGatherPriority, NewCardSortOrder, ReviewCardOrder, ReviewMix},
decks::limits::{remaining_limits_map, RemainingLimits},
prelude::*, prelude::*,
}; };
@ -28,8 +26,9 @@ pub(crate) struct DueCard {
pub mtime: TimestampSecs, pub mtime: TimestampSecs,
pub due: i32, pub due: i32,
pub interval: u32, pub interval: u32,
pub original_deck_id: DeckId,
pub hash: u64, pub hash: u64,
pub current_deck_id: DeckId,
pub original_deck_id: DeckId,
} }
/// Temporary holder for new cards that will be built into a queue. /// Temporary holder for new cards that will be built into a queue.
@ -85,6 +84,7 @@ pub(super) struct BuryMode {
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]
pub(super) struct QueueSortOptions { pub(super) struct QueueSortOptions {
pub(super) new_order: NewCardSortOrder, pub(super) new_order: NewCardSortOrder,
pub(super) new_gather_priority: NewCardGatherPriority,
pub(super) review_order: ReviewCardOrder, pub(super) review_order: ReviewCardOrder,
pub(super) day_learn_mix: ReviewMix, pub(super) day_learn_mix: ReviewMix,
pub(super) new_review_mix: ReviewMix, pub(super) new_review_mix: ReviewMix,
@ -192,16 +192,17 @@ impl Collection {
pub(crate) fn build_queues(&mut self, deck_id: DeckId) -> Result<CardQueues> { pub(crate) fn build_queues(&mut self, deck_id: DeckId) -> Result<CardQueues> {
let now = TimestampSecs::now(); let now = TimestampSecs::now();
let timing = self.timing_for_timestamp(now)?; let timing = self.timing_for_timestamp(now)?;
let (decks, parent_count) = self.storage.deck_with_parents_and_children(deck_id)?; let decks = self.storage.deck_with_children(deck_id)?;
// need full map, since filtered decks may contain cards from decks/
// outside tree
let deck_map = self.storage.get_decks_map()?; let deck_map = self.storage.get_decks_map()?;
let config = self.storage.get_deck_config_map()?; let config = self.storage.get_deck_config_map()?;
let sort_options = decks let sort_options = decks[0]
.get(parent_count)
.unwrap()
.config_id() .config_id()
.and_then(|config_id| config.get(&config_id)) .and_then(|config_id| config.get(&config_id))
.map(|config| QueueSortOptions { .map(|config| QueueSortOptions {
new_order: config.inner.new_card_sort_order(), new_order: config.inner.new_card_sort_order(),
new_gather_priority: config.inner.new_card_gather_priority(),
review_order: config.inner.review_order(), review_order: config.inner.review_order(),
day_learn_mix: config.inner.interday_learning_mix(), day_learn_mix: config.inner.interday_learning_mix(),
new_review_mix: config.inner.new_mix(), new_review_mix: config.inner.new_mix(),
@ -213,9 +214,18 @@ impl Collection {
..Default::default() ..Default::default()
} }
}); });
let limits = remaining_limits_capped_to_parents(&decks, &config, timing.days_elapsed);
let selected_deck_limits = limits[parent_count]; // fetch remaining limits, and cap to selected deck limits so that we don't
let mut queues = QueueBuilder::new(sort_options); // do more work than necessary
let mut remaining = remaining_limits_map(decks.iter(), &config, timing.days_elapsed);
let selected_deck_limits_at_start = *remaining.get(&deck_id).unwrap();
let mut selected_deck_limits = selected_deck_limits_at_start;
for limit in remaining.values_mut() {
limit.cap_to(selected_deck_limits);
}
self.storage.update_active_decks(&decks[0])?;
let mut queues = QueueBuilder::new(sort_options.clone());
let get_bury_mode = |home_deck: DeckId| { let get_bury_mode = |home_deck: DeckId| {
deck_map deck_map
@ -229,28 +239,70 @@ impl Collection {
.unwrap_or_default() .unwrap_or_default()
}; };
for (deck, mut limit) in decks.iter().zip(limits).skip(parent_count) { // intraday cards first, noting down any notes that will need burying
if limit.review > 0 { self.storage
self.storage.for_each_due_card_in_deck( .for_each_intraday_card_in_active_decks(timing.next_day_at, |card| {
timing.days_elapsed, let bury = get_bury_mode(card.current_deck_id);
timing.next_day_at, queues.add_intraday_learning_card(card, bury)
deck.id, })?;
|queue, card| {
let bury = get_bury_mode(card.original_deck_id.or(deck.id)); // reviews and interday learning next
queues.add_due_card(&mut limit, queue, card, bury) if selected_deck_limits.review != 0 {
}, self.storage.for_each_review_card_in_active_decks(
)?; timing.days_elapsed,
sort_options.review_order,
|queue, card| {
if selected_deck_limits.review == 0 {
return false;
}
let bury = get_bury_mode(card.original_deck_id.or(card.current_deck_id));
let limits = remaining.get_mut(&card.current_deck_id).unwrap();
if limits.review != 0 && queues.add_due_card(queue, card, bury) {
selected_deck_limits.review -= 1;
limits.review -= 1;
}
true
},
)?;
}
// New cards last
for limit in remaining.values_mut() {
limit.new = limit.new.min(limit.review).min(selected_deck_limits.review);
}
selected_deck_limits.new = selected_deck_limits.new.min(selected_deck_limits.review);
let can_exit_early = sort_options.new_gather_priority == NewCardGatherPriority::Deck;
for deck in &decks {
if can_exit_early && selected_deck_limits.new == 0 {
break;
} }
let limit = remaining.get_mut(&deck.id).unwrap();
if limit.new > 0 { if limit.new > 0 {
self.storage.for_each_new_card_in_deck(deck.id, |card| { self.storage.for_each_new_card_in_deck(deck.id, |card| {
let bury = get_bury_mode(card.original_deck_id.or(deck.id)); let bury = get_bury_mode(card.original_deck_id.or(deck.id));
queues.add_new_card(&mut limit, card, bury) if limit.new != 0 {
if queues.add_new_card(card, bury) {
limit.new -= 1;
selected_deck_limits.new = selected_deck_limits.new.saturating_sub(1);
}
true
} else {
false
}
})?; })?;
} }
} }
let final_limits = RemainingLimits {
new: selected_deck_limits_at_start
.new
.min(selected_deck_limits.review),
..selected_deck_limits_at_start
};
let queues = queues.build( let queues = queues.build(
selected_deck_limits, final_limits,
self.learn_ahead_secs() as i64, self.learn_ahead_secs() as i64,
deck_id, deck_id,
timing.days_elapsed, timing.days_elapsed,

View File

@ -31,23 +31,10 @@ impl QueueBuilder {
.for_each(DueCard::hash_id_and_mtime); .for_each(DueCard::hash_id_and_mtime);
self.day_learning.sort_unstable_by(day_then_hash); self.day_learning.sort_unstable_by(day_then_hash);
match self.sort_options.review_order { // other sorting is done in SQL
ReviewCardOrder::DayThenRandom => { if self.sort_options.review_order == ReviewCardOrder::DayThenRandom {
self.review.iter_mut().for_each(DueCard::hash_id_and_mtime); self.review.iter_mut().for_each(DueCard::hash_id_and_mtime);
self.review.sort_unstable_by(day_then_hash); self.review.sort_unstable_by(day_then_hash);
}
ReviewCardOrder::IntervalsAscending => {
self.review.sort_unstable_by(intervals_ascending);
}
ReviewCardOrder::IntervalsDescending => {
self.review
.sort_unstable_by(|a, b| intervals_ascending(b, a));
} // ReviewCardOrder::RelativeOverdue => {
// self.review
// .iter_mut()
// .for_each(|card| card.set_hash_to_relative_overdue(current_day));
// self.review.sort_unstable_by(due_card_hash)
// }
} }
} }
} }
@ -72,15 +59,6 @@ fn day_then_hash(a: &DueCard, b: &DueCard) -> Ordering {
(a.due, a.hash).cmp(&(b.due, b.hash)) (a.due, a.hash).cmp(&(b.due, b.hash))
} }
#[allow(dead_code)]
fn due_card_hash(a: &DueCard, b: &DueCard) -> Ordering {
a.hash.cmp(&b.hash)
}
fn intervals_ascending(a: &DueCard, b: &DueCard) -> Ordering {
(a.interval, a.hash).cmp(&(a.interval, b.hash))
}
// We sort based on a hash so that if the queue is rebuilt, remaining // We sort based on a hash so that if the queue is rebuilt, remaining
// cards come back in the same approximate order (mixing + due learning cards // cards come back in the same approximate order (mixing + due learning cards
// may still result in a different card) // may still result in a different card)

View File

@ -1,219 +0,0 @@
// 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 super::{Deck, DeckKind};
use crate::deckconfig::{DeckConfig, DeckConfigId};
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct RemainingLimits {
pub review: u32,
pub new: u32,
}
impl RemainingLimits {
pub(crate) fn new(deck: &Deck, config: Option<&DeckConfig>, today: u32) -> Self {
if let Some(config) = config {
let (new_today, rev_today) = deck.new_rev_counts(today);
RemainingLimits {
review: ((config.inner.reviews_per_day as i32) - rev_today).max(0) as u32,
new: ((config.inner.new_per_day as i32) - new_today).max(0) as u32,
}
} else {
RemainingLimits {
review: std::u32::MAX,
new: std::u32::MAX,
}
}
}
fn limit_to_parent(&mut self, parent: RemainingLimits) {
self.review = self.review.min(parent.review);
self.new = self.new.min(parent.new);
}
}
pub(super) fn remaining_limits_capped_to_parents(
decks: &[Deck],
config: &HashMap<DeckConfigId, DeckConfig>,
today: u32,
) -> Vec<RemainingLimits> {
let mut limits = get_remaining_limits(decks, config, today);
cap_limits_to_parents(decks.iter().map(|d| d.name.as_native_str()), &mut limits);
limits
}
/// Return the remaining limits for each of the provided decks, in
/// the provided deck order.
fn get_remaining_limits(
decks: &[Deck],
config: &HashMap<DeckConfigId, DeckConfig>,
today: u32,
) -> Vec<RemainingLimits> {
decks
.iter()
.map(move |deck| {
// get deck config if not filtered
let config = if let DeckKind::Normal(normal) = &deck.kind {
config.get(&DeckConfigId(normal.config_id))
} else {
None
};
RemainingLimits::new(deck, config, today)
})
.collect()
}
/// Given a sorted list of deck names and their current limits,
/// cap child limits to their parents.
fn cap_limits_to_parents<'a>(
names: impl IntoIterator<Item = &'a str>,
limits: &'a mut Vec<RemainingLimits>,
) {
let mut parent_limits = vec![];
let mut last_limit = None;
let mut last_level = 0;
names
.into_iter()
.zip(limits.iter_mut())
.for_each(|(name, limits)| {
let level = name.matches('\x1f').count() + 1;
if last_limit.is_none() {
// top-level deck
last_limit = Some(*limits);
last_level = level;
} else {
// add/remove parent limits if descending/ascending
let mut target = level;
while target != last_level {
if target < last_level {
// current deck is at higher level than previous
parent_limits.pop();
target += 1;
} else {
// current deck is at a lower level than previous. this
// will push the same remaining counts multiple times if
// the deck tree is missing a parent
parent_limits.push(last_limit.unwrap());
target -= 1;
}
}
// apply current parent limit
limits.limit_to_parent(*parent_limits.last().unwrap());
last_level = level;
last_limit = Some(*limits);
}
})
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn limits() {
let limits_map = vec![
(
"A",
RemainingLimits {
review: 100,
new: 20,
},
),
(
"A\x1fB",
RemainingLimits {
review: 50,
new: 30,
},
),
(
"A\x1fC",
RemainingLimits {
review: 10,
new: 10,
},
),
("A\x1fC\x1fD", RemainingLimits { review: 5, new: 30 }),
(
"A\x1fE",
RemainingLimits {
review: 200,
new: 100,
},
),
];
let (names, mut limits): (Vec<_>, Vec<_>) = limits_map.into_iter().unzip();
cap_limits_to_parents(names.into_iter(), &mut limits);
assert_eq!(
&limits,
&[
RemainingLimits {
review: 100,
new: 20
},
RemainingLimits {
review: 50,
new: 20
},
RemainingLimits {
review: 10,
new: 10
},
RemainingLimits { review: 5, new: 10 },
RemainingLimits {
review: 100,
new: 20
}
]
);
// missing parents should not break it
let limits_map = vec![
(
"A",
RemainingLimits {
review: 100,
new: 20,
},
),
(
"A\x1fB\x1fC\x1fD",
RemainingLimits {
review: 50,
new: 30,
},
),
(
"A\x1fC",
RemainingLimits {
review: 100,
new: 100,
},
),
];
let (names, mut limits): (Vec<_>, Vec<_>) = limits_map.into_iter().unzip();
cap_limits_to_parents(names.into_iter(), &mut limits);
assert_eq!(
&limits,
&[
RemainingLimits {
review: 100,
new: 20
},
RemainingLimits {
review: 50,
new: 20
},
RemainingLimits {
review: 100,
new: 20
},
]
);
}
}

View File

@ -4,11 +4,10 @@
mod builder; mod builder;
mod entry; mod entry;
mod learning; mod learning;
mod limits;
mod main; mod main;
pub(crate) mod undo; pub(crate) mod undo;
use std::collections::VecDeque; use std::{collections::VecDeque, time::Instant};
pub(crate) use builder::{DueCard, NewCard}; pub(crate) use builder::{DueCard, NewCard};
pub(crate) use entry::{QueueEntry, QueueEntryKind}; pub(crate) use entry::{QueueEntry, QueueEntryKind};
@ -171,7 +170,9 @@ impl Collection {
self.discard_undo_and_study_queues(); self.discard_undo_and_study_queues();
} }
if self.state.card_queues.is_none() { if self.state.card_queues.is_none() {
let now = Instant::now();
self.state.card_queues = Some(self.build_queues(deck)?); self.state.card_queues = Some(self.build_queues(deck)?);
println!("queue build in {:?}", now.elapsed());
} }
Ok(self.state.card_queues.as_mut().unwrap()) Ok(self.state.card_queues.as_mut().unwrap())

View File

@ -4,16 +4,14 @@ SELECT queue,
due, due,
cast(ivl AS integer), cast(ivl AS integer),
cast(mod AS integer), cast(mod AS integer),
did,
odid odid
FROM cards FROM cards
WHERE did = ?1 WHERE did IN (
SELECT id
FROM active_decks
)
AND ( AND (
( queue IN (2, 3)
queue IN (2, 3) AND due <= ?
AND due <= ?2
)
OR (
queue IN (1, 4)
AND due <= ?3
)
) )

View File

@ -0,0 +1,15 @@
SELECT id,
nid,
due,
cast(mod AS integer),
did,
odid
FROM cards
WHERE did IN (
SELECT id
FROM active_decks
)
AND (
queue IN (1, 4)
AND due <= ?
)

View File

@ -14,7 +14,7 @@ use rusqlite::{
use super::ids_to_string; use super::ids_to_string;
use crate::{ use crate::{
card::{Card, CardId, CardQueue, CardType}, card::{Card, CardId, CardQueue, CardType},
deckconfig::DeckConfigId, deckconfig::{DeckConfigId, ReviewCardOrder},
decks::{Deck, DeckId, DeckKind}, decks::{Deck, DeckId, DeckKind},
error::Result, error::Result,
notes::NoteId, notes::NoteId,
@ -164,25 +164,52 @@ impl super::SqliteStorage {
Ok(()) Ok(())
} }
/// Call func() for each due card, stopping when it returns false pub(crate) fn for_each_intraday_card_in_active_decks<F>(
/// or no more cards found. &self,
pub(crate) fn for_each_due_card_in_deck<F>( learn_cutoff: TimestampSecs,
mut func: F,
) -> Result<()>
where
F: FnMut(DueCard),
{
let mut stmt = self.db.prepare_cached(include_str!("intraday_due.sql"))?;
let mut rows = stmt.query(params![learn_cutoff])?;
while let Some(row) = rows.next()? {
func(DueCard {
id: row.get(0)?,
note_id: row.get(1)?,
due: row.get(2).ok().unwrap_or_default(),
mtime: row.get(3)?,
current_deck_id: row.get(4)?,
original_deck_id: row.get(5)?,
interval: 0,
hash: 0,
})
}
Ok(())
}
pub(crate) fn for_each_review_card_in_active_decks<F>(
&self, &self,
day_cutoff: u32, day_cutoff: u32,
learn_cutoff: TimestampSecs, order: ReviewCardOrder,
deck: DeckId,
mut func: F, mut func: F,
) -> Result<()> ) -> Result<()>
where where
F: FnMut(CardQueue, DueCard) -> bool, F: FnMut(CardQueue, DueCard) -> bool,
{ {
let mut stmt = self.db.prepare_cached(include_str!("due_cards.sql"))?; let order_clause = match order {
let mut rows = stmt.query(params![ ReviewCardOrder::DayThenRandom => "order by due",
// with many subdecks, avoiding named params shaves off a few milliseconds ReviewCardOrder::IntervalsAscending => "order by ivl asc",
deck, ReviewCardOrder::IntervalsDescending => "order by ivl desc",
day_cutoff, };
learn_cutoff let mut stmt = self.db.prepare_cached(&format!(
])?; "{} {}",
include_str!("due_cards.sql"),
order_clause
))?;
let mut rows = stmt.query(params![day_cutoff])?;
while let Some(row) = rows.next()? { while let Some(row) = rows.next()? {
let queue: CardQueue = row.get(0)?; let queue: CardQueue = row.get(0)?;
if !func( if !func(
@ -193,8 +220,9 @@ impl super::SqliteStorage {
due: row.get(3).ok().unwrap_or_default(), due: row.get(3).ok().unwrap_or_default(),
interval: row.get(4)?, interval: row.get(4)?,
mtime: row.get(5)?, mtime: row.get(5)?,
current_deck_id: row.get(6)?,
original_deck_id: row.get(7)?,
hash: 0, hash: 0,
original_deck_id: row.get(6)?,
}, },
) { ) {
break; break;
@ -408,6 +436,8 @@ impl super::SqliteStorage {
} }
pub(crate) fn congrats_info(&self, current: &Deck, today: u32) -> Result<CongratsInfo> { pub(crate) fn congrats_info(&self, current: &Deck, today: u32) -> Result<CongratsInfo> {
// FIXME: when v1/v2 are dropped, this line will become obsolete, as it's run
// on queue build by v3
self.update_active_decks(current)?; self.update_active_decks(current)?;
self.db self.db
.prepare(include_str!("congrats.sql"))? .prepare(include_str!("congrats.sql"))?

View File

@ -4,7 +4,12 @@ SELECT did,
queue = :review_queue queue = :review_queue
AND due <= :day_cutoff AND due <= :day_cutoff
), ),
-- learning -- interday learning
sum(
queue = :daylearn_queue
AND due <= :day_cutoff
),
-- intraday learning
sum( sum(
( (
CASE CASE
@ -15,10 +20,6 @@ SELECT did,
queue = :learn_queue queue = :learn_queue
AND due < :learn_cutoff AND due < :learn_cutoff
) )
OR (
queue = :daylearn_queue
AND due <= :day_cutoff
)
OR ( OR (
queue = :preview_queue queue = :preview_queue
AND due <= :learn_cutoff AND due <= :learn_cutoff
@ -29,8 +30,6 @@ SELECT did,
CASE CASE
WHEN queue = :learn_queue WHEN queue = :learn_queue
AND due < :learn_cutoff THEN left / 1000 AND due < :learn_cutoff THEN left / 1000
WHEN queue = :daylearn_queue
AND due <= :day_cutoff THEN 1
ELSE 0 ELSE 0
END END
) )

View File

@ -1,7 +1,10 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::collections::{HashMap, HashSet}; use std::{
collections::{HashMap, HashSet},
iter,
};
use prost::Message; use prost::Message;
use rusqlite::{named_params, params, Row, NO_PARAMS}; use rusqlite::{named_params, params, Row, NO_PARAMS};
@ -35,13 +38,24 @@ fn row_to_deck(row: &Row) -> Result<Deck> {
}) })
} }
fn row_to_due_counts(row: &Row) -> Result<(DeckId, DueCounts)> { fn row_to_due_counts(row: &Row, v3: bool) -> Result<(DeckId, DueCounts)> {
let deck_id = row.get(0)?;
let new = row.get(1)?;
let mut review = row.get(2)?;
let interday: u32 = row.get(3)?;
let intraday: u32 = row.get(4)?;
let learning = if v3 {
review += interday;
intraday
} else {
intraday + interday
};
Ok(( Ok((
row.get(0)?, deck_id,
DueCounts { DueCounts {
new: row.get(1)?, new,
review: row.get(2)?, review,
learning: row.get(3)?, learning,
}, },
)) ))
} }
@ -193,24 +207,11 @@ impl SqliteStorage {
.collect() .collect()
} }
/// Return the provided deck with its parents and children in an ordered list, and pub(crate) fn deck_with_children(&self, deck_id: DeckId) -> Result<Vec<Deck>> {
/// the number of parent decks that need to be skipped to get to the chosen deck.
pub(crate) fn deck_with_parents_and_children(
&self,
deck_id: DeckId,
) -> Result<(Vec<Deck>, usize)> {
let deck = self.get_deck(deck_id)?.ok_or(AnkiError::NotFound)?; let deck = self.get_deck(deck_id)?.ok_or(AnkiError::NotFound)?;
let mut parents = self.parent_decks(&deck)?;
parents.reverse();
let parent_count = parents.len();
let prefix_start = format!("{}\x1f", deck.name); let prefix_start = format!("{}\x1f", deck.name);
let prefix_end = format!("{}\x20", deck.name); let prefix_end = format!("{}\x20", deck.name);
parents.push(deck); iter::once(Ok(deck))
let decks = parents
.into_iter()
.map(Result::Ok)
.chain( .chain(
self.db self.db
.prepare_cached(concat!( .prepare_cached(concat!(
@ -219,9 +220,7 @@ impl SqliteStorage {
))? ))?
.query_and_then(&[prefix_start, prefix_end], row_to_deck)?, .query_and_then(&[prefix_start, prefix_end], row_to_deck)?,
) )
.collect::<Result<_>>()?; .collect()
Ok((decks, parent_count))
} }
/// Return the parents of `child`, with the most immediate parent coming first. /// Return the parents of `child`, with the most immediate parent coming first.
@ -252,6 +251,7 @@ impl SqliteStorage {
day_cutoff: u32, day_cutoff: u32,
learn_cutoff: u32, learn_cutoff: u32,
top_deck: Option<&str>, top_deck: Option<&str>,
v3: bool,
) -> Result<HashMap<DeckId, DueCounts>> { ) -> Result<HashMap<DeckId, DueCounts>> {
let sched_ver = sched as u8; let sched_ver = sched as u8;
let mut params = named_params! { let mut params = named_params! {
@ -292,7 +292,7 @@ impl SqliteStorage {
self.db self.db
.prepare_cached(sql)? .prepare_cached(sql)?
.query_and_then_named(&params, row_to_due_counts)? .query_and_then_named(&params, |row| row_to_due_counts(row, v3))?
.collect() .collect()
} }
@ -323,7 +323,7 @@ impl SqliteStorage {
/// Write active decks into temporary active_decks table. /// Write active decks into temporary active_decks table.
pub(crate) fn update_active_decks(&self, current: &Deck) -> Result<()> { pub(crate) fn update_active_decks(&self, current: &Deck) -> Result<()> {
self.db.execute_batch(concat!( self.db.execute_batch(concat!(
"drop table if exists temp.active_decks;", "drop table if exists active_decks;",
"create temporary table active_decks (id integer primary key not null);" "create temporary table active_decks (id integer primary key not null);"
))?; ))?;

View File

@ -67,10 +67,3 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
defaultValue={defaults.lapseMultiplier} defaultValue={defaults.lapseMultiplier}
value={$config.lapseMultiplier} value={$config.lapseMultiplier}
on:changed={(evt) => ($config.lapseMultiplier = evt.detail.value)} /> on:changed={(evt) => ($config.lapseMultiplier = evt.detail.value)} />
<SpinBox
label={tr.schedulingMinimumInterval()}
tooltip={tr.deckConfigMinimumIntervalTooltip()}
min={1}
defaultValue={defaults.minimumLapseInterval}
bind:value={$config.minimumLapseInterval} />

View File

@ -8,6 +8,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import NewOptions from "./NewOptions.svelte"; import NewOptions from "./NewOptions.svelte";
import AdvancedOptions from "./AdvancedOptions.svelte"; import AdvancedOptions from "./AdvancedOptions.svelte";
import ReviewOptions from "./ReviewOptions.svelte"; import ReviewOptions from "./ReviewOptions.svelte";
import LapseOptions from "./LapseOptions.svelte";
import GeneralOptions from "./GeneralOptions.svelte"; import GeneralOptions from "./GeneralOptions.svelte";
import Addons from "./Addons.svelte"; import Addons from "./Addons.svelte";
import type { DeckOptionsState } from "./lib"; import type { DeckOptionsState } from "./lib";
@ -30,6 +31,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<LearningOptions {state} /> <LearningOptions {state} />
<NewOptions {state} /> <NewOptions {state} />
<ReviewOptions {state} /> <ReviewOptions {state} />
<LapseOptions {state} />
<GeneralOptions {state} /> <GeneralOptions {state} />
<Addons {state} /> <Addons {state} />
<AdvancedOptions {state} /> <AdvancedOptions {state} />

View File

@ -13,7 +13,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let parentLimits = state.parentLimits; let parentLimits = state.parentLimits;
$: newCardsGreaterThanParent = $: newCardsGreaterThanParent =
$config.newPerDay > $parentLimits.newCards !state.v3Scheduler && $config.newPerDay > $parentLimits.newCards
? tr.deckConfigDailyLimitWillBeCapped({ cards: $parentLimits.newCards }) ? tr.deckConfigDailyLimitWillBeCapped({ cards: $parentLimits.newCards })
: ""; : "";

View File

@ -0,0 +1,61 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import * as tr from "lib/i18n";
import SpinBox from "./SpinBox.svelte";
import EnumSelector from "./EnumSelector.svelte";
import StepsInput from "./StepsInput.svelte";
import type { DeckOptionsState } from "./lib";
export let state: DeckOptionsState;
let config = state.currentConfig;
let defaults = state.defaults;
let stepsExceedMinimumInterval: string;
$: {
const lastRelearnStepInDays = $config.relearnSteps.length
? $config.relearnSteps[$config.relearnSteps.length - 1] / 60 / 24
: 0;
stepsExceedMinimumInterval =
lastRelearnStepInDays > $config.minimumLapseInterval
? tr.deckConfigRelearningStepsAboveMinimumInterval()
: "";
}
const leechChoices = [tr.actionsSuspendCard(), tr.schedulingTagOnly()];
</script>
<div>
<h2>{tr.schedulingLapses()}</h2>
<StepsInput
label={tr.deckConfigRelearningSteps()}
tooltip={tr.deckConfigRelearningStepsTooltip()}
defaultValue={defaults.relearnSteps}
value={$config.relearnSteps}
on:changed={(evt) => ($config.relearnSteps = evt.detail.value)} />
<SpinBox
label={tr.schedulingMinimumInterval()}
tooltip={tr.deckConfigMinimumIntervalTooltip()}
warnings={[stepsExceedMinimumInterval]}
min={1}
defaultValue={defaults.minimumLapseInterval}
bind:value={$config.minimumLapseInterval} />
<SpinBox
label={tr.schedulingLeechThreshold()}
tooltip={tr.deckConfigLeechThresholdTooltip()}
min={1}
defaultValue={defaults.leechThreshold}
bind:value={$config.leechThreshold} />
<EnumSelector
label={tr.schedulingLeechAction()}
tooltip={tr.deckConfigLeechActionTooltip()}
choices={leechChoices}
defaultValue={defaults.leechAction}
bind:value={$config.leechAction} />
</div>

View File

@ -14,7 +14,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let defaults = state.defaults; let defaults = state.defaults;
</script> </script>
<h2>{tr.deckConfigLearningRelearningTitle()}</h2> <h2>{tr.deckConfigLearningTitle()}</h2>
<StepsInput <StepsInput
label={tr.deckConfigLearningSteps()} label={tr.deckConfigLearningSteps()}
@ -23,13 +23,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
value={$config.learnSteps} value={$config.learnSteps}
on:changed={(evt) => ($config.learnSteps = evt.detail.value)} /> on:changed={(evt) => ($config.learnSteps = evt.detail.value)} />
<StepsInput
label={tr.deckConfigRelearningSteps()}
tooltip={tr.deckConfigRelearningStepsTooltip()}
defaultValue={defaults.relearnSteps}
value={$config.relearnSteps}
on:changed={(evt) => ($config.relearnSteps = evt.detail.value)} />
{#if state.v3Scheduler} {#if state.v3Scheduler}
<EnumSelector <EnumSelector
label={tr.deckConfigInterdayStepPriority()} label={tr.deckConfigInterdayStepPriority()}

View File

@ -14,10 +14,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let config = state.currentConfig; let config = state.currentConfig;
let defaults = state.defaults; let defaults = state.defaults;
const newFetchOrderChoices = [ const newInsertOrderChoices = [
tr.deckConfigNewInsertionOrderSequential(), tr.deckConfigNewInsertionOrderSequential(),
tr.deckConfigNewInsertionOrderRandom(), tr.deckConfigNewInsertionOrderRandom(),
]; ];
const newGatherPriorityChoices = [
tr.deckConfigNewGatherPriorityDeck(),
tr.deckConfigNewGatherPriorityPosition(),
];
const newSortOrderChoices = [ const newSortOrderChoices = [
tr.deckConfigSortOrderCardTemplateThenPosition(), tr.deckConfigSortOrderCardTemplateThenPosition(),
tr.deckConfigSortOrderCardTemplateThenRandom(), tr.deckConfigSortOrderCardTemplateThenRandom(),
@ -62,9 +66,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<EnumSelector <EnumSelector
label={tr.deckConfigNewInsertionOrder()} label={tr.deckConfigNewInsertionOrder()}
tooltip={tr.deckConfigNewInsertionOrderTooltip()} tooltip={tr.deckConfigNewInsertionOrderTooltip()}
choices={newFetchOrderChoices} choices={newInsertOrderChoices}
defaultValue={defaults.newCardFetchOrder} defaultValue={defaults.newCardInsertOrder}
bind:value={$config.newCardFetchOrder} /> bind:value={$config.newCardInsertOrder} />
<EnumSelector
label={tr.deckConfigNewGatherPriority()}
tooltip={tr.deckConfigNewGatherPriorityTooltip()}
choices={newGatherPriorityChoices}
defaultValue={defaults.newCardGatherPriority}
bind:value={$config.newCardGatherPriority} />
<EnumSelector <EnumSelector
label={tr.deckConfigSortOrder()} label={tr.deckConfigSortOrder()}
@ -74,8 +85,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
bind:value={$config.newCardSortOrder} /> bind:value={$config.newCardSortOrder} />
<EnumSelector <EnumSelector
label={tr.deckConfigNewPriority()} label={tr.deckConfigReviewPriority()}
tooltip={tr.deckConfigNewPriorityTooltip()} tooltip={tr.deckConfigReviewPriorityTooltip()}
choices={reviewMixChoices()} choices={reviewMixChoices()}
defaultValue={defaults.newMix} defaultValue={defaults.newMix}
bind:value={$config.newMix} /> bind:value={$config.newMix} />

View File

@ -4,7 +4,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import * as tr from "lib/i18n"; import * as tr from "lib/i18n";
import SpinBox from "./SpinBox.svelte";
import CheckBox from "./CheckBox.svelte"; import CheckBox from "./CheckBox.svelte";
import EnumSelector from "./EnumSelector.svelte"; import EnumSelector from "./EnumSelector.svelte";
import type { DeckOptionsState } from "./lib"; import type { DeckOptionsState } from "./lib";
@ -18,8 +17,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
tr.deckConfigSortOrderAscendingIntervals(), tr.deckConfigSortOrderAscendingIntervals(),
tr.deckConfigSortOrderDescendingIntervals(), tr.deckConfigSortOrderDescendingIntervals(),
]; ];
const leechChoices = [tr.actionsSuspendCard(), tr.schedulingTagOnly()];
</script> </script>
<div> <div>
@ -32,20 +29,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
defaultValue={defaults.reviewOrder} defaultValue={defaults.reviewOrder}
bind:value={$config.reviewOrder} /> bind:value={$config.reviewOrder} />
<SpinBox
label={tr.schedulingLeechThreshold()}
tooltip={tr.deckConfigLeechThresholdTooltip()}
min={1}
defaultValue={defaults.leechThreshold}
bind:value={$config.leechThreshold} />
<EnumSelector
label={tr.schedulingLeechAction()}
tooltip={tr.deckConfigLeechActionTooltip()}
choices={leechChoices}
defaultValue={defaults.leechAction}
bind:value={$config.leechAction} />
<CheckBox <CheckBox
label={tr.deckConfigBuryReviewSiblings()} label={tr.deckConfigBuryReviewSiblings()}
tooltip={tr.deckConfigBuryTooltip()} tooltip={tr.deckConfigBuryTooltip()}