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,
|
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.
|
||||||
|
@ -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()
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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(¤t_config_id)
|
.get(¤t_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
|
||||||
|
@ -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
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 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;
|
||||||
|
@ -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))
|
||||||
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -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 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())
|
||||||
|
@ -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
|
|
||||||
)
|
|
||||||
)
|
)
|
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 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"))?
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -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(¶ms, row_to_due_counts)?
|
.query_and_then_named(¶ms, |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);"
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
|
@ -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} />
|
|
||||||
|
@ -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} />
|
||||||
|
@ -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 })
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
|
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;
|
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()}
|
||||||
|
@ -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} />
|
||||||
|
@ -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()}
|
||||||
|
Loading…
Reference in New Issue
Block a user