mirror of
https://github.com/ankitects/anki.git
synced 2025-12-20 10:22:57 -05:00
set_deck()
This commit is contained in:
parent
9214c4a700
commit
56ceb6ba76
13 changed files with 167 additions and 139 deletions
|
|
@ -150,6 +150,7 @@ service BackendService {
|
|||
rpc UpdateCard (Card) returns (Empty);
|
||||
rpc AddCard (Card) returns (CardID);
|
||||
rpc RemoveCards (RemoveCardsIn) returns (Empty);
|
||||
rpc SetDeck (SetDeckIn) returns (Empty);
|
||||
|
||||
// notes
|
||||
|
||||
|
|
@ -1081,3 +1082,8 @@ message SortDeckIn {
|
|||
int64 deck_id = 1;
|
||||
bool randomize = 2;
|
||||
}
|
||||
|
||||
message SetDeckIn {
|
||||
repeated int64 card_ids = 1;
|
||||
int64 deck_id = 2;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -384,6 +384,9 @@ class Collection:
|
|||
"You probably want .remove_notes_by_card() instead."
|
||||
self.backend.remove_cards(card_ids=card_ids)
|
||||
|
||||
def set_deck(self, card_ids: List[int], deck_id: int) -> None:
|
||||
self.backend.set_deck(card_ids=card_ids, deck_id=deck_id)
|
||||
|
||||
# legacy
|
||||
|
||||
def remCards(self, ids: List[int], notes: bool = True) -> None:
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ from anki.models import NoteType
|
|||
from anki.notes import Note
|
||||
from anki.rsbackend import TR, DeckTreeNode, InvalidInput
|
||||
from anki.stats import CardStats
|
||||
from anki.utils import htmlToTextLine, ids2str, intTime, isMac, isWin
|
||||
from anki.utils import htmlToTextLine, ids2str, isMac, isWin
|
||||
from aqt import AnkiQt, gui_hooks
|
||||
from aqt.editor import Editor
|
||||
from aqt.exporting import ExportDialog
|
||||
|
|
@ -1601,21 +1601,7 @@ where id in %s"""
|
|||
return
|
||||
self.model.beginReset()
|
||||
self.mw.checkpoint(_("Change Deck"))
|
||||
mod = intTime()
|
||||
usn = self.col.usn()
|
||||
# normal cards
|
||||
scids = ids2str(cids)
|
||||
# remove any cards from filtered deck first
|
||||
self.col.sched.remFromDyn(cids)
|
||||
# then move into new deck
|
||||
self.col.db.execute(
|
||||
"""
|
||||
update cards set usn=?, mod=?, did=? where id in """
|
||||
+ scids,
|
||||
usn,
|
||||
mod,
|
||||
did,
|
||||
)
|
||||
self.col.set_deck(cids, did)
|
||||
self.model.endReset()
|
||||
self.mw.requireReset(reason=ResetReason.BrowserSetDeck, context=self)
|
||||
|
||||
|
|
|
|||
|
|
@ -817,6 +817,12 @@ impl BackendService for Backend {
|
|||
})
|
||||
}
|
||||
|
||||
fn set_deck(&mut self, input: pb::SetDeckIn) -> BackendResult<Empty> {
|
||||
let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect();
|
||||
let deck_id = input.deck_id.into();
|
||||
self.with_col(|col| col.set_deck(&cids, deck_id).map(Into::into))
|
||||
}
|
||||
|
||||
// notes
|
||||
//-------------------------------------------------------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::decks::{DeckFilterContext, DeckID};
|
||||
use crate::decks::DeckID;
|
||||
use crate::define_newtype;
|
||||
use crate::err::{AnkiError, Result};
|
||||
use crate::notes::NoteID;
|
||||
|
|
@ -102,103 +102,10 @@ impl Card {
|
|||
self.usn = usn;
|
||||
}
|
||||
|
||||
pub(crate) fn move_into_filtered_deck(&mut self, ctx: &DeckFilterContext, position: i32) {
|
||||
// filtered and v1 learning cards are excluded, so odue should be guaranteed to be zero
|
||||
if self.original_due != 0 {
|
||||
println!("bug: odue was set");
|
||||
return;
|
||||
}
|
||||
|
||||
self.original_deck_id = self.deck_id;
|
||||
self.deck_id = ctx.target_deck;
|
||||
|
||||
self.original_due = self.due;
|
||||
|
||||
if ctx.scheduler == SchedulerVersion::V1 {
|
||||
if self.ctype == CardType::Review && self.due <= ctx.today as i32 {
|
||||
// review cards that are due are left in the review queue
|
||||
} else {
|
||||
// new + non-due go into new queue
|
||||
self.queue = CardQueue::New;
|
||||
}
|
||||
if self.due != 0 {
|
||||
self.due = position;
|
||||
}
|
||||
} else {
|
||||
// if rescheduling is disabled, all cards go in the review queue
|
||||
if !ctx.config.reschedule {
|
||||
self.queue = CardQueue::Review;
|
||||
}
|
||||
// fixme: can we unify this with v1 scheduler in the future?
|
||||
// https://anki.tenderapp.com/discussions/ankidesktop/35978-rebuilding-filtered-deck-on-experimental-v2-empties-deck-and-reschedules-to-the-year-1745
|
||||
if self.due > 0 {
|
||||
self.due = position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Restores to the original deck and clears original_due.
|
||||
/// This does not update the queue or type, so should only be used as
|
||||
/// part of an operation that adjusts those separately.
|
||||
pub(crate) fn remove_from_filtered_deck_before_reschedule(&mut self) {
|
||||
if self.original_deck_id.0 != 0 {
|
||||
self.deck_id = self.original_deck_id;
|
||||
self.original_deck_id.0 = 0;
|
||||
self.original_due = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove_from_filtered_deck_restoring_queue(&mut self, sched: SchedulerVersion) {
|
||||
if self.original_deck_id.0 == 0 {
|
||||
// not in a filtered deck
|
||||
return;
|
||||
}
|
||||
|
||||
self.deck_id = self.original_deck_id;
|
||||
self.original_deck_id.0 = 0;
|
||||
|
||||
match sched {
|
||||
SchedulerVersion::V1 => {
|
||||
self.due = self.original_due;
|
||||
self.queue = match self.ctype {
|
||||
CardType::New => CardQueue::New,
|
||||
CardType::Learn => CardQueue::New,
|
||||
CardType::Review => CardQueue::Review,
|
||||
// not applicable in v1, should not happen
|
||||
CardType::Relearn => {
|
||||
println!("did not expect relearn type in v1 for card {}", self.id);
|
||||
CardQueue::New
|
||||
}
|
||||
};
|
||||
if self.ctype == CardType::Learn {
|
||||
self.ctype = CardType::New;
|
||||
}
|
||||
}
|
||||
SchedulerVersion::V2 => {
|
||||
// original_due is cleared if card answered in filtered deck
|
||||
if self.original_due > 0 {
|
||||
self.due = self.original_due;
|
||||
}
|
||||
|
||||
if (self.queue as i8) >= 0 {
|
||||
self.queue = match self.ctype {
|
||||
CardType::Learn | CardType::Relearn => {
|
||||
if self.due > 1_000_000_000 {
|
||||
// unix timestamp
|
||||
CardQueue::Learn
|
||||
} else {
|
||||
// day number
|
||||
CardQueue::DayLearn
|
||||
}
|
||||
}
|
||||
CardType::New => CardQueue::New,
|
||||
CardType::Review => CardQueue::Review,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.original_due = 0;
|
||||
/// Caller must ensure provided deck exists and is not filtered.
|
||||
fn set_deck(&mut self, deck: DeckID, sched: SchedulerVersion) {
|
||||
self.remove_from_filtered_deck_restoring_queue(sched);
|
||||
self.deck_id = deck;
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
|
|
@ -289,6 +196,27 @@ impl Collection {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_deck(&mut self, cards: &[CardID], deck_id: DeckID) -> Result<()> {
|
||||
let deck = self.get_deck(deck_id)?.ok_or(AnkiError::NotFound)?;
|
||||
if deck.is_filtered() {
|
||||
return Err(AnkiError::DeckIsFiltered);
|
||||
}
|
||||
self.storage.set_search_table_to_card_ids(cards)?;
|
||||
let sched = self.sched_ver();
|
||||
let usn = self.usn()?;
|
||||
self.transact(None, |col| {
|
||||
for mut card in col.storage.all_searched_cards()? {
|
||||
if card.deck_id == deck_id {
|
||||
continue;
|
||||
}
|
||||
let original = card.clone();
|
||||
card.set_deck(deck_id, sched);
|
||||
col.update_card(&mut card, &original, usn)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -17,11 +17,9 @@ use crate::{
|
|||
types::Usn,
|
||||
};
|
||||
mod counts;
|
||||
mod filtered;
|
||||
mod schema11;
|
||||
mod tree;
|
||||
pub(crate) use counts::DueCounts;
|
||||
pub(crate) use filtered::DeckFilterContext;
|
||||
pub use schema11::DeckSchema11;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use super::{Deck, DeckID};
|
||||
pub use crate::backend_proto::{
|
||||
deck_kind::Kind as DeckKind, filtered_search_term::FilteredSearchOrder, Deck as DeckProto,
|
||||
DeckCommon, DeckKind as DeckKindProto, FilteredDeck, FilteredSearchTerm, NormalDeck,
|
||||
};
|
||||
use crate::decks::{Deck, DeckID};
|
||||
use crate::{
|
||||
card::{CardID, CardQueue},
|
||||
card::{Card, CardID, CardQueue, CardType},
|
||||
collection::Collection,
|
||||
config::SchedulerVersion,
|
||||
err::Result,
|
||||
|
|
@ -17,6 +17,107 @@ use crate::{
|
|||
types::Usn,
|
||||
};
|
||||
|
||||
impl Card {
|
||||
pub(crate) fn move_into_filtered_deck(&mut self, ctx: &DeckFilterContext, position: i32) {
|
||||
// filtered and v1 learning cards are excluded, so odue should be guaranteed to be zero
|
||||
if self.original_due != 0 {
|
||||
println!("bug: odue was set");
|
||||
return;
|
||||
}
|
||||
|
||||
self.original_deck_id = self.deck_id;
|
||||
self.deck_id = ctx.target_deck;
|
||||
|
||||
self.original_due = self.due;
|
||||
|
||||
if ctx.scheduler == SchedulerVersion::V1 {
|
||||
if self.ctype == CardType::Review && self.due <= ctx.today as i32 {
|
||||
// review cards that are due are left in the review queue
|
||||
} else {
|
||||
// new + non-due go into new queue
|
||||
self.queue = CardQueue::New;
|
||||
}
|
||||
if self.due != 0 {
|
||||
self.due = position;
|
||||
}
|
||||
} else {
|
||||
// if rescheduling is disabled, all cards go in the review queue
|
||||
if !ctx.config.reschedule {
|
||||
self.queue = CardQueue::Review;
|
||||
}
|
||||
// fixme: can we unify this with v1 scheduler in the future?
|
||||
// https://anki.tenderapp.com/discussions/ankidesktop/35978-rebuilding-filtered-deck-on-experimental-v2-empties-deck-and-reschedules-to-the-year-1745
|
||||
if self.due > 0 {
|
||||
self.due = position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Restores to the original deck and clears original_due.
|
||||
/// This does not update the queue or type, so should only be used as
|
||||
/// part of an operation that adjusts those separately.
|
||||
pub(crate) fn remove_from_filtered_deck_before_reschedule(&mut self) {
|
||||
if self.original_deck_id.0 != 0 {
|
||||
self.deck_id = self.original_deck_id;
|
||||
self.original_deck_id.0 = 0;
|
||||
self.original_due = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove_from_filtered_deck_restoring_queue(&mut self, sched: SchedulerVersion) {
|
||||
if self.original_deck_id.0 == 0 {
|
||||
// not in a filtered deck
|
||||
return;
|
||||
}
|
||||
|
||||
self.deck_id = self.original_deck_id;
|
||||
self.original_deck_id.0 = 0;
|
||||
|
||||
match sched {
|
||||
SchedulerVersion::V1 => {
|
||||
self.due = self.original_due;
|
||||
self.queue = match self.ctype {
|
||||
CardType::New => CardQueue::New,
|
||||
CardType::Learn => CardQueue::New,
|
||||
CardType::Review => CardQueue::Review,
|
||||
// not applicable in v1, should not happen
|
||||
CardType::Relearn => {
|
||||
println!("did not expect relearn type in v1 for card {}", self.id);
|
||||
CardQueue::New
|
||||
}
|
||||
};
|
||||
if self.ctype == CardType::Learn {
|
||||
self.ctype = CardType::New;
|
||||
}
|
||||
}
|
||||
SchedulerVersion::V2 => {
|
||||
// original_due is cleared if card answered in filtered deck
|
||||
if self.original_due > 0 {
|
||||
self.due = self.original_due;
|
||||
}
|
||||
|
||||
if (self.queue as i8) >= 0 {
|
||||
self.queue = match self.ctype {
|
||||
CardType::Learn | CardType::Relearn => {
|
||||
if self.due > 1_000_000_000 {
|
||||
// unix timestamp
|
||||
CardQueue::Learn
|
||||
} else {
|
||||
// day number
|
||||
CardQueue::DayLearn
|
||||
}
|
||||
}
|
||||
CardType::New => CardQueue::New,
|
||||
CardType::Review => CardQueue::Review,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.original_due = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl Deck {
|
||||
pub fn new_filtered() -> Deck {
|
||||
let mut filt = FilteredDeck::default();
|
||||
|
|
@ -13,6 +13,7 @@ pub mod dbcheck;
|
|||
pub mod deckconf;
|
||||
pub mod decks;
|
||||
pub mod err;
|
||||
pub mod filtered;
|
||||
pub mod findreplace;
|
||||
pub mod i18n;
|
||||
pub mod latex;
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ impl Collection {
|
|||
|
||||
pub fn unbury_or_unsuspend_cards(&mut self, cids: &[CardID]) -> Result<()> {
|
||||
self.transact(None, |col| {
|
||||
col.set_search_table_to_card_ids(cids)?;
|
||||
col.storage.set_search_table_to_card_ids(cids)?;
|
||||
col.unsuspend_or_unbury_searched_cards()
|
||||
})
|
||||
}
|
||||
|
|
@ -143,7 +143,7 @@ impl Collection {
|
|||
mode: pb::bury_or_suspend_cards_in::Mode,
|
||||
) -> Result<()> {
|
||||
self.transact(None, |col| {
|
||||
col.set_search_table_to_card_ids(cids)?;
|
||||
col.storage.set_search_table_to_card_ids(cids)?;
|
||||
col.bury_or_suspend_searched_cards(mode)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ impl Collection {
|
|||
let usn = self.usn()?;
|
||||
let mut position = self.get_next_card_position();
|
||||
self.transact(None, |col| {
|
||||
col.set_search_table_to_card_ids(cids)?;
|
||||
col.storage.set_search_table_to_card_ids(cids)?;
|
||||
let cards = col.storage.all_searched_cards()?;
|
||||
for mut card in cards {
|
||||
let original = card.clone();
|
||||
|
|
@ -113,7 +113,7 @@ impl Collection {
|
|||
if shift {
|
||||
self.shift_existing_cards(starting_from, step * cids.len() as u32, usn)?;
|
||||
}
|
||||
self.set_search_table_to_card_ids(cids)?;
|
||||
self.storage.set_search_table_to_card_ids(cids)?;
|
||||
let cards = self.storage.all_searched_cards()?;
|
||||
let sorter = NewCardSorter::new(&cards, starting_from, step, random);
|
||||
for mut card in cards {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ impl Collection {
|
|||
let mut rng = rand::thread_rng();
|
||||
let distribution = Uniform::from(min_days..=max_days);
|
||||
self.transact(None, |col| {
|
||||
col.set_search_table_to_card_ids(cids)?;
|
||||
col.storage.set_search_table_to_card_ids(cids)?;
|
||||
for mut card in col.storage.all_searched_cards()? {
|
||||
let original = card.clone();
|
||||
let interval = distribution.sample(&mut rng);
|
||||
|
|
|
|||
|
|
@ -106,22 +106,6 @@ impl Collection {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Injects the provided card IDs into the search_cids table, for
|
||||
/// when ids have arrived outside of a search.
|
||||
/// Clear with clear_searched_cards().
|
||||
pub(crate) fn set_search_table_to_card_ids(&mut self, cards: &[CardID]) -> Result<()> {
|
||||
self.storage.setup_searched_cards_table()?;
|
||||
let mut stmt = self
|
||||
.storage
|
||||
.db
|
||||
.prepare_cached("insert into search_cids values (?)")?;
|
||||
for cid in cards {
|
||||
stmt.execute(&[cid])?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// If the sort mode is based on a config setting, look it up.
|
||||
fn resolve_config_sort(&self, mode: &mut SortMode) {
|
||||
if mode == &SortMode::FromConfig {
|
||||
|
|
|
|||
|
|
@ -335,6 +335,21 @@ impl super::SqliteStorage {
|
|||
.execute("drop table if exists search_cids", NO_PARAMS)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Injects the provided card IDs into the search_cids table, for
|
||||
/// when ids have arrived outside of a search.
|
||||
/// Clear with clear_searched_cards().
|
||||
pub(crate) fn set_search_table_to_card_ids(&mut self, cards: &[CardID]) -> Result<()> {
|
||||
self.setup_searched_cards_table()?;
|
||||
let mut stmt = self
|
||||
.db
|
||||
.prepare_cached("insert into search_cids values (?)")?;
|
||||
for cid in cards {
|
||||
stmt.execute(&[cid])?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
Loading…
Reference in a new issue