diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 7064c6885..8a7b2b49a 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -243,6 +243,7 @@ Lee Doughty <32392044+leedoughty@users.noreply.github.com> memchr Max Romanowski Aldlss +jariji ******************** diff --git a/ftl/core/deck-config.ftl b/ftl/core/deck-config.ftl index 1e193dc04..fae113d8e 100644 --- a/ftl/core/deck-config.ftl +++ b/ftl/core/deck-config.ftl @@ -142,6 +142,8 @@ deck-config-new-gather-priority-tooltip-2 = can stop before all subdecks have been checked. This order is fastest in large collections, and allows you to prioritize subdecks that are closer to the top. + `Each from random deck`: Repeatedly picks a random deck and takes the first card from it. + `Ascending position`: Gathers cards by ascending position (due #), which is typically the oldest-added first. @@ -151,6 +153,7 @@ deck-config-new-gather-priority-tooltip-2 = `Random notes`: Picks notes at random, then gathers all of its cards. `Random cards`: Gathers cards in a random order. +deck-config-new-gather-priority-each-from-random-deck = Each from random deck deck-config-new-card-sort-order = New card sort order deck-config-new-card-sort-order-tooltip-2 = `Card type, then order gathered`: Shows cards in order of card type number. diff --git a/proto/anki/deck_config.proto b/proto/anki/deck_config.proto index 5ed02423e..3925d1df0 100644 --- a/proto/anki/deck_config.proto +++ b/proto/anki/deck_config.proto @@ -79,6 +79,8 @@ message DeckConfig { NEW_CARD_GATHER_PRIORITY_RANDOM_NOTES = 3; // Siblings are neither grouped nor ordered. NEW_CARD_GATHER_PRIORITY_RANDOM_CARDS = 4; + // Pick one card from random deck. + NEW_CARD_GATHER_PRIORITY_EACH_FROM_RANDOM_DECK = 6; } enum NewCardSortOrder { // Ascending card template ordinal. diff --git a/rslib/src/scheduler/queue/builder/gathering.rs b/rslib/src/scheduler/queue/builder/gathering.rs index 293b50dc4..135dbf3f9 100644 --- a/rslib/src/scheduler/queue/builder/gathering.rs +++ b/rslib/src/scheduler/queue/builder/gathering.rs @@ -1,6 +1,11 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use std::collections::HashSet; + +use rand::rng; +use rand::Rng; + use super::DueCard; use super::NewCard; use super::QueueBuilder; @@ -66,6 +71,9 @@ impl QueueBuilder { NewCardGatherPriority::Deck => { self.gather_new_cards_by_deck(col, NewCardSorting::LowestPosition) } + NewCardGatherPriority::EachFromRandomDeck => { + self.gather_new_cards_by_each_from_random_deck(col, NewCardSorting::LowestPosition) + } NewCardGatherPriority::DeckThenRandomNotes => { self.gather_new_cards_by_deck(col, NewCardSorting::RandomNotes(salt)) } @@ -110,6 +118,53 @@ impl QueueBuilder { Ok(()) } + fn gather_new_cards_by_each_from_random_deck( + &mut self, + col: &mut Collection, + sort: NewCardSorting, + ) -> Result<()> { + let mut deck_ids = col.storage.get_active_deck_ids_sorted()?; + let mut rng = rng(); + let mut cards_added = HashSet::::new(); + + // Continue until global limit is reached or no more decks with cards. + while !self.limits.root_limit_reached(LimitKind::New) && !deck_ids.is_empty() { + let selected_index = rng.random_range(0..deck_ids.len()); + let selected_deck = deck_ids[selected_index]; + + if self.limits.limit_reached(selected_deck, LimitKind::New)? { + // Remove the deck from the list since it's at its limit. + deck_ids.swap_remove(selected_index); + continue; + } + + let mut found_card = false; + col.storage + .for_each_new_card_in_deck(selected_deck, sort, |card| { + let limit_reached = self.limits.limit_reached(selected_deck, LimitKind::New)?; + if limit_reached { + Ok(false) + } else if !cards_added.contains(&card.id) && self.add_new_card(card) { + cards_added.insert(card.id); + self.limits + .decrement_deck_and_parent_limits(selected_deck, LimitKind::New)?; + found_card = true; + // Stop iterating this deck after getting one card. + Ok(false) + } else { + Ok(true) + } + })?; + + // If we couldn't find any card from this deck, remove it from consideration + if !found_card { + deck_ids.swap_remove(selected_index); + } + } + + Ok(()) + } + fn gather_new_cards_sorted( &mut self, col: &mut Collection, diff --git a/rslib/src/scheduler/queue/builder/mod.rs b/rslib/src/scheduler/queue/builder/mod.rs index 064220bce..b9f5190c7 100644 --- a/rslib/src/scheduler/queue/builder/mod.rs +++ b/rslib/src/scheduler/queue/builder/mod.rs @@ -425,6 +425,29 @@ mod test { ]; assert_eq!(col.queue_as_deck_and_template(parent.id), cards); + // each from random deck - test that we get expected count with variety across + // decks + col.set_deck_gather_order(&mut parent, NewCardGatherPriority::EachFromRandomDeck); + let cards = col.queue_as_deck_and_template(parent.id); + + // Verify we get cards from multiple decks + let unique_decks: std::collections::HashSet<_> = + cards.iter().map(|(deck_id, _)| *deck_id).collect(); + assert!( + unique_decks.len() > 1, + "EachFromRandomDeck should select from multiple decks" + ); + + // Verify the child limit is respected (child + grandchild <= 3) + let child_family_count = cards + .iter() + .filter(|(deck_id, _)| *deck_id == child.id || *deck_id == grandchild.id) + .count(); + assert!(child_family_count <= 3, "Child limit should be respected"); + + // Should get 7 (out of 8) cards total (respects child limit of 3) + assert_eq!(cards.len(), 7); + Ok(()) } diff --git a/ts/routes/deck-options/choices.ts b/ts/routes/deck-options/choices.ts index 6f34eae0e..1ddc87672 100644 --- a/ts/routes/deck-options/choices.ts +++ b/ts/routes/deck-options/choices.ts @@ -25,6 +25,10 @@ export function newGatherPriorityChoices(): Choice