Allow applying limits of inactive parents (#2824)

* Allow applying limits of inactive parents

* Tweak label/help text (dae)
This commit is contained in:
RumovZ 2023-11-13 05:30:19 +01:00 committed by GitHub
parent 2c83ec9e14
commit 39a60bc3a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 83 additions and 14 deletions

View file

@ -44,6 +44,11 @@ deck-config-new-cards-ignore-review-limit-tooltip =
By default, the review limit also applies to new cards, and no new cards will be
shown when the review limit has been reached. If this option is enabled, new cards
will be shown regardless of the review limit.
deck-config-apply-all-parent-limits = Limits start from top
deck-config-apply-all-parent-limits-tooltip =
By default, limits start from the deck you select. If this option is enabled, the limits will
start from the top-level deck instead, which can be useful if you wish to study individual
sub-decks, while enforcing a total limit on cards/day.
deck-config-affects-entire-collection = Affects the entire collection.
## Daily limit tabs: please try to keep these as short as the English version,

View file

@ -196,6 +196,7 @@ message DeckConfigsForUpdate {
// only applies to v3 scheduler
bool new_cards_ignore_review_limit = 7;
bool fsrs = 8;
bool apply_all_parent_limits = 9;
}
message UpdateDeckConfigsRequest {
@ -209,4 +210,5 @@ message UpdateDeckConfigsRequest {
DeckConfigsForUpdate.CurrentDeck.Limits limits = 6;
bool new_cards_ignore_review_limit = 7;
bool fsrs = 8;
bool apply_all_parent_limits = 9;
}

View file

@ -10,6 +10,7 @@ use crate::prelude::*;
#[derive(Debug, Clone, Copy, IntoStaticStr)]
#[strum(serialize_all = "camelCase")]
pub enum BoolKey {
ApplyAllParentLimits,
BrowserTableShowNotesMode,
CardCountsSeparateInactive,
CollapseCardState,

View file

@ -102,6 +102,7 @@ impl From<anki_proto::deck_config::UpdateDeckConfigsRequest> for UpdateDeckConfi
card_state_customizer: c.card_state_customizer,
limits: c.limits.unwrap_or_default(),
new_cards_ignore_review_limit: c.new_cards_ignore_review_limit,
apply_all_parent_limits: c.apply_all_parent_limits,
fsrs: c.fsrs,
}
}

View file

@ -31,6 +31,7 @@ pub struct UpdateDeckConfigsRequest {
pub card_state_customizer: String,
pub limits: Limits,
pub new_cards_ignore_review_limit: bool,
pub apply_all_parent_limits: bool,
pub fsrs: bool,
}
@ -52,6 +53,7 @@ impl Collection {
.schema_changed_since_sync(),
card_state_customizer: self.get_config_string(StringKey::CardStateCustomizer),
new_cards_ignore_review_limit: self.get_config_bool(BoolKey::NewCardsIgnoreReviewLimit),
apply_all_parent_limits: self.get_config_bool(BoolKey::ApplyAllParentLimits),
fsrs: self.get_config_bool(BoolKey::Fsrs),
})
}
@ -255,6 +257,7 @@ impl Collection {
BoolKey::NewCardsIgnoreReviewLimit,
req.new_cards_ignore_review_limit,
)?;
self.set_config_bool_inner(BoolKey::ApplyAllParentLimits, req.apply_all_parent_limits)?;
Ok(())
}
@ -350,6 +353,7 @@ mod test {
// add the keys so it doesn't trigger a change below
col.set_config_string_inner(StringKey::CardStateCustomizer, "")?;
col.set_config_bool_inner(BoolKey::NewCardsIgnoreReviewLimit, false)?;
col.set_config_bool_inner(BoolKey::ApplyAllParentLimits, false)?;
// pretend we're in sync
let stamps = col.storage.get_collection_timestamps()?;
@ -383,6 +387,7 @@ mod test {
card_state_customizer: "".to_string(),
limits: Limits::default(),
new_cards_ignore_review_limit: false,
apply_all_parent_limits: false,
fsrs: false,
};
assert!(!col.update_deck_configs(input.clone())?.changes.had_change());

View file

@ -229,25 +229,24 @@ pub(crate) struct LimitTreeMap {
}
impl LimitTreeMap {
/// Child [Deck]s must be sorted by name.
/// [Deck]s must be sorted by name.
pub(crate) fn build(
root_deck: &Deck,
child_decks: Vec<Deck>,
decks: &[Deck],
config: &HashMap<DeckConfigId, DeckConfig>,
today: u32,
new_cards_ignore_review_limit: bool,
) -> Self {
let root_limits = NodeLimits::new(root_deck, config, today, new_cards_ignore_review_limit);
let root_limits = NodeLimits::new(&decks[0], config, today, new_cards_ignore_review_limit);
let mut tree = Tree::new();
let root_id = tree
.insert(Node::new(root_limits), InsertBehavior::AsRoot)
.unwrap();
let mut map = HashMap::new();
map.insert(root_deck.id, root_id.clone());
map.insert(decks[0].id, root_id.clone());
let mut limits = Self { tree, map };
let mut remaining_decks = child_decks.into_iter().peekable();
let mut remaining_decks = decks[1..].iter().peekable();
limits.add_child_nodes(
root_id,
&mut remaining_decks,
@ -264,10 +263,10 @@ impl LimitTreeMap {
/// Given [Deck]s are assumed to arrive in depth-first order.
/// The tree-from-deck-list logic is taken from
/// [crate::decks::tree::add_child_nodes].
fn add_child_nodes(
fn add_child_nodes<'d>(
&mut self,
parent_node_id: NodeId,
remaining_decks: &mut Peekable<impl Iterator<Item = Deck>>,
remaining_decks: &mut Peekable<impl Iterator<Item = &'d Deck>>,
config: &HashMap<DeckConfigId, DeckConfig>,
today: u32,
new_cards_ignore_review_limit: bool,

View file

@ -142,11 +142,16 @@ impl AddAssign for NodeCountsV3 {
fn sum_counts_and_apply_limits_v3(
node: &mut DeckTreeNode,
limits: &HashMap<DeckId, RemainingLimits>,
mut parent_limits: Option<RemainingLimits>,
) -> NodeCountsV3 {
let remaining = limits
let mut remaining = limits
.get(&DeckId(node.deck_id))
.copied()
.unwrap_or_default();
if let Some(parent_remaining) = parent_limits {
remaining.cap_to(parent_remaining);
parent_limits.replace(remaining);
}
// initialize with this node's values
let mut this_node_uncapped = NodeCountsV3 {
@ -160,7 +165,7 @@ fn sum_counts_and_apply_limits_v3(
// add capped child counts / uncapped total
for child in &mut node.children {
this_node_uncapped += sum_counts_and_apply_limits_v3(child, limits);
this_node_uncapped += sum_counts_and_apply_limits_v3(child, limits, parent_limits);
total_including_children += child.total_including_children;
}
@ -266,6 +271,9 @@ impl Collection {
let learn_cutoff = (timestamp.0 as u32) + self.learn_ahead_secs();
let new_cards_ignore_review_limit =
self.get_config_bool(BoolKey::NewCardsIgnoreReviewLimit);
let parent_limits = self
.get_config_bool(BoolKey::ApplyAllParentLimits)
.then(Default::default);
let counts = self.due_counts(days_elapsed, learn_cutoff)?;
let dconf = self.storage.get_deck_config_map()?;
add_counts(&mut tree, &counts);
@ -275,7 +283,7 @@ impl Collection {
days_elapsed,
new_cards_ignore_review_limit,
);
sum_counts_and_apply_limits_v3(&mut tree, &limits);
sum_counts_and_apply_limits_v3(&mut tree, &limits, parent_limits);
}
Ok(tree)

View file

@ -125,12 +125,18 @@ impl QueueBuilder {
pub(super) fn new(col: &mut Collection, deck_id: DeckId) -> Result<Self> {
let timing = col.timing_for_timestamp(TimestampSecs::now())?;
let new_cards_ignore_review_limit = col.get_config_bool(BoolKey::NewCardsIgnoreReviewLimit);
let apply_all_parent_limits = col.get_config_bool(BoolKey::ApplyAllParentLimits);
let config_map = col.storage.get_deck_config_map()?;
let root_deck = col.storage.get_deck(deck_id)?.or_not_found(deck_id)?;
let child_decks = col.storage.child_decks(&root_deck)?;
let mut decks = col.storage.child_decks(&root_deck)?;
decks.insert(0, root_deck.clone());
if apply_all_parent_limits {
for parent in col.storage.parent_decks(&root_deck)? {
decks.insert(0, parent);
}
}
let limits = LimitTreeMap::build(
&root_deck,
child_decks,
&decks,
&config_map,
timing.days_elapsed,
new_cards_ignore_review_limit,
@ -502,4 +508,20 @@ mod test {
CardAdder::new().siblings(2).due_dates(["0"]).add(&mut col);
assert_eq!(col.card_queue_len(), 2);
}
#[test]
fn may_apply_parent_limits() {
let mut col = Collection::new_v3();
col.set_config_bool(BoolKey::ApplyAllParentLimits, true, false)
.unwrap();
col.update_default_deck_config(|config| {
config.new_per_day = 0;
});
let child = DeckAdder::new("Default::child")
.with_config(|_| ())
.add(&mut col);
CardAdder::new().deck(child.id).add(&mut col);
col.set_current_deck(child.id).unwrap();
assert_eq!(col.card_queue_len(), 0);
}
}

View file

@ -45,6 +45,7 @@
const limits = state.deckLimits;
const defaults = state.defaults;
const newCardsIgnoreReviewLimit = state.newCardsIgnoreReviewLimit;
const applyAllParentLimits = state.applyAllParentLimits;
const v3Extra =
"\n\n" + tr.deckConfigLimitDeckV3() + "\n\n" + tr.deckConfigTabDescription();
@ -53,6 +54,10 @@
tr.deckConfigAffectsEntireCollection() +
"\n\n" +
tr.deckConfigNewCardsIgnoreReviewLimitTooltip();
const applyAllParentLimitsHelp =
tr.deckConfigAffectsEntireCollection() +
"\n\n" +
tr.deckConfigApplyAllParentLimitsTooltip();
$: reviewsTooLow =
Math.min(9999, newValue * 10) > reviewsValue
@ -129,6 +134,11 @@
help: newCardsIgnoreReviewLimitHelp,
url: HelpPage.DeckOptions.newCardsday,
},
applyAllParentLimits: {
title: tr.deckConfigApplyAllParentLimits(),
help: applyAllParentLimitsHelp,
url: HelpPage.DeckOptions.newCardsday,
},
};
const helpSections = Object.values(settings) as HelpItem[];
@ -193,5 +203,18 @@
</SettingTitle>
</SwitchRow>
</Item>
<Item>
<SwitchRow bind:value={$applyAllParentLimits} defaultValue={false}>
<SettingTitle
on:click={() =>
openHelpModal(
Object.keys(settings).indexOf("applyAllParentLimits"),
)}
>
{settings.applyAllParentLimits.title}
</SettingTitle>
</SwitchRow>
</Item>
</DynamicallySlottable>
</TitledContainer>

View file

@ -41,6 +41,7 @@ export class DeckOptionsState {
readonly defaults: DeckConfig_Config;
readonly addonComponents: Writable<DynamicSvelteComponent[]>;
readonly newCardsIgnoreReviewLimit: Writable<boolean>;
readonly applyAllParentLimits: Writable<boolean>;
readonly fsrs: Writable<boolean>;
readonly currentPresetName: Writable<string>;
@ -73,6 +74,7 @@ export class DeckOptionsState {
this.cardStateCustomizer = writable(data.cardStateCustomizer);
this.deckLimits = writable(data.currentDeck?.limits ?? createLimits());
this.newCardsIgnoreReviewLimit = writable(data.newCardsIgnoreReviewLimit);
this.applyAllParentLimits = writable(data.applyAllParentLimits);
this.fsrs = writable(data.fsrs);
// decrement the use count of the starting item, as we'll apply +1 to currently
@ -199,6 +201,7 @@ export class DeckOptionsState {
cardStateCustomizer: get(this.cardStateCustomizer),
limits: get(this.deckLimits),
newCardsIgnoreReviewLimit: get(this.newCardsIgnoreReviewLimit),
applyAllParentLimits: get(this.applyAllParentLimits),
fsrs: get(this.fsrs),
};
}