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 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 shown when the review limit has been reached. If this option is enabled, new cards
will be shown regardless of the review limit. 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. deck-config-affects-entire-collection = Affects the entire collection.
## Daily limit tabs: please try to keep these as short as the English version, ## 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 // only applies to v3 scheduler
bool new_cards_ignore_review_limit = 7; bool new_cards_ignore_review_limit = 7;
bool fsrs = 8; bool fsrs = 8;
bool apply_all_parent_limits = 9;
} }
message UpdateDeckConfigsRequest { message UpdateDeckConfigsRequest {
@ -209,4 +210,5 @@ message UpdateDeckConfigsRequest {
DeckConfigsForUpdate.CurrentDeck.Limits limits = 6; DeckConfigsForUpdate.CurrentDeck.Limits limits = 6;
bool new_cards_ignore_review_limit = 7; bool new_cards_ignore_review_limit = 7;
bool fsrs = 8; bool fsrs = 8;
bool apply_all_parent_limits = 9;
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -142,11 +142,16 @@ impl AddAssign for NodeCountsV3 {
fn sum_counts_and_apply_limits_v3( fn sum_counts_and_apply_limits_v3(
node: &mut DeckTreeNode, node: &mut DeckTreeNode,
limits: &HashMap<DeckId, RemainingLimits>, limits: &HashMap<DeckId, RemainingLimits>,
mut parent_limits: Option<RemainingLimits>,
) -> NodeCountsV3 { ) -> NodeCountsV3 {
let remaining = limits let mut remaining = limits
.get(&DeckId(node.deck_id)) .get(&DeckId(node.deck_id))
.copied() .copied()
.unwrap_or_default(); .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 // initialize with this node's values
let mut this_node_uncapped = NodeCountsV3 { let mut this_node_uncapped = NodeCountsV3 {
@ -160,7 +165,7 @@ fn sum_counts_and_apply_limits_v3(
// add capped child counts / uncapped total // add capped child counts / uncapped total
for child in &mut node.children { 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; 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 learn_cutoff = (timestamp.0 as u32) + self.learn_ahead_secs();
let new_cards_ignore_review_limit = let new_cards_ignore_review_limit =
self.get_config_bool(BoolKey::NewCardsIgnoreReviewLimit); 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 counts = self.due_counts(days_elapsed, learn_cutoff)?;
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);
@ -275,7 +283,7 @@ impl Collection {
days_elapsed, days_elapsed,
new_cards_ignore_review_limit, 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) Ok(tree)

View file

@ -125,12 +125,18 @@ impl QueueBuilder {
pub(super) fn new(col: &mut Collection, deck_id: DeckId) -> Result<Self> { pub(super) fn new(col: &mut Collection, deck_id: DeckId) -> Result<Self> {
let timing = col.timing_for_timestamp(TimestampSecs::now())?; let timing = col.timing_for_timestamp(TimestampSecs::now())?;
let new_cards_ignore_review_limit = col.get_config_bool(BoolKey::NewCardsIgnoreReviewLimit); 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 config_map = col.storage.get_deck_config_map()?;
let root_deck = col.storage.get_deck(deck_id)?.or_not_found(deck_id)?; 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( let limits = LimitTreeMap::build(
&root_deck, &decks,
child_decks,
&config_map, &config_map,
timing.days_elapsed, timing.days_elapsed,
new_cards_ignore_review_limit, new_cards_ignore_review_limit,
@ -502,4 +508,20 @@ mod test {
CardAdder::new().siblings(2).due_dates(["0"]).add(&mut col); CardAdder::new().siblings(2).due_dates(["0"]).add(&mut col);
assert_eq!(col.card_queue_len(), 2); 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 limits = state.deckLimits;
const defaults = state.defaults; const defaults = state.defaults;
const newCardsIgnoreReviewLimit = state.newCardsIgnoreReviewLimit; const newCardsIgnoreReviewLimit = state.newCardsIgnoreReviewLimit;
const applyAllParentLimits = state.applyAllParentLimits;
const v3Extra = const v3Extra =
"\n\n" + tr.deckConfigLimitDeckV3() + "\n\n" + tr.deckConfigTabDescription(); "\n\n" + tr.deckConfigLimitDeckV3() + "\n\n" + tr.deckConfigTabDescription();
@ -53,6 +54,10 @@
tr.deckConfigAffectsEntireCollection() + tr.deckConfigAffectsEntireCollection() +
"\n\n" + "\n\n" +
tr.deckConfigNewCardsIgnoreReviewLimitTooltip(); tr.deckConfigNewCardsIgnoreReviewLimitTooltip();
const applyAllParentLimitsHelp =
tr.deckConfigAffectsEntireCollection() +
"\n\n" +
tr.deckConfigApplyAllParentLimitsTooltip();
$: reviewsTooLow = $: reviewsTooLow =
Math.min(9999, newValue * 10) > reviewsValue Math.min(9999, newValue * 10) > reviewsValue
@ -129,6 +134,11 @@
help: newCardsIgnoreReviewLimitHelp, help: newCardsIgnoreReviewLimitHelp,
url: HelpPage.DeckOptions.newCardsday, url: HelpPage.DeckOptions.newCardsday,
}, },
applyAllParentLimits: {
title: tr.deckConfigApplyAllParentLimits(),
help: applyAllParentLimitsHelp,
url: HelpPage.DeckOptions.newCardsday,
},
}; };
const helpSections = Object.values(settings) as HelpItem[]; const helpSections = Object.values(settings) as HelpItem[];
@ -193,5 +203,18 @@
</SettingTitle> </SettingTitle>
</SwitchRow> </SwitchRow>
</Item> </Item>
<Item>
<SwitchRow bind:value={$applyAllParentLimits} defaultValue={false}>
<SettingTitle
on:click={() =>
openHelpModal(
Object.keys(settings).indexOf("applyAllParentLimits"),
)}
>
{settings.applyAllParentLimits.title}
</SettingTitle>
</SwitchRow>
</Item>
</DynamicallySlottable> </DynamicallySlottable>
</TitledContainer> </TitledContainer>

View file

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