From 39a60bc3a4484730a23ddac631039fc7e919912c Mon Sep 17 00:00:00 2001 From: RumovZ Date: Mon, 13 Nov 2023 05:30:19 +0100 Subject: [PATCH] Allow applying limits of inactive parents (#2824) * Allow applying limits of inactive parents * Tweak label/help text (dae) --- ftl/core/deck-config.ftl | 5 +++++ proto/anki/deck_config.proto | 2 ++ rslib/src/config/bool.rs | 1 + rslib/src/deckconfig/service.rs | 1 + rslib/src/deckconfig/update.rs | 5 +++++ rslib/src/decks/limits.rs | 15 ++++++------- rslib/src/decks/tree.rs | 14 +++++++++--- rslib/src/scheduler/queue/builder/mod.rs | 28 +++++++++++++++++++++--- ts/deck-options/DailyLimits.svelte | 23 +++++++++++++++++++ ts/deck-options/lib.ts | 3 +++ 10 files changed, 83 insertions(+), 14 deletions(-) diff --git a/ftl/core/deck-config.ftl b/ftl/core/deck-config.ftl index cebd2ab38..83bbfb70f 100644 --- a/ftl/core/deck-config.ftl +++ b/ftl/core/deck-config.ftl @@ -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, diff --git a/proto/anki/deck_config.proto b/proto/anki/deck_config.proto index ca45cff33..26576ac0c 100644 --- a/proto/anki/deck_config.proto +++ b/proto/anki/deck_config.proto @@ -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; } diff --git a/rslib/src/config/bool.rs b/rslib/src/config/bool.rs index 6bdf3143c..1177baa71 100644 --- a/rslib/src/config/bool.rs +++ b/rslib/src/config/bool.rs @@ -10,6 +10,7 @@ use crate::prelude::*; #[derive(Debug, Clone, Copy, IntoStaticStr)] #[strum(serialize_all = "camelCase")] pub enum BoolKey { + ApplyAllParentLimits, BrowserTableShowNotesMode, CardCountsSeparateInactive, CollapseCardState, diff --git a/rslib/src/deckconfig/service.rs b/rslib/src/deckconfig/service.rs index 5ebbf4816..a920ed04b 100644 --- a/rslib/src/deckconfig/service.rs +++ b/rslib/src/deckconfig/service.rs @@ -102,6 +102,7 @@ impl From 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, } } diff --git a/rslib/src/deckconfig/update.rs b/rslib/src/deckconfig/update.rs index 45480fd3f..ac2d82c4e 100644 --- a/rslib/src/deckconfig/update.rs +++ b/rslib/src/deckconfig/update.rs @@ -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()); diff --git a/rslib/src/decks/limits.rs b/rslib/src/decks/limits.rs index d8e020451..77a59354a 100644 --- a/rslib/src/decks/limits.rs +++ b/rslib/src/decks/limits.rs @@ -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, + decks: &[Deck], config: &HashMap, 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>, + remaining_decks: &mut Peekable>, config: &HashMap, today: u32, new_cards_ignore_review_limit: bool, diff --git a/rslib/src/decks/tree.rs b/rslib/src/decks/tree.rs index e2f7cca71..28dc5477a 100644 --- a/rslib/src/decks/tree.rs +++ b/rslib/src/decks/tree.rs @@ -142,11 +142,16 @@ impl AddAssign for NodeCountsV3 { fn sum_counts_and_apply_limits_v3( node: &mut DeckTreeNode, limits: &HashMap, + mut parent_limits: Option, ) -> 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) diff --git a/rslib/src/scheduler/queue/builder/mod.rs b/rslib/src/scheduler/queue/builder/mod.rs index cdba84d9e..724178c07 100644 --- a/rslib/src/scheduler/queue/builder/mod.rs +++ b/rslib/src/scheduler/queue/builder/mod.rs @@ -125,12 +125,18 @@ impl QueueBuilder { pub(super) fn new(col: &mut Collection, deck_id: DeckId) -> Result { 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); + } } diff --git a/ts/deck-options/DailyLimits.svelte b/ts/deck-options/DailyLimits.svelte index df8834f75..9bfd260bd 100644 --- a/ts/deck-options/DailyLimits.svelte +++ b/ts/deck-options/DailyLimits.svelte @@ -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 @@ + + + + + openHelpModal( + Object.keys(settings).indexOf("applyAllParentLimits"), + )} + > + {settings.applyAllParentLimits.title} + + + diff --git a/ts/deck-options/lib.ts b/ts/deck-options/lib.ts index 084f2c81f..bfdeffccf 100644 --- a/ts/deck-options/lib.ts +++ b/ts/deck-options/lib.ts @@ -41,6 +41,7 @@ export class DeckOptionsState { readonly defaults: DeckConfig_Config; readonly addonComponents: Writable; readonly newCardsIgnoreReviewLimit: Writable; + readonly applyAllParentLimits: Writable; readonly fsrs: Writable; readonly currentPresetName: Writable; @@ -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), }; }