This commit is contained in:
David Grundberg 2026-01-09 20:02:55 +00:00 committed by GitHub
commit be7c6c4103
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 141 additions and 1 deletions

View file

@ -256,6 +256,7 @@ Eltaurus <https://github.com/Eltaurus-Lt>
jariji jariji
Francisco Esteva <fr.esteva@duocuc.cl> Francisco Esteva <fr.esteva@duocuc.cl>
SelfishPig <https://github.com/SelfishPig> SelfishPig <https://github.com/SelfishPig>
David Grundberg <75159519+individ-divided@users.noreply.github.com>
******************** ********************

View file

@ -142,6 +142,13 @@ deck-config-new-gather-priority-tooltip-2 =
can stop before all subdecks have been checked. This order is fastest in large collections, and 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. allows you to prioritize subdecks that are closer to the top.
`Interleaved Decks`: Gathers cards by taking one card at a time
from each deck in order. This allows new cards to be distributed
evenly across decks. Cards from each subdeck are gathered in
ascending position. If the number of decks are not equal to the
daily limit of the selected deck, the last round of cards will be
drawn from a random sample of decks.
`Ascending position`: Gathers cards by ascending position (due #), which is typically `Ascending position`: Gathers cards by ascending position (due #), which is typically
the oldest-added first. the oldest-added first.
@ -197,6 +204,8 @@ deck-config-display-order-will-use-current-deck =
deck-config-new-gather-priority-deck = Deck deck-config-new-gather-priority-deck = Deck
# Gather new cards ordered by deck, then ordered by random notes, ensuring all cards of the same note are grouped together. # Gather new cards ordered by deck, then ordered by random notes, ensuring all cards of the same note are grouped together.
deck-config-new-gather-priority-deck-then-random-notes = Deck, then random notes deck-config-new-gather-priority-deck-then-random-notes = Deck, then random notes
# Gather new cards by interleaving decks, taking one card at a time from each deck in order.
deck-config-new-gather-priority-interleaved-decks = Interleaved Decks
# Gather new cards ordered by position number, ascending (lowest to highest). # Gather new cards ordered by position number, ascending (lowest to highest).
deck-config-new-gather-priority-position-lowest-first = Ascending position deck-config-new-gather-priority-position-lowest-first = Ascending position
# Gather new cards ordered by position number, descending (highest to lowest). # Gather new cards ordered by position number, descending (highest to lowest).

View file

@ -69,6 +69,8 @@ message DeckConfig {
// Notes are randomly picked from each deck in alphabetical order. // Notes are randomly picked from each deck in alphabetical order.
// Siblings are consecutive, provided they have the same position. // Siblings are consecutive, provided they have the same position.
NEW_CARD_GATHER_PRIORITY_DECK_THEN_RANDOM_NOTES = 5; NEW_CARD_GATHER_PRIORITY_DECK_THEN_RANDOM_NOTES = 5;
// One card from each deck at a time, ascending position in each deck.
NEW_CARD_GATHER_PRIORITY_INTERLEAVED_DECKS = 6;
// Ascending position. // Ascending position.
// Siblings are consecutive, provided they have the same position. // Siblings are consecutive, provided they have the same position.
NEW_CARD_GATHER_PRIORITY_LOWEST_POSITION = 1; NEW_CARD_GATHER_PRIORITY_LOWEST_POSITION = 1;

View file

@ -351,7 +351,7 @@ impl LimitTreeMap {
.map(|node_id| self.get_node_limits(node_id)) .map(|node_id| self.get_node_limits(node_id))
} }
fn get_root_limits(&self) -> RemainingLimits { pub(crate) fn get_root_limits(&self) -> RemainingLimits {
self.get_node_limits(self.tree.root_node_id().unwrap()) self.get_node_limits(self.tree.root_node_id().unwrap())
} }

View file

@ -1,6 +1,11 @@
// 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::HashSet;
use rand::rng;
use rand::Rng;
use super::DueCard; use super::DueCard;
use super::NewCard; use super::NewCard;
use super::QueueBuilder; use super::QueueBuilder;
@ -69,6 +74,9 @@ impl QueueBuilder {
NewCardGatherPriority::DeckThenRandomNotes => { NewCardGatherPriority::DeckThenRandomNotes => {
self.gather_new_cards_by_deck(col, NewCardSorting::RandomNotes(salt)) self.gather_new_cards_by_deck(col, NewCardSorting::RandomNotes(salt))
} }
NewCardGatherPriority::InterleavedDecks => {
self.gather_new_cards_by_interleaved_decks(col, NewCardSorting::LowestPosition)
}
NewCardGatherPriority::LowestPosition => { NewCardGatherPriority::LowestPosition => {
self.gather_new_cards_sorted(col, NewCardSorting::LowestPosition) self.gather_new_cards_sorted(col, NewCardSorting::LowestPosition)
} }
@ -110,6 +118,91 @@ impl QueueBuilder {
Ok(()) Ok(())
} }
fn gather_new_cards_by_interleaved_decks(
&mut self,
col: &mut Collection,
sort: NewCardSorting,
) -> Result<()> {
struct InterleavedDeckData {
deck_id: DeckId,
depleted: bool,
cards: std::iter::Peekable<std::vec::IntoIter<NewCard>>,
}
let mut decks: Vec<InterleavedDeckData> = vec![];
for deck_id in col.storage.get_active_deck_ids_sorted()? {
let x: std::iter::Peekable<std::vec::IntoIter<NewCard>> = col
.storage
.new_cards_in_deck(deck_id, sort)?
.into_iter()
.peekable();
decks.push(InterleavedDeckData {
deck_id,
depleted: false,
cards: x,
});
}
let mut rng = rng();
let mut do_continue = true;
while do_continue {
do_continue = false;
let mut non_depleted_decks = 0;
for deck_data in &mut decks {
if self
.limits
.limit_reached(deck_data.deck_id, LimitKind::New)?
{
deck_data.depleted = true;
continue;
}
if deck_data.cards.peek().is_none() {
deck_data.depleted = true;
} else {
non_depleted_decks += 1;
}
}
let root_limit = self.limits.get_root_limits().get(LimitKind::New);
let mut sampled_deck_ids = HashSet::<DeckId>::new();
let sampling = root_limit < non_depleted_decks;
if sampling {
// switch to sampling
let mut deck_ids: Vec<DeckId> = vec![];
for deck_data in &decks {
if !deck_data.depleted {
deck_ids.push(deck_data.deck_id);
}
}
for _i in 0..root_limit {
let selected_index = rng.random_range(0..deck_ids.len());
let selected_deck_id = deck_ids[selected_index];
sampled_deck_ids.insert(selected_deck_id);
deck_ids.swap_remove(selected_index);
}
}
for deck_data in &mut decks {
if self.limits.root_limit_reached(LimitKind::New) {
do_continue = false;
break;
}
if sampling && !sampled_deck_ids.contains(&deck_data.deck_id) {
continue;
}
if deck_data.depleted {
continue;
}
if let Some(card) = deck_data.cards.next() {
if self.add_new_card(card) {
self.limits
.decrement_deck_and_parent_limits(deck_data.deck_id, LimitKind::New)?;
do_continue = true;
}
}
}
}
Ok(())
}
fn gather_new_cards_sorted( fn gather_new_cards_sorted(
&mut self, &mut self,
col: &mut Collection, col: &mut Collection,

View file

@ -399,6 +399,19 @@ mod test {
]; ];
assert_eq!(col.queue_as_deck_and_template(parent.id), cards); assert_eq!(col.queue_as_deck_and_template(parent.id), cards);
col.set_deck_gather_order(&mut parent, NewCardGatherPriority::InterleavedDecks);
let cards = vec![
(parent.id, 0),
(child.id, 0),
(grandchild.id, 0),
(child_2.id, 0),
(parent.id, 1),
(child.id, 1),
(grandchild.id, 1),
(child_2.id, 1),
];
assert_eq!(col.queue_as_deck_and_template(parent.id), cards);
// insertion order // insertion order
col.set_deck_gather_order(&mut parent, NewCardGatherPriority::LowestPosition); col.set_deck_gather_order(&mut parent, NewCardGatherPriority::LowestPosition);
let cards = vec![ let cards = vec![

View file

@ -341,6 +341,24 @@ impl super::SqliteStorage {
Ok(()) Ok(())
} }
pub(crate) fn new_cards_in_deck(
&self,
deck: DeckId,
sort: NewCardSorting,
) -> Result<Vec<NewCard>> {
let mut stmt = self.db.prepare_cached(&format!(
"{} ORDER BY {}",
include_str!("new_cards.sql"),
sort.write()
))?;
let mut rows = stmt.query(params![deck])?;
let mut names = Vec::new();
while let Some(row) = rows.next()? {
names.push(row_to_new_card(row)?);
}
Ok(names)
}
/// Call func() for each new card in the active decks, stopping when it /// Call func() for each new card in the active decks, stopping when it
/// returns false or no more cards found. /// returns false or no more cards found.
pub(crate) fn for_each_new_card_in_active_decks<F>( pub(crate) fn for_each_new_card_in_active_decks<F>(

View file

@ -25,6 +25,10 @@ export function newGatherPriorityChoices(): Choice<DeckConfig_Config_NewCardGath
label: tr.deckConfigNewGatherPriorityDeckThenRandomNotes(), label: tr.deckConfigNewGatherPriorityDeckThenRandomNotes(),
value: DeckConfig_Config_NewCardGatherPriority.DECK_THEN_RANDOM_NOTES, value: DeckConfig_Config_NewCardGatherPriority.DECK_THEN_RANDOM_NOTES,
}, },
{
label: tr.deckConfigNewGatherPriorityInterleavedDecks(),
value: DeckConfig_Config_NewCardGatherPriority.INTERLEAVED_DECKS,
},
{ {
label: tr.deckConfigNewGatherPriorityPositionLowestFirst(), label: tr.deckConfigNewGatherPriorityPositionLowestFirst(),
value: DeckConfig_Config_NewCardGatherPriority.LOWEST_POSITION, value: DeckConfig_Config_NewCardGatherPriority.LOWEST_POSITION,