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:
parent
35063316d3
commit
13519a929c
@ -21,9 +21,9 @@ deck-config-review-limit-tooltip =
|
||||
The maximum number of review cards to show in a day,
|
||||
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
|
||||
# Please don't translate '5m' or '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.
|
||||
Once all steps have been passed, the card will become a review card, and
|
||||
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-tooltip = When to show (re)learning cards that cross a day boundary.
|
||||
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-tooltip =
|
||||
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.
|
||||
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-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-tooltip =
|
||||
After today's new cards have been gathered, this option controls the order
|
||||
they are then presented in. The default is to sort by card template first, to avoid
|
||||
multiple cards of the same note from being shown in succession.
|
||||
This option controls how cards are sorted after they have been gathered.
|
||||
By default, Anki sorts by card template first, to avoid multiple cards of
|
||||
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-random = Card template, then random
|
||||
deck-config-sort-order-position = Position (siblings together)
|
||||
deck-config-sort-order-random = Random
|
||||
deck-config-new-priority = Priority
|
||||
deck-config-new-priority-tooltip = When to show new cards in a study session.
|
||||
deck-config-review-priority = Review priority
|
||||
deck-config-review-priority-tooltip = When to show these cards in relation to review cards.
|
||||
|
||||
## Review section
|
||||
|
||||
deck-config-review-sort-order-tooltip =
|
||||
After today's review cards have been gathered, this option controls the order
|
||||
they are then presented in. The default is to sort by due date, then shuffle, so
|
||||
that if you have a backlog of reviews, the cards that have been waiting longest
|
||||
will be shown first. The other choices can be useful when you have a large backlog
|
||||
and want to tackle it in a different way.
|
||||
The default order fetches cards from each subdeck in turn, stopping when the limit
|
||||
of the selected deck has been reached. The gathered cards are then shuffled together,
|
||||
and shown in due date order. Because gathering stops when the parent limit has been
|
||||
reached, your child decks should have smaller limits if you wish to see cards from
|
||||
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-ascending-intervals = Ascending 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 =
|
||||
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
|
||||
@ -185,3 +202,4 @@ deck-config-reviews-too-low =
|
||||
}, 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-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.
|
||||
|
@ -475,8 +475,6 @@ def review_limits_setup() -> Tuple[anki.collection.Collection, Dict]:
|
||||
|
||||
|
||||
def test_review_limits():
|
||||
if is_2021():
|
||||
pytest.skip("old sched only")
|
||||
col, child = review_limits_setup()
|
||||
|
||||
tree = col.sched.deck_due_tree().children
|
||||
@ -499,30 +497,6 @@ def test_review_limits():
|
||||
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():
|
||||
col = getEmptyCol()
|
||||
note = col.newNote()
|
||||
|
@ -309,9 +309,13 @@ service CardsService {
|
||||
|
||||
message DeckConfig {
|
||||
message Config {
|
||||
enum NewCardFetchOrder {
|
||||
NEW_CARD_FETCH_ORDER_DUE = 0;
|
||||
NEW_CARD_FETCH_ORDER_RANDOM = 1;
|
||||
enum NewCardInsertOrder {
|
||||
NEW_CARD_INSERT_ORDER_DUE = 0;
|
||||
NEW_CARD_INSERT_ORDER_RANDOM = 1;
|
||||
}
|
||||
enum NewCardGatherPriority {
|
||||
NEW_CARD_GATHER_PRIORITY_DECK = 0;
|
||||
NEW_CARD_GATHER_PRIORITY_POSITION = 1;
|
||||
}
|
||||
enum NewCardSortOrder {
|
||||
NEW_CARD_SORT_ORDER_TEMPLATE_THEN_DUE = 0;
|
||||
@ -356,11 +360,13 @@ message DeckConfig {
|
||||
uint32 graduating_interval_good = 18;
|
||||
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;
|
||||
ReviewMix new_mix = 30;
|
||||
|
||||
ReviewCardOrder review_order = 33;
|
||||
|
||||
ReviewMix new_mix = 30;
|
||||
ReviewMix interday_learning_mix = 31;
|
||||
|
||||
LeechAction leech_action = 21;
|
||||
@ -659,7 +665,6 @@ message DeckTreeIn {
|
||||
message DeckTreeNode {
|
||||
int64 deck_id = 1;
|
||||
string name = 2;
|
||||
repeated DeckTreeNode children = 3;
|
||||
uint32 level = 4;
|
||||
bool collapsed = 5;
|
||||
|
||||
@ -668,6 +673,10 @@ message DeckTreeNode {
|
||||
uint32 new_count = 8;
|
||||
|
||||
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 {
|
||||
|
@ -9,7 +9,10 @@ pub use schema11::{DeckConfSchema11, NewCardOrderSchema11};
|
||||
pub use update::UpdateDeckConfigsIn;
|
||||
|
||||
pub use crate::backend_proto::deck_config::{
|
||||
config::{LeechAction, NewCardFetchOrder, NewCardSortOrder, ReviewCardOrder, ReviewMix},
|
||||
config::{
|
||||
LeechAction, NewCardGatherPriority, NewCardInsertOrder, NewCardSortOrder, ReviewCardOrder,
|
||||
ReviewMix,
|
||||
},
|
||||
Config as DeckConfigInner,
|
||||
};
|
||||
|
||||
@ -58,7 +61,8 @@ impl Default for DeckConfig {
|
||||
minimum_lapse_interval: 1,
|
||||
graduating_interval_good: 1,
|
||||
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,
|
||||
review_order: ReviewCardOrder::DayThenRandom as i32,
|
||||
new_mix: ReviewMix::MixWithReviews as i32,
|
||||
|
@ -10,7 +10,7 @@ use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use serde_tuple::Serialize_tuple;
|
||||
|
||||
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};
|
||||
|
||||
@ -48,6 +48,8 @@ pub struct DeckConfSchema11 {
|
||||
review_order: i32,
|
||||
#[serde(default)]
|
||||
new_sort_order: i32,
|
||||
#[serde(default)]
|
||||
new_gather_priority: i32,
|
||||
|
||||
#[serde(flatten)]
|
||||
other: HashMap<String, Value>,
|
||||
@ -213,6 +215,7 @@ impl Default for DeckConfSchema11 {
|
||||
interday_learning_mix: 0,
|
||||
review_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,
|
||||
graduating_interval_good: c.new.ints.good as u32,
|
||||
graduating_interval_easy: c.new.ints.easy as u32,
|
||||
new_card_fetch_order: match c.new.order {
|
||||
NewCardOrderSchema11::Random => NewCardFetchOrder::Random,
|
||||
NewCardOrderSchema11::Due => NewCardFetchOrder::Due,
|
||||
new_card_insert_order: match c.new.order {
|
||||
NewCardOrderSchema11::Random => NewCardInsertOrder::Random,
|
||||
NewCardOrderSchema11::Due => NewCardInsertOrder::Due,
|
||||
} as i32,
|
||||
new_card_gather_priority: c.new_gather_priority,
|
||||
new_card_sort_order: c.new_sort_order,
|
||||
review_order: c.review_order,
|
||||
new_mix: c.new_mix,
|
||||
@ -312,7 +316,7 @@ impl From<DeckConfig> for DeckConfSchema11 {
|
||||
}
|
||||
}
|
||||
let i = c.inner;
|
||||
let new_order = i.new_card_fetch_order();
|
||||
let new_order = i.new_card_insert_order();
|
||||
DeckConfSchema11 {
|
||||
id: c.id,
|
||||
mtime: c.mtime_secs,
|
||||
@ -333,8 +337,8 @@ impl From<DeckConfig> for DeckConfSchema11 {
|
||||
_unused: 0,
|
||||
},
|
||||
order: match new_order {
|
||||
NewCardFetchOrder::Random => NewCardOrderSchema11::Random,
|
||||
NewCardFetchOrder::Due => NewCardOrderSchema11::Due,
|
||||
NewCardInsertOrder::Random => NewCardOrderSchema11::Random,
|
||||
NewCardInsertOrder::Due => NewCardOrderSchema11::Due,
|
||||
},
|
||||
per_day: i.new_per_day,
|
||||
other: new_other,
|
||||
@ -365,6 +369,7 @@ impl From<DeckConfig> for DeckConfSchema11 {
|
||||
interday_learning_mix: i.interday_learning_mix,
|
||||
review_order: i.review_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",
|
||||
"reviewOrder",
|
||||
"newSortOrder",
|
||||
"newGatherPriority",
|
||||
] {
|
||||
top_other.remove(*key);
|
||||
}
|
||||
|
@ -151,7 +151,7 @@ impl Collection {
|
||||
let previous_config_id = DeckConfigId(normal.config_id);
|
||||
let previous_order = configs_before_update
|
||||
.get(&previous_config_id)
|
||||
.map(|c| c.inner.new_card_fetch_order())
|
||||
.map(|c| c.inner.new_card_insert_order())
|
||||
.unwrap_or_default();
|
||||
|
||||
// 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
|
||||
let current_order = configs_after_update
|
||||
.get(¤t_config_id)
|
||||
.map(|c| c.inner.new_card_fetch_order())
|
||||
.map(|c| c.inner.new_card_insert_order())
|
||||
.unwrap_or_default();
|
||||
if previous_order != current_order {
|
||||
self.sort_deck(deck_id, current_order, usn)?;
|
||||
@ -184,7 +184,7 @@ impl Collection {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::{collection::open_test_collection, deckconfig::NewCardFetchOrder};
|
||||
use crate::{collection::open_test_collection, deckconfig::NewCardInsertOrder};
|
||||
|
||||
#[test]
|
||||
fn updating() -> Result<()> {
|
||||
@ -258,7 +258,7 @@ mod test {
|
||||
assert_eq!(card1_pos(&mut col), 1);
|
||||
reset_card1_pos(&mut col);
|
||||
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!(
|
||||
col.update_deck_configs(input.clone())?.changes.changes.card,
|
||||
true
|
||||
|
@ -30,12 +30,14 @@ impl Collection {
|
||||
days_elapsed: u32,
|
||||
learn_cutoff: u32,
|
||||
limit_to: Option<&str>,
|
||||
v3: bool,
|
||||
) -> Result<HashMap<DeckId, DueCounts>> {
|
||||
self.storage.due_counts(
|
||||
self.scheduler_version(),
|
||||
days_elapsed,
|
||||
learn_cutoff,
|
||||
limit_to,
|
||||
v3,
|
||||
)
|
||||
}
|
||||
|
||||
|
59
rslib/src/decks/limits.rs
Normal file
59
rslib/src/decks/limits.rs
Normal 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()
|
||||
}
|
@ -5,6 +5,7 @@ mod addupdate;
|
||||
mod counts;
|
||||
mod current;
|
||||
mod filtered;
|
||||
pub(crate) mod limits;
|
||||
mod name;
|
||||
mod remove;
|
||||
mod reparent;
|
||||
|
@ -9,15 +9,18 @@ use std::{
|
||||
use serde_tuple::Serialize_tuple;
|
||||
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;
|
||||
use crate::{
|
||||
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 it = names.into_iter().peekable();
|
||||
let mut it = names.peekable();
|
||||
|
||||
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.
|
||||
/// Counts are (new, review).
|
||||
fn apply_limits(
|
||||
fn apply_limits_v1(
|
||||
node: &mut DeckTreeNode,
|
||||
today: u32,
|
||||
decks: &HashMap<DeckId, Deck>,
|
||||
dconf: &HashMap<DeckConfigId, DeckConfig>,
|
||||
parent_limits: (u32, u32),
|
||||
limits: &HashMap<DeckId, RemainingLimits>,
|
||||
parent_limits: RemainingLimits,
|
||||
) {
|
||||
let (mut remaining_new, mut remaining_rev) =
|
||||
remaining_counts_for_deck(DeckId(node.deck_id), today, decks, dconf);
|
||||
|
||||
// cap remaining to parent limits
|
||||
remaining_new = remaining_new.min(parent_limits.0);
|
||||
remaining_rev = remaining_rev.min(parent_limits.1);
|
||||
let mut remaining = limits
|
||||
.get(&DeckId(node.deck_id))
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
remaining.cap_to(parent_limits);
|
||||
|
||||
// apply our limit to children and tally their counts
|
||||
let mut child_new_total = 0;
|
||||
let mut child_rev_total = 0;
|
||||
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_rev_total += child.review_count;
|
||||
// no limit on learning cards
|
||||
@ -116,82 +115,87 @@ fn apply_limits(
|
||||
}
|
||||
|
||||
// add child counts to our count, capped to remaining limit
|
||||
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.new_count = (node.new_count + child_new_total).min(remaining.new);
|
||||
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
|
||||
/// 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.
|
||||
/// Counts are (new, review).
|
||||
fn apply_limits_v2_old(
|
||||
fn apply_limits_v2(
|
||||
node: &mut DeckTreeNode,
|
||||
today: u32,
|
||||
decks: &HashMap<DeckId, Deck>,
|
||||
dconf: &HashMap<DeckConfigId, DeckConfig>,
|
||||
parent_limits: (u32, u32),
|
||||
limits: &HashMap<DeckId, RemainingLimits>,
|
||||
parent_limits: RemainingLimits,
|
||||
) -> u32 {
|
||||
let original_rev_count = node.review_count;
|
||||
|
||||
let (mut remaining_new, remaining_rev) =
|
||||
remaining_counts_for_deck(DeckId(node.deck_id), today, decks, dconf);
|
||||
|
||||
// cap remaining to parent limits
|
||||
remaining_new = remaining_new.min(parent_limits.0);
|
||||
let mut remaining = limits
|
||||
.get(&DeckId(node.deck_id))
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
remaining.new = remaining.new.min(parent_limits.new);
|
||||
|
||||
// apply our limit to children and tally their counts
|
||||
let mut child_new_total = 0;
|
||||
let mut child_rev_total = 0;
|
||||
for child in &mut node.children {
|
||||
child_rev_total +=
|
||||
apply_limits_v2_old(child, today, decks, dconf, (remaining_new, remaining_rev));
|
||||
child_rev_total += apply_limits_v2(child, limits, remaining);
|
||||
child_new_total += child.new_count;
|
||||
// no limit on learning cards
|
||||
node.learn_count += child.learn_count;
|
||||
}
|
||||
|
||||
// add child counts to our count, capped to remaining limit
|
||||
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.new_count = (node.new_count + child_new_total).min(remaining.new);
|
||||
node.review_count = (node.review_count + child_rev_total).min(remaining.review);
|
||||
|
||||
original_rev_count + child_rev_total
|
||||
}
|
||||
|
||||
fn remaining_counts_for_deck(
|
||||
did: DeckId,
|
||||
today: u32,
|
||||
decks: &HashMap<DeckId, Deck>,
|
||||
dconf: &HashMap<DeckConfigId, DeckConfig>,
|
||||
/// Add child counts, then limit to remaining limit. The v3 scheduler does not
|
||||
/// propagate limits down the tree. Limits for a deck affect only the amount
|
||||
/// that deck itself will gather.
|
||||
/// The v3 scheduler also caps the new limit to the remaining review limit,
|
||||
/// 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) {
|
||||
if let Some(deck) = decks.get(&did) {
|
||||
match &deck.kind {
|
||||
DeckKind::Normal(norm) => {
|
||||
let (new_today, rev_today) = deck.new_rev_counts(today);
|
||||
if let Some(conf) = dconf
|
||||
.get(&DeckConfigId(norm.config_id))
|
||||
.or_else(|| dconf.get(&DeckConfigId(1)))
|
||||
{
|
||||
let new = (conf.inner.new_per_day as i32)
|
||||
.saturating_sub(new_today)
|
||||
.max(0);
|
||||
let rev = (conf.inner.reviews_per_day as i32)
|
||||
.saturating_sub(rev_today)
|
||||
.max(0);
|
||||
(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)
|
||||
let mut remaining = limits
|
||||
.get(&DeckId(node.deck_id))
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
|
||||
// recurse into children, tallying their counts
|
||||
let mut child_new_total = 0;
|
||||
let mut child_rev_total = 0;
|
||||
for child in &mut node.children {
|
||||
let child_counts = apply_limits_v3(child, limits);
|
||||
child_new_total += child_counts.0;
|
||||
child_rev_total += child_counts.1;
|
||||
// no limit on learning cards
|
||||
node.learn_count += child.learn_count;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@ -257,7 +261,7 @@ impl Collection {
|
||||
top_deck_id: Option<DeckId>,
|
||||
) -> Result<DeckTreeNode> {
|
||||
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()?;
|
||||
|
||||
@ -276,27 +280,20 @@ impl Collection {
|
||||
});
|
||||
let days_elapsed = self.timing_for_timestamp(now)?.days_elapsed;
|
||||
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()?;
|
||||
add_counts(&mut tree, &counts);
|
||||
if self.scheduler_version() == SchedulerVersion::V2
|
||||
&& !self.get_bool(BoolKey::Sched2021)
|
||||
{
|
||||
apply_limits_v2_old(
|
||||
&mut tree,
|
||||
days_elapsed,
|
||||
&decks_map,
|
||||
&dconf,
|
||||
(std::u32::MAX, std::u32::MAX),
|
||||
);
|
||||
let limits = remaining_limits_map(decks_map.values(), &dconf, days_elapsed);
|
||||
if sched_ver == SchedulerVersion::V2 {
|
||||
if v3 {
|
||||
apply_limits_v3(&mut tree, &limits);
|
||||
} else {
|
||||
apply_limits_v2(&mut tree, &limits, RemainingLimits::default());
|
||||
}
|
||||
} else {
|
||||
apply_limits(
|
||||
&mut tree,
|
||||
days_elapsed,
|
||||
&decks_map,
|
||||
&dconf,
|
||||
(std::u32::MAX, std::u32::MAX),
|
||||
);
|
||||
apply_limits_v1(&mut tree, &limits, RemainingLimits::default());
|
||||
}
|
||||
}
|
||||
|
||||
@ -328,7 +325,9 @@ impl Collection {
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Collection {
|
||||
pub(crate) fn legacy_deck_tree(&mut self) -> Result<LegacyDueCounts> {
|
||||
let tree = self.deck_tree(Some(TimestampSecs::now()), None)?;
|
||||
Ok(LegacyDueCounts::from(tree))
|
||||
|
@ -311,10 +311,10 @@ impl Collection {
|
||||
.get(&did)
|
||||
.unwrap()
|
||||
.inner
|
||||
.new_card_fetch_order()
|
||||
.new_card_insert_order()
|
||||
{
|
||||
crate::deckconfig::NewCardFetchOrder::Random => Ok(random_position(next_pos)),
|
||||
crate::deckconfig::NewCardFetchOrder::Due => Ok(next_pos),
|
||||
crate::deckconfig::NewCardInsertOrder::Random => Ok(random_position(next_pos)),
|
||||
crate::deckconfig::NewCardInsertOrder::Due => Ok(next_pos),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -261,7 +261,7 @@ impl Collection {
|
||||
if let Some(revlog_partial) = updater.apply_study_state(current_state, answer.new_state)? {
|
||||
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)?;
|
||||
let timing = updater.timing;
|
||||
let mut card = updater.into_card();
|
||||
@ -308,26 +308,22 @@ impl Collection {
|
||||
usn: Usn,
|
||||
answer: &CardAnswer,
|
||||
updater: &CardStateUpdater,
|
||||
from_queue: CardQueue,
|
||||
) -> 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(
|
||||
updater.timing.days_elapsed,
|
||||
usn,
|
||||
backend_proto::UpdateStatsIn {
|
||||
deck_id: updater.deck.id.0,
|
||||
new_delta: if matches!(answer.current_state, CardState::Normal(NormalState::New(_)))
|
||||
{
|
||||
1
|
||||
} else {
|
||||
0
|
||||
},
|
||||
review_delta: if matches!(
|
||||
answer.current_state,
|
||||
CardState::Normal(NormalState::Review(_))
|
||||
) {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
},
|
||||
new_delta,
|
||||
review_delta,
|
||||
millisecond_delta: answer.milliseconds_taken as i32,
|
||||
},
|
||||
)
|
||||
|
@ -7,7 +7,7 @@ use rand::seq::SliceRandom;
|
||||
|
||||
use crate::{
|
||||
card::{CardQueue, CardType},
|
||||
deckconfig::NewCardFetchOrder,
|
||||
deckconfig::NewCardInsertOrder,
|
||||
prelude::*,
|
||||
search::{SortMode, StateKind},
|
||||
};
|
||||
@ -171,9 +171,9 @@ impl Collection {
|
||||
col.sort_deck(
|
||||
deck,
|
||||
if random {
|
||||
NewCardFetchOrder::Random
|
||||
NewCardInsertOrder::Random
|
||||
} else {
|
||||
NewCardFetchOrder::Due
|
||||
NewCardInsertOrder::Due
|
||||
},
|
||||
col.usn()?,
|
||||
)
|
||||
@ -183,7 +183,7 @@ impl Collection {
|
||||
pub(crate) fn sort_deck(
|
||||
&mut self,
|
||||
deck: DeckId,
|
||||
order: NewCardFetchOrder,
|
||||
order: NewCardInsertOrder,
|
||||
usn: Usn,
|
||||
) -> Result<usize> {
|
||||
let cids = self.search_cards(match_all![deck, StateKind::New], SortMode::NoOrder)?;
|
||||
@ -241,11 +241,11 @@ mod test {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NewCardFetchOrder> for NewCardDueOrder {
|
||||
fn from(o: NewCardFetchOrder) -> Self {
|
||||
impl From<NewCardInsertOrder> for NewCardDueOrder {
|
||||
fn from(o: NewCardInsertOrder) -> Self {
|
||||
match o {
|
||||
NewCardFetchOrder::Due => NewCardDueOrder::NoteId,
|
||||
NewCardFetchOrder::Random => NewCardDueOrder::Random,
|
||||
NewCardInsertOrder::Due => NewCardDueOrder::NoteId,
|
||||
NewCardInsertOrder::Random => NewCardDueOrder::Random,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,62 +1,58 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// 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::*};
|
||||
|
||||
impl QueueBuilder {
|
||||
/// Assumes cards will arrive sorted in (queue, due) order, so learning
|
||||
/// cards come first, and reviews come before day-learning and preview cards.
|
||||
pub(in super::super) fn add_intraday_learning_card(
|
||||
&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(
|
||||
&mut self,
|
||||
limit: &mut RemainingLimits,
|
||||
queue: CardQueue,
|
||||
card: DueCard,
|
||||
bury_mode: BuryMode,
|
||||
) -> bool {
|
||||
let bury_reviews = self
|
||||
let bury_this_card = self
|
||||
.get_and_update_bury_mode_for_note(card.note_id, bury_mode)
|
||||
.map(|mode| mode.bury_reviews)
|
||||
.unwrap_or_default();
|
||||
|
||||
match queue {
|
||||
CardQueue::Learn | CardQueue::PreviewRepeat => self.learning.push(card),
|
||||
CardQueue::DayLearn => {
|
||||
self.day_learning.push(card);
|
||||
}
|
||||
CardQueue::Review => {
|
||||
if !bury_reviews {
|
||||
self.review.push(card);
|
||||
limit.review -= 1;
|
||||
if bury_this_card {
|
||||
false
|
||||
} else {
|
||||
match queue {
|
||||
CardQueue::DayLearn => {
|
||||
self.day_learning.push(card);
|
||||
}
|
||||
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(
|
||||
&mut self,
|
||||
limit: &mut RemainingLimits,
|
||||
card: NewCard,
|
||||
bury_mode: BuryMode,
|
||||
) -> bool {
|
||||
// True if limit should be decremented.
|
||||
pub(in super::super) fn add_new_card(&mut self, card: NewCard, bury_mode: BuryMode) -> bool {
|
||||
let previous_bury_mode = self
|
||||
.get_and_update_bury_mode_for_note(card.note_id, bury_mode)
|
||||
.map(|mode| mode.bury_new);
|
||||
// no previous siblings seen?
|
||||
if previous_bury_mode.is_none() {
|
||||
self.new.push(card);
|
||||
limit.new -= 1;
|
||||
return limit.new != 0;
|
||||
return true;
|
||||
}
|
||||
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
|
||||
// siblings sharing the same due number by default. In the
|
||||
@ -76,10 +72,12 @@ impl QueueBuilder {
|
||||
.unwrap_or(false);
|
||||
|
||||
if previous_card_was_sibling_with_higher_ordinal {
|
||||
if bury_new {
|
||||
if bury_this_card {
|
||||
// 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;
|
||||
|
||||
false
|
||||
} else {
|
||||
// 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).
|
||||
@ -100,17 +98,19 @@ impl QueueBuilder {
|
||||
.next()
|
||||
.unwrap_or(0);
|
||||
self.new.insert(target_idx, card);
|
||||
limit.new -= 1;
|
||||
|
||||
true
|
||||
}
|
||||
} else {
|
||||
// card has arrived in expected order - add if burying disabled
|
||||
if !bury_new {
|
||||
if bury_this_card {
|
||||
false
|
||||
} else {
|
||||
self.new.push(card);
|
||||
limit.new -= 1;
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
limit.new != 0
|
||||
}
|
||||
|
||||
/// If burying is enabled in `new_settings`, existing entry will be updated.
|
||||
@ -142,11 +142,6 @@ mod test {
|
||||
#[test]
|
||||
fn new_siblings() {
|
||||
let mut builder = QueueBuilder::default();
|
||||
let mut limits = RemainingLimits {
|
||||
review: 0,
|
||||
new: 100,
|
||||
};
|
||||
|
||||
let cards = vec![
|
||||
NewCard {
|
||||
id: CardId(1),
|
||||
@ -177,7 +172,6 @@ mod test {
|
||||
|
||||
for card in &cards {
|
||||
builder.add_new_card(
|
||||
&mut limits,
|
||||
card.clone(),
|
||||
BuryMode {
|
||||
bury_new: true,
|
||||
@ -194,7 +188,7 @@ mod test {
|
||||
let mut builder = QueueBuilder::default();
|
||||
|
||||
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));
|
||||
|
@ -11,12 +11,10 @@ use std::collections::{HashMap, VecDeque};
|
||||
use intersperser::Intersperser;
|
||||
use sized_chain::SizedChain;
|
||||
|
||||
use super::{
|
||||
limits::{remaining_limits_capped_to_parents, RemainingLimits},
|
||||
CardQueues, Counts, LearningQueueEntry, MainQueueEntry, MainQueueEntryKind,
|
||||
};
|
||||
use super::{CardQueues, Counts, LearningQueueEntry, MainQueueEntry, MainQueueEntryKind};
|
||||
use crate::{
|
||||
deckconfig::{NewCardSortOrder, ReviewCardOrder, ReviewMix},
|
||||
deckconfig::{NewCardGatherPriority, NewCardSortOrder, ReviewCardOrder, ReviewMix},
|
||||
decks::limits::{remaining_limits_map, RemainingLimits},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
@ -28,8 +26,9 @@ pub(crate) struct DueCard {
|
||||
pub mtime: TimestampSecs,
|
||||
pub due: i32,
|
||||
pub interval: u32,
|
||||
pub original_deck_id: DeckId,
|
||||
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.
|
||||
@ -85,6 +84,7 @@ pub(super) struct BuryMode {
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub(super) struct QueueSortOptions {
|
||||
pub(super) new_order: NewCardSortOrder,
|
||||
pub(super) new_gather_priority: NewCardGatherPriority,
|
||||
pub(super) review_order: ReviewCardOrder,
|
||||
pub(super) day_learn_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> {
|
||||
let now = TimestampSecs::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 config = self.storage.get_deck_config_map()?;
|
||||
let sort_options = decks
|
||||
.get(parent_count)
|
||||
.unwrap()
|
||||
let sort_options = decks[0]
|
||||
.config_id()
|
||||
.and_then(|config_id| config.get(&config_id))
|
||||
.map(|config| QueueSortOptions {
|
||||
new_order: config.inner.new_card_sort_order(),
|
||||
new_gather_priority: config.inner.new_card_gather_priority(),
|
||||
review_order: config.inner.review_order(),
|
||||
day_learn_mix: config.inner.interday_learning_mix(),
|
||||
new_review_mix: config.inner.new_mix(),
|
||||
@ -213,9 +214,18 @@ impl Collection {
|
||||
..Default::default()
|
||||
}
|
||||
});
|
||||
let limits = remaining_limits_capped_to_parents(&decks, &config, timing.days_elapsed);
|
||||
let selected_deck_limits = limits[parent_count];
|
||||
let mut queues = QueueBuilder::new(sort_options);
|
||||
|
||||
// fetch remaining limits, and cap to selected deck limits so that we don't
|
||||
// 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| {
|
||||
deck_map
|
||||
@ -229,28 +239,70 @@ impl Collection {
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
for (deck, mut limit) in decks.iter().zip(limits).skip(parent_count) {
|
||||
if limit.review > 0 {
|
||||
self.storage.for_each_due_card_in_deck(
|
||||
timing.days_elapsed,
|
||||
timing.next_day_at,
|
||||
deck.id,
|
||||
|queue, card| {
|
||||
let bury = get_bury_mode(card.original_deck_id.or(deck.id));
|
||||
queues.add_due_card(&mut limit, queue, card, bury)
|
||||
},
|
||||
)?;
|
||||
// intraday cards first, noting down any notes that will need burying
|
||||
self.storage
|
||||
.for_each_intraday_card_in_active_decks(timing.next_day_at, |card| {
|
||||
let bury = get_bury_mode(card.current_deck_id);
|
||||
queues.add_intraday_learning_card(card, bury)
|
||||
})?;
|
||||
|
||||
// reviews and interday learning next
|
||||
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 {
|
||||
self.storage.for_each_new_card_in_deck(deck.id, |card| {
|
||||
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(
|
||||
selected_deck_limits,
|
||||
final_limits,
|
||||
self.learn_ahead_secs() as i64,
|
||||
deck_id,
|
||||
timing.days_elapsed,
|
||||
|
@ -31,23 +31,10 @@ impl QueueBuilder {
|
||||
.for_each(DueCard::hash_id_and_mtime);
|
||||
self.day_learning.sort_unstable_by(day_then_hash);
|
||||
|
||||
match self.sort_options.review_order {
|
||||
ReviewCardOrder::DayThenRandom => {
|
||||
self.review.iter_mut().for_each(DueCard::hash_id_and_mtime);
|
||||
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)
|
||||
// }
|
||||
// other sorting is done in SQL
|
||||
if self.sort_options.review_order == ReviewCardOrder::DayThenRandom {
|
||||
self.review.iter_mut().for_each(DueCard::hash_id_and_mtime);
|
||||
self.review.sort_unstable_by(day_then_hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -72,15 +59,6 @@ fn day_then_hash(a: &DueCard, b: &DueCard) -> Ordering {
|
||||
(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
|
||||
// cards come back in the same approximate order (mixing + due learning cards
|
||||
// may still result in a different card)
|
||||
|
@ -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
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
@ -4,11 +4,10 @@
|
||||
mod builder;
|
||||
mod entry;
|
||||
mod learning;
|
||||
mod limits;
|
||||
mod main;
|
||||
pub(crate) mod undo;
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::{collections::VecDeque, time::Instant};
|
||||
|
||||
pub(crate) use builder::{DueCard, NewCard};
|
||||
pub(crate) use entry::{QueueEntry, QueueEntryKind};
|
||||
@ -171,7 +170,9 @@ impl Collection {
|
||||
self.discard_undo_and_study_queues();
|
||||
}
|
||||
if self.state.card_queues.is_none() {
|
||||
let now = Instant::now();
|
||||
self.state.card_queues = Some(self.build_queues(deck)?);
|
||||
println!("queue build in {:?}", now.elapsed());
|
||||
}
|
||||
|
||||
Ok(self.state.card_queues.as_mut().unwrap())
|
||||
|
@ -4,16 +4,14 @@ SELECT queue,
|
||||
due,
|
||||
cast(ivl AS integer),
|
||||
cast(mod AS integer),
|
||||
did,
|
||||
odid
|
||||
FROM cards
|
||||
WHERE did = ?1
|
||||
WHERE did IN (
|
||||
SELECT id
|
||||
FROM active_decks
|
||||
)
|
||||
AND (
|
||||
(
|
||||
queue IN (2, 3)
|
||||
AND due <= ?2
|
||||
)
|
||||
OR (
|
||||
queue IN (1, 4)
|
||||
AND due <= ?3
|
||||
)
|
||||
queue IN (2, 3)
|
||||
AND due <= ?
|
||||
)
|
15
rslib/src/storage/card/intraday_due.sql
Normal file
15
rslib/src/storage/card/intraday_due.sql
Normal 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 <= ?
|
||||
)
|
@ -14,7 +14,7 @@ use rusqlite::{
|
||||
use super::ids_to_string;
|
||||
use crate::{
|
||||
card::{Card, CardId, CardQueue, CardType},
|
||||
deckconfig::DeckConfigId,
|
||||
deckconfig::{DeckConfigId, ReviewCardOrder},
|
||||
decks::{Deck, DeckId, DeckKind},
|
||||
error::Result,
|
||||
notes::NoteId,
|
||||
@ -164,25 +164,52 @@ impl super::SqliteStorage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Call func() for each due card, stopping when it returns false
|
||||
/// or no more cards found.
|
||||
pub(crate) fn for_each_due_card_in_deck<F>(
|
||||
pub(crate) fn for_each_intraday_card_in_active_decks<F>(
|
||||
&self,
|
||||
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,
|
||||
day_cutoff: u32,
|
||||
learn_cutoff: TimestampSecs,
|
||||
deck: DeckId,
|
||||
order: ReviewCardOrder,
|
||||
mut func: F,
|
||||
) -> Result<()>
|
||||
where
|
||||
F: FnMut(CardQueue, DueCard) -> bool,
|
||||
{
|
||||
let mut stmt = self.db.prepare_cached(include_str!("due_cards.sql"))?;
|
||||
let mut rows = stmt.query(params![
|
||||
// with many subdecks, avoiding named params shaves off a few milliseconds
|
||||
deck,
|
||||
day_cutoff,
|
||||
learn_cutoff
|
||||
])?;
|
||||
let order_clause = match order {
|
||||
ReviewCardOrder::DayThenRandom => "order by due",
|
||||
ReviewCardOrder::IntervalsAscending => "order by ivl asc",
|
||||
ReviewCardOrder::IntervalsDescending => "order by ivl desc",
|
||||
};
|
||||
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()? {
|
||||
let queue: CardQueue = row.get(0)?;
|
||||
if !func(
|
||||
@ -193,8 +220,9 @@ impl super::SqliteStorage {
|
||||
due: row.get(3).ok().unwrap_or_default(),
|
||||
interval: row.get(4)?,
|
||||
mtime: row.get(5)?,
|
||||
current_deck_id: row.get(6)?,
|
||||
original_deck_id: row.get(7)?,
|
||||
hash: 0,
|
||||
original_deck_id: row.get(6)?,
|
||||
},
|
||||
) {
|
||||
break;
|
||||
@ -408,6 +436,8 @@ impl super::SqliteStorage {
|
||||
}
|
||||
|
||||
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.db
|
||||
.prepare(include_str!("congrats.sql"))?
|
||||
|
@ -4,7 +4,12 @@ SELECT did,
|
||||
queue = :review_queue
|
||||
AND due <= :day_cutoff
|
||||
),
|
||||
-- learning
|
||||
-- interday learning
|
||||
sum(
|
||||
queue = :daylearn_queue
|
||||
AND due <= :day_cutoff
|
||||
),
|
||||
-- intraday learning
|
||||
sum(
|
||||
(
|
||||
CASE
|
||||
@ -15,10 +20,6 @@ SELECT did,
|
||||
queue = :learn_queue
|
||||
AND due < :learn_cutoff
|
||||
)
|
||||
OR (
|
||||
queue = :daylearn_queue
|
||||
AND due <= :day_cutoff
|
||||
)
|
||||
OR (
|
||||
queue = :preview_queue
|
||||
AND due <= :learn_cutoff
|
||||
@ -29,8 +30,6 @@ SELECT did,
|
||||
CASE
|
||||
WHEN queue = :learn_queue
|
||||
AND due < :learn_cutoff THEN left / 1000
|
||||
WHEN queue = :daylearn_queue
|
||||
AND due <= :day_cutoff THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
)
|
||||
|
@ -1,7 +1,10 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// 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 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((
|
||||
row.get(0)?,
|
||||
deck_id,
|
||||
DueCounts {
|
||||
new: row.get(1)?,
|
||||
review: row.get(2)?,
|
||||
learning: row.get(3)?,
|
||||
new,
|
||||
review,
|
||||
learning,
|
||||
},
|
||||
))
|
||||
}
|
||||
@ -193,24 +207,11 @@ impl SqliteStorage {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Return the provided deck with its parents and children in an ordered list, and
|
||||
/// 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)> {
|
||||
pub(crate) fn deck_with_children(&self, deck_id: DeckId) -> Result<Vec<Deck>> {
|
||||
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_end = format!("{}\x20", deck.name);
|
||||
parents.push(deck);
|
||||
|
||||
let decks = parents
|
||||
.into_iter()
|
||||
.map(Result::Ok)
|
||||
iter::once(Ok(deck))
|
||||
.chain(
|
||||
self.db
|
||||
.prepare_cached(concat!(
|
||||
@ -219,9 +220,7 @@ impl SqliteStorage {
|
||||
))?
|
||||
.query_and_then(&[prefix_start, prefix_end], row_to_deck)?,
|
||||
)
|
||||
.collect::<Result<_>>()?;
|
||||
|
||||
Ok((decks, parent_count))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Return the parents of `child`, with the most immediate parent coming first.
|
||||
@ -252,6 +251,7 @@ impl SqliteStorage {
|
||||
day_cutoff: u32,
|
||||
learn_cutoff: u32,
|
||||
top_deck: Option<&str>,
|
||||
v3: bool,
|
||||
) -> Result<HashMap<DeckId, DueCounts>> {
|
||||
let sched_ver = sched as u8;
|
||||
let mut params = named_params! {
|
||||
@ -292,7 +292,7 @@ impl SqliteStorage {
|
||||
|
||||
self.db
|
||||
.prepare_cached(sql)?
|
||||
.query_and_then_named(¶ms, row_to_due_counts)?
|
||||
.query_and_then_named(¶ms, |row| row_to_due_counts(row, v3))?
|
||||
.collect()
|
||||
}
|
||||
|
||||
@ -323,7 +323,7 @@ impl SqliteStorage {
|
||||
/// Write active decks into temporary active_decks table.
|
||||
pub(crate) fn update_active_decks(&self, current: &Deck) -> Result<()> {
|
||||
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);"
|
||||
))?;
|
||||
|
||||
|
@ -67,10 +67,3 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
defaultValue={defaults.lapseMultiplier}
|
||||
value={$config.lapseMultiplier}
|
||||
on:changed={(evt) => ($config.lapseMultiplier = evt.detail.value)} />
|
||||
|
||||
<SpinBox
|
||||
label={tr.schedulingMinimumInterval()}
|
||||
tooltip={tr.deckConfigMinimumIntervalTooltip()}
|
||||
min={1}
|
||||
defaultValue={defaults.minimumLapseInterval}
|
||||
bind:value={$config.minimumLapseInterval} />
|
||||
|
@ -8,6 +8,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import NewOptions from "./NewOptions.svelte";
|
||||
import AdvancedOptions from "./AdvancedOptions.svelte";
|
||||
import ReviewOptions from "./ReviewOptions.svelte";
|
||||
import LapseOptions from "./LapseOptions.svelte";
|
||||
import GeneralOptions from "./GeneralOptions.svelte";
|
||||
import Addons from "./Addons.svelte";
|
||||
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} />
|
||||
<NewOptions {state} />
|
||||
<ReviewOptions {state} />
|
||||
<LapseOptions {state} />
|
||||
<GeneralOptions {state} />
|
||||
<Addons {state} />
|
||||
<AdvancedOptions {state} />
|
||||
|
@ -13,7 +13,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
let parentLimits = state.parentLimits;
|
||||
|
||||
$: newCardsGreaterThanParent =
|
||||
$config.newPerDay > $parentLimits.newCards
|
||||
!state.v3Scheduler && $config.newPerDay > $parentLimits.newCards
|
||||
? tr.deckConfigDailyLimitWillBeCapped({ cards: $parentLimits.newCards })
|
||||
: "";
|
||||
|
||||
|
61
ts/deckoptions/LapseOptions.svelte
Normal file
61
ts/deckoptions/LapseOptions.svelte
Normal 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>
|
@ -14,7 +14,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
let defaults = state.defaults;
|
||||
</script>
|
||||
|
||||
<h2>{tr.deckConfigLearningRelearningTitle()}</h2>
|
||||
<h2>{tr.deckConfigLearningTitle()}</h2>
|
||||
|
||||
<StepsInput
|
||||
label={tr.deckConfigLearningSteps()}
|
||||
@ -23,13 +23,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
value={$config.learnSteps}
|
||||
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}
|
||||
<EnumSelector
|
||||
label={tr.deckConfigInterdayStepPriority()}
|
||||
|
@ -14,10 +14,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
let config = state.currentConfig;
|
||||
let defaults = state.defaults;
|
||||
|
||||
const newFetchOrderChoices = [
|
||||
const newInsertOrderChoices = [
|
||||
tr.deckConfigNewInsertionOrderSequential(),
|
||||
tr.deckConfigNewInsertionOrderRandom(),
|
||||
];
|
||||
const newGatherPriorityChoices = [
|
||||
tr.deckConfigNewGatherPriorityDeck(),
|
||||
tr.deckConfigNewGatherPriorityPosition(),
|
||||
];
|
||||
const newSortOrderChoices = [
|
||||
tr.deckConfigSortOrderCardTemplateThenPosition(),
|
||||
tr.deckConfigSortOrderCardTemplateThenRandom(),
|
||||
@ -62,9 +66,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
<EnumSelector
|
||||
label={tr.deckConfigNewInsertionOrder()}
|
||||
tooltip={tr.deckConfigNewInsertionOrderTooltip()}
|
||||
choices={newFetchOrderChoices}
|
||||
defaultValue={defaults.newCardFetchOrder}
|
||||
bind:value={$config.newCardFetchOrder} />
|
||||
choices={newInsertOrderChoices}
|
||||
defaultValue={defaults.newCardInsertOrder}
|
||||
bind:value={$config.newCardInsertOrder} />
|
||||
|
||||
<EnumSelector
|
||||
label={tr.deckConfigNewGatherPriority()}
|
||||
tooltip={tr.deckConfigNewGatherPriorityTooltip()}
|
||||
choices={newGatherPriorityChoices}
|
||||
defaultValue={defaults.newCardGatherPriority}
|
||||
bind:value={$config.newCardGatherPriority} />
|
||||
|
||||
<EnumSelector
|
||||
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} />
|
||||
|
||||
<EnumSelector
|
||||
label={tr.deckConfigNewPriority()}
|
||||
tooltip={tr.deckConfigNewPriorityTooltip()}
|
||||
label={tr.deckConfigReviewPriority()}
|
||||
tooltip={tr.deckConfigReviewPriorityTooltip()}
|
||||
choices={reviewMixChoices()}
|
||||
defaultValue={defaults.newMix}
|
||||
bind:value={$config.newMix} />
|
||||
|
@ -4,7 +4,6 @@ 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 CheckBox from "./CheckBox.svelte";
|
||||
import EnumSelector from "./EnumSelector.svelte";
|
||||
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.deckConfigSortOrderDescendingIntervals(),
|
||||
];
|
||||
|
||||
const leechChoices = [tr.actionsSuspendCard(), tr.schedulingTagOnly()];
|
||||
</script>
|
||||
|
||||
<div>
|
||||
@ -32,20 +29,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
defaultValue={defaults.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
|
||||
label={tr.deckConfigBuryReviewSiblings()}
|
||||
tooltip={tr.deckConfigBuryTooltip()}
|
||||
|
Loading…
Reference in New Issue
Block a user