support undo of filtered deck build/empty
This commit is contained in:
parent
2a168adb66
commit
12597e1094
@ -19,3 +19,4 @@ undo-update-card = Update Card
|
|||||||
undo-update-deck = Update Deck
|
undo-update-deck = Update Deck
|
||||||
undo-forget-card = Forget Card
|
undo-forget-card = Forget Card
|
||||||
undo-set-flag = Set Flag
|
undo-set-flag = Set Flag
|
||||||
|
undo-build-filtered-deck = Build Deck
|
||||||
|
@ -433,9 +433,9 @@ class DeckManager:
|
|||||||
"The currrently active dids."
|
"The currrently active dids."
|
||||||
return self.col.get_config("activeDecks", [1])
|
return self.col.get_config("activeDecks", [1])
|
||||||
|
|
||||||
def selected(self) -> int:
|
def selected(self) -> DeckID:
|
||||||
"The currently selected did."
|
"The currently selected did."
|
||||||
return int(self.col.conf["curDeck"])
|
return DeckID(int(self.col.conf["curDeck"]))
|
||||||
|
|
||||||
def current(self) -> Deck:
|
def current(self) -> Deck:
|
||||||
return self.get(self.selected())
|
return self.get(self.selected())
|
||||||
|
@ -71,6 +71,7 @@ class SchedulerBase:
|
|||||||
|
|
||||||
# fixme: used by custom study
|
# fixme: used by custom study
|
||||||
def totalRevForCurrentDeck(self) -> int:
|
def totalRevForCurrentDeck(self) -> int:
|
||||||
|
assert self.col.db
|
||||||
return self.col.db.scalar(
|
return self.col.db.scalar(
|
||||||
f"""
|
f"""
|
||||||
select count() from cards where id in (
|
select count() from cards where id in (
|
||||||
@ -88,11 +89,11 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
|
|||||||
# Filtered deck handling
|
# Filtered deck handling
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def rebuild_filtered_deck(self, deck_id: int) -> int:
|
def rebuild_filtered_deck(self, deck_id: int) -> OpChangesWithCount:
|
||||||
return self.col._backend.rebuild_filtered_deck(deck_id)
|
return self.col._backend.rebuild_filtered_deck(deck_id)
|
||||||
|
|
||||||
def empty_filtered_deck(self, deck_id: int) -> None:
|
def empty_filtered_deck(self, deck_id: int) -> OpChanges:
|
||||||
self.col._backend.empty_filtered_deck(deck_id)
|
return self.col._backend.empty_filtered_deck(deck_id)
|
||||||
|
|
||||||
# Suspending & burying
|
# Suspending & burying
|
||||||
##########################################################################
|
##########################################################################
|
||||||
@ -140,17 +141,22 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
|
|||||||
"""Set cards to be due in `days`, turning them into review cards if necessary.
|
"""Set cards to be due in `days`, turning them into review cards if necessary.
|
||||||
`days` can be of the form '5' or '5..7'
|
`days` can be of the form '5' or '5..7'
|
||||||
If `config_key` is provided, provided days will be remembered in config."""
|
If `config_key` is provided, provided days will be remembered in config."""
|
||||||
|
key: Optional[Config.String]
|
||||||
if config_key:
|
if config_key:
|
||||||
key = Config.String(key=config_key)
|
key = Config.String(key=config_key)
|
||||||
else:
|
else:
|
||||||
key = None
|
key = None
|
||||||
return self.col._backend.set_due_date(
|
return self.col._backend.set_due_date(
|
||||||
card_ids=card_ids, days=days, config_key=key
|
card_ids=card_ids,
|
||||||
|
days=days,
|
||||||
|
# this value is optional; the auto-generated typing is wrong
|
||||||
|
config_key=key, # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
def resetCards(self, ids: List[int]) -> None:
|
def resetCards(self, ids: List[int]) -> None:
|
||||||
"Completely reset cards for export."
|
"Completely reset cards for export."
|
||||||
sids = ids2str(ids)
|
sids = ids2str(ids)
|
||||||
|
assert self.col.db
|
||||||
# we want to avoid resetting due number of existing new cards on export
|
# we want to avoid resetting due number of existing new cards on export
|
||||||
nonNew = self.col.db.list(
|
nonNew = self.col.db.list(
|
||||||
f"select id from cards where id in %s and (queue != {QUEUE_TYPE_NEW} or type != {CARD_TYPE_NEW})"
|
f"select id from cards where id in %s and (queue != {QUEUE_TYPE_NEW} or type != {CARD_TYPE_NEW})"
|
||||||
|
@ -50,7 +50,7 @@ class SchedulerBaseWithLegacy(SchedulerBase):
|
|||||||
|
|
||||||
def rebuildDyn(self, did: Optional[int] = None) -> Optional[int]:
|
def rebuildDyn(self, did: Optional[int] = None) -> Optional[int]:
|
||||||
did = did or self.col.decks.selected()
|
did = did or self.col.decks.selected()
|
||||||
count = self.rebuild_filtered_deck(did) or None
|
count = self.rebuild_filtered_deck(did).count or None
|
||||||
if not count:
|
if not count:
|
||||||
return None
|
return None
|
||||||
# and change to our new deck
|
# and change to our new deck
|
||||||
|
@ -16,6 +16,8 @@ disallow_untyped_defs = False
|
|||||||
[mypy-anki.exporting]
|
[mypy-anki.exporting]
|
||||||
disallow_untyped_defs = False
|
disallow_untyped_defs = False
|
||||||
|
|
||||||
|
[mypy-anki.scheduler.base]
|
||||||
|
no_strict_optional = false
|
||||||
|
|
||||||
[mypy-win32file]
|
[mypy-win32file]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
@ -8,6 +8,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
|
|||||||
import aqt
|
import aqt
|
||||||
from anki.collection import OpChanges
|
from anki.collection import OpChanges
|
||||||
from aqt import gui_hooks
|
from aqt import gui_hooks
|
||||||
|
from aqt.scheduling_ops import empty_filtered_deck, rebuild_filtered_deck
|
||||||
from aqt.sound import av_player
|
from aqt.sound import av_player
|
||||||
from aqt.toolbar import BottomBar
|
from aqt.toolbar import BottomBar
|
||||||
from aqt.utils import TR, askUserDialog, openLink, shortcut, tooltip, tr
|
from aqt.utils import TR, askUserDialog, openLink, shortcut, tooltip, tr
|
||||||
@ -52,12 +53,12 @@ class Overview:
|
|||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def refresh(self) -> None:
|
def refresh(self) -> None:
|
||||||
|
self._refresh_needed = False
|
||||||
self.mw.col.reset()
|
self.mw.col.reset()
|
||||||
self._renderPage()
|
self._renderPage()
|
||||||
self._renderBottom()
|
self._renderBottom()
|
||||||
self.mw.web.setFocus()
|
self.mw.web.setFocus()
|
||||||
gui_hooks.overview_did_refresh(self)
|
gui_hooks.overview_did_refresh(self)
|
||||||
self._refresh_needed = False
|
|
||||||
|
|
||||||
def refresh_if_needed(self) -> None:
|
def refresh_if_needed(self) -> None:
|
||||||
if self._refresh_needed:
|
if self._refresh_needed:
|
||||||
@ -88,11 +89,9 @@ class Overview:
|
|||||||
elif url == "cram":
|
elif url == "cram":
|
||||||
aqt.dialogs.open("DynDeckConfDialog", self.mw)
|
aqt.dialogs.open("DynDeckConfDialog", self.mw)
|
||||||
elif url == "refresh":
|
elif url == "refresh":
|
||||||
self.mw.col.sched.rebuild_filtered_deck(self.mw.col.decks.selected())
|
self.rebuild_current_filtered_deck()
|
||||||
self.mw.reset()
|
|
||||||
elif url == "empty":
|
elif url == "empty":
|
||||||
self.mw.col.sched.empty_filtered_deck(self.mw.col.decks.selected())
|
self.empty_current_filtered_deck()
|
||||||
self.mw.reset()
|
|
||||||
elif url == "decks":
|
elif url == "decks":
|
||||||
self.mw.moveToState("deckBrowser")
|
self.mw.moveToState("deckBrowser")
|
||||||
elif url == "review":
|
elif url == "review":
|
||||||
@ -108,27 +107,25 @@ class Overview:
|
|||||||
def _shortcutKeys(self) -> List[Tuple[str, Callable]]:
|
def _shortcutKeys(self) -> List[Tuple[str, Callable]]:
|
||||||
return [
|
return [
|
||||||
("o", self.mw.onDeckConf),
|
("o", self.mw.onDeckConf),
|
||||||
("r", self.onRebuildKey),
|
("r", self.rebuild_current_filtered_deck),
|
||||||
("e", self.onEmptyKey),
|
("e", self.empty_current_filtered_deck),
|
||||||
("c", self.onCustomStudyKey),
|
("c", self.onCustomStudyKey),
|
||||||
("u", self.onUnbury),
|
("u", self.onUnbury),
|
||||||
]
|
]
|
||||||
|
|
||||||
def _filteredDeck(self) -> int:
|
def _current_deck_is_filtered(self) -> int:
|
||||||
return self.mw.col.decks.current()["dyn"]
|
return self.mw.col.decks.current()["dyn"]
|
||||||
|
|
||||||
def onRebuildKey(self) -> None:
|
def rebuild_current_filtered_deck(self) -> None:
|
||||||
if self._filteredDeck():
|
if self._current_deck_is_filtered():
|
||||||
self.mw.col.sched.rebuild_filtered_deck(self.mw.col.decks.selected())
|
rebuild_filtered_deck(mw=self.mw, deck_id=self.mw.col.decks.selected())
|
||||||
self.mw.reset()
|
|
||||||
|
|
||||||
def onEmptyKey(self) -> None:
|
def empty_current_filtered_deck(self) -> None:
|
||||||
if self._filteredDeck():
|
if self._current_deck_is_filtered():
|
||||||
self.mw.col.sched.empty_filtered_deck(self.mw.col.decks.selected())
|
empty_filtered_deck(mw=self.mw, deck_id=self.mw.col.decks.selected())
|
||||||
self.mw.reset()
|
|
||||||
|
|
||||||
def onCustomStudyKey(self) -> None:
|
def onCustomStudyKey(self) -> None:
|
||||||
if not self._filteredDeck():
|
if not self._current_deck_is_filtered():
|
||||||
self.onStudyMore()
|
self.onStudyMore()
|
||||||
|
|
||||||
def onUnbury(self) -> None:
|
def onUnbury(self) -> None:
|
||||||
|
@ -7,6 +7,7 @@ from typing import List, Optional, Sequence
|
|||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.collection import CARD_TYPE_NEW, Config
|
from anki.collection import CARD_TYPE_NEW, Config
|
||||||
|
from anki.decks import DeckID
|
||||||
from anki.lang import TR
|
from anki.lang import TR
|
||||||
from aqt import AnkiQt
|
from aqt import AnkiQt
|
||||||
from aqt.main import PerformOpOptionalSuccessCallback
|
from aqt.main import PerformOpOptionalSuccessCallback
|
||||||
@ -173,3 +174,11 @@ def bury_note(
|
|||||||
lambda: mw.col.card_ids_of_note(note_id),
|
lambda: mw.col.card_ids_of_note(note_id),
|
||||||
lambda future: bury_cards(mw=mw, card_ids=future.result(), success=success),
|
lambda future: bury_cards(mw=mw, card_ids=future.result(), success=success),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def rebuild_filtered_deck(*, mw: AnkiQt, deck_id: DeckID) -> None:
|
||||||
|
mw.perform_op(lambda: mw.col.sched.rebuild_filtered_deck(deck_id))
|
||||||
|
|
||||||
|
|
||||||
|
def empty_filtered_deck(*, mw: AnkiQt, deck_id: DeckID) -> None:
|
||||||
|
mw.perform_op(lambda: mw.col.sched.empty_filtered_deck(deck_id))
|
||||||
|
@ -121,8 +121,8 @@ service SchedulingService {
|
|||||||
rpc RestoreBuriedAndSuspendedCards(CardIDs) returns (OpChanges);
|
rpc RestoreBuriedAndSuspendedCards(CardIDs) returns (OpChanges);
|
||||||
rpc UnburyCardsInCurrentDeck(UnburyCardsInCurrentDeckIn) returns (Empty);
|
rpc UnburyCardsInCurrentDeck(UnburyCardsInCurrentDeckIn) returns (Empty);
|
||||||
rpc BuryOrSuspendCards(BuryOrSuspendCardsIn) returns (OpChanges);
|
rpc BuryOrSuspendCards(BuryOrSuspendCardsIn) returns (OpChanges);
|
||||||
rpc EmptyFilteredDeck(DeckID) returns (Empty);
|
rpc EmptyFilteredDeck(DeckID) returns (OpChanges);
|
||||||
rpc RebuildFilteredDeck(DeckID) returns (UInt32);
|
rpc RebuildFilteredDeck(DeckID) returns (OpChangesWithCount);
|
||||||
rpc ScheduleCardsAsNew(ScheduleCardsAsNewIn) returns (OpChanges);
|
rpc ScheduleCardsAsNew(ScheduleCardsAsNewIn) returns (OpChanges);
|
||||||
rpc SetDueDate(SetDueDateIn) returns (OpChanges);
|
rpc SetDueDate(SetDueDateIn) returns (OpChanges);
|
||||||
rpc SortCards(SortCardsIn) returns (OpChangesWithCount);
|
rpc SortCards(SortCardsIn) returns (OpChangesWithCount);
|
||||||
@ -148,6 +148,8 @@ service DecksService {
|
|||||||
rpc RemoveDecks(DeckIDs) returns (OpChangesWithCount);
|
rpc RemoveDecks(DeckIDs) returns (OpChangesWithCount);
|
||||||
rpc ReparentDecks(ReparentDecksIn) returns (OpChangesWithCount);
|
rpc ReparentDecks(ReparentDecksIn) returns (OpChangesWithCount);
|
||||||
rpc RenameDeck(RenameDeckIn) returns (OpChanges);
|
rpc RenameDeck(RenameDeckIn) returns (OpChanges);
|
||||||
|
rpc GetOrCreateFilteredDeck(DeckID) returns (FilteredDeckForUpdate);
|
||||||
|
rpc AddOrUpdateFilteredDeck(FilteredDeckForUpdate) returns (OpChangesWithID);
|
||||||
}
|
}
|
||||||
|
|
||||||
service NotesService {
|
service NotesService {
|
||||||
@ -565,6 +567,7 @@ message BackendError {
|
|||||||
Empty not_found_error = 11;
|
Empty not_found_error = 11;
|
||||||
Empty exists = 12;
|
Empty exists = 12;
|
||||||
Empty deck_is_filtered = 13;
|
Empty deck_is_filtered = 13;
|
||||||
|
Empty filtered_deck_empty = 14;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1525,6 +1528,12 @@ message RenameDeckIn {
|
|||||||
string new_name = 2;
|
string new_name = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message FilteredDeckForUpdate {
|
||||||
|
int64 deck_id = 1;
|
||||||
|
string name = 2;
|
||||||
|
FilteredDeck settings = 3;
|
||||||
|
}
|
||||||
|
|
||||||
message SetFlagIn {
|
message SetFlagIn {
|
||||||
repeated int64 card_ids = 1;
|
repeated int64 card_ids = 1;
|
||||||
uint32 flag = 2;
|
uint32 flag = 2;
|
||||||
|
@ -138,6 +138,17 @@ impl DecksService for Backend {
|
|||||||
self.with_col(|col| col.rename_deck(input.deck_id.into(), &input.new_name))
|
self.with_col(|col| col.rename_deck(input.deck_id.into(), &input.new_name))
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_or_create_filtered_deck(&self, _input: pb::DeckId) -> Result<pb::FilteredDeckForUpdate> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_or_update_filtered_deck(
|
||||||
|
&self,
|
||||||
|
_input: pb::FilteredDeckForUpdate,
|
||||||
|
) -> Result<pb::OpChangesWithId> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<pb::DeckId> for DeckID {
|
impl From<pb::DeckId> for DeckID {
|
||||||
|
@ -31,6 +31,7 @@ pub(super) fn anki_error_to_proto_error(err: AnkiError, i18n: &I18n) -> pb::Back
|
|||||||
AnkiError::SearchError(_) => V::InvalidInput(pb::Empty {}),
|
AnkiError::SearchError(_) => V::InvalidInput(pb::Empty {}),
|
||||||
AnkiError::TemplateSaveError { .. } => V::TemplateParse(pb::Empty {}),
|
AnkiError::TemplateSaveError { .. } => V::TemplateParse(pb::Empty {}),
|
||||||
AnkiError::ParseNumError => V::InvalidInput(pb::Empty {}),
|
AnkiError::ParseNumError => V::InvalidInput(pb::Empty {}),
|
||||||
|
AnkiError::FilteredDeckEmpty => V::FilteredDeckEmpty(pb::Empty {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
pb::BackendError {
|
pb::BackendError {
|
||||||
|
@ -95,11 +95,11 @@ impl SchedulingService for Backend {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn empty_filtered_deck(&self, input: pb::DeckId) -> Result<pb::Empty> {
|
fn empty_filtered_deck(&self, input: pb::DeckId) -> Result<pb::OpChanges> {
|
||||||
self.with_col(|col| col.empty_filtered_deck(input.did.into()).map(Into::into))
|
self.with_col(|col| col.empty_filtered_deck(input.did.into()).map(Into::into))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rebuild_filtered_deck(&self, input: pb::DeckId) -> Result<pb::UInt32> {
|
fn rebuild_filtered_deck(&self, input: pb::DeckId) -> Result<pb::OpChangesWithCount> {
|
||||||
self.with_col(|col| col.rebuild_filtered_deck(input.did.into()).map(Into::into))
|
self.with_col(|col| col.rebuild_filtered_deck(input.did.into()).map(Into::into))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,6 +101,14 @@ impl Deck {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn filtered(&self) -> Result<&FilteredDeck> {
|
||||||
|
if let DeckKind::Filtered(filtered) = &self.kind {
|
||||||
|
Ok(filtered)
|
||||||
|
} else {
|
||||||
|
Err(AnkiError::invalid_input("deck not filtered"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) fn filtered_mut(&mut self) -> Result<&mut FilteredDeck> {
|
pub(crate) fn filtered_mut(&mut self) -> Result<&mut FilteredDeck> {
|
||||||
if let DeckKind::Filtered(filtered) = &mut self.kind {
|
if let DeckKind::Filtered(filtered) = &mut self.kind {
|
||||||
@ -283,13 +291,14 @@ impl Collection {
|
|||||||
return Err(AnkiError::invalid_input("deck to add must have id 0"));
|
return Err(AnkiError::invalid_input("deck to add must have id 0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.transact(Op::AddDeck, |col| {
|
self.transact(Op::AddDeck, |col| col.add_deck_inner(deck, col.usn()?))
|
||||||
let usn = col.usn()?;
|
}
|
||||||
col.prepare_deck_for_update(deck, usn)?;
|
|
||||||
|
pub(crate) fn add_deck_inner(&mut self, deck: &mut Deck, usn: Usn) -> Result<()> {
|
||||||
|
self.prepare_deck_for_update(deck, usn)?;
|
||||||
deck.set_modified(usn);
|
deck.set_modified(usn);
|
||||||
col.match_or_create_parents(deck, usn)?;
|
self.match_or_create_parents(deck, usn)?;
|
||||||
col.add_deck_undoable(deck)
|
self.add_deck_undoable(deck)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_deck(&mut self, deck: &mut Deck) -> Result<OpOutput<()>> {
|
pub fn update_deck(&mut self, deck: &mut Deck) -> Result<OpOutput<()>> {
|
||||||
@ -308,7 +317,12 @@ impl Collection {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_deck_inner(&mut self, deck: &mut Deck, original: Deck, usn: Usn) -> Result<()> {
|
pub(crate) fn update_deck_inner(
|
||||||
|
&mut self,
|
||||||
|
deck: &mut Deck,
|
||||||
|
original: Deck,
|
||||||
|
usn: Usn,
|
||||||
|
) -> Result<()> {
|
||||||
self.prepare_deck_for_update(deck, usn)?;
|
self.prepare_deck_for_update(deck, usn)?;
|
||||||
deck.set_modified(usn);
|
deck.set_modified(usn);
|
||||||
let name_changed = original.name != deck.name;
|
let name_changed = original.name != deck.name;
|
||||||
|
@ -65,6 +65,9 @@ pub enum AnkiError {
|
|||||||
|
|
||||||
#[fail(display = "Invalid search.")]
|
#[fail(display = "Invalid search.")]
|
||||||
SearchError(SearchErrorKind),
|
SearchError(SearchErrorKind),
|
||||||
|
|
||||||
|
#[fail(display = "Provided search(es) did not match any cards.")]
|
||||||
|
FilteredDeckEmpty,
|
||||||
}
|
}
|
||||||
|
|
||||||
// error helpers
|
// error helpers
|
||||||
|
@ -8,9 +8,12 @@ pub enum Op {
|
|||||||
AddDeck,
|
AddDeck,
|
||||||
AddNote,
|
AddNote,
|
||||||
AnswerCard,
|
AnswerCard,
|
||||||
|
BuildFilteredDeck,
|
||||||
Bury,
|
Bury,
|
||||||
ClearUnusedTags,
|
ClearUnusedTags,
|
||||||
|
EmptyFilteredDeck,
|
||||||
FindAndReplace,
|
FindAndReplace,
|
||||||
|
RebuildFilteredDeck,
|
||||||
RemoveDeck,
|
RemoveDeck,
|
||||||
RemoveNote,
|
RemoveNote,
|
||||||
RemoveTag,
|
RemoveTag,
|
||||||
@ -60,6 +63,9 @@ impl Op {
|
|||||||
Op::RemoveTag => TR::ActionsRemoveTag,
|
Op::RemoveTag => TR::ActionsRemoveTag,
|
||||||
Op::ReparentTag => TR::ActionsRenameTag,
|
Op::ReparentTag => TR::ActionsRenameTag,
|
||||||
Op::ReparentDeck => TR::ActionsRenameDeck,
|
Op::ReparentDeck => TR::ActionsRenameDeck,
|
||||||
|
Op::BuildFilteredDeck => TR::UndoBuildFilteredDeck,
|
||||||
|
Op::RebuildFilteredDeck => TR::UndoBuildFilteredDeck,
|
||||||
|
Op::EmptyFilteredDeck => TR::StudyingEmpty,
|
||||||
};
|
};
|
||||||
|
|
||||||
i18n.tr(key).to_string()
|
i18n.tr(key).to_string()
|
||||||
|
@ -76,7 +76,7 @@ mod test {
|
|||||||
let mut filtered_deck = Deck::new_filtered();
|
let mut filtered_deck = Deck::new_filtered();
|
||||||
filtered_deck.filtered_mut()?.reschedule = false;
|
filtered_deck.filtered_mut()?.reschedule = false;
|
||||||
col.add_or_update_deck(&mut filtered_deck)?;
|
col.add_or_update_deck(&mut filtered_deck)?;
|
||||||
assert_eq!(col.rebuild_filtered_deck(filtered_deck.id)?, 1);
|
assert_eq!(col.rebuild_filtered_deck(filtered_deck.id)?.output, 1);
|
||||||
|
|
||||||
let next = col.get_next_card_states(c.id)?;
|
let next = col.get_next_card_states(c.id)?;
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
|
@ -1,248 +0,0 @@
|
|||||||
// Copyright: Ankitects Pty Ltd and contributors
|
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
||||||
|
|
||||||
use crate::decks::{FilteredDeck, FilteredSearchOrder, FilteredSearchTerm};
|
|
||||||
use crate::{
|
|
||||||
card::{CardQueue, CardType},
|
|
||||||
config::SchedulerVersion,
|
|
||||||
prelude::*,
|
|
||||||
search::SortMode,
|
|
||||||
};
|
|
||||||
|
|
||||||
impl Card {
|
|
||||||
pub(crate) fn restore_queue_from_type(&mut self) {
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 original_or_current_deck_id(&self) -> DeckID {
|
|
||||||
if self.original_deck_id.0 > 0 {
|
|
||||||
self.original_deck_id
|
|
||||||
} else {
|
|
||||||
self.deck_id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.restore_queue_from_type();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.original_due = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct DeckFilterContext<'a> {
|
|
||||||
pub target_deck: DeckID,
|
|
||||||
pub config: &'a FilteredDeck,
|
|
||||||
pub scheduler: SchedulerVersion,
|
|
||||||
pub usn: Usn,
|
|
||||||
pub today: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Collection {
|
|
||||||
pub fn empty_filtered_deck(&mut self, did: DeckID) -> Result<()> {
|
|
||||||
self.transact_no_undo(|col| col.return_all_cards_in_filtered_deck(did))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn return_all_cards_in_filtered_deck(&mut self, did: DeckID) -> Result<()> {
|
|
||||||
let cids = self.storage.all_cards_in_single_deck(did)?;
|
|
||||||
self.return_cards_to_home_deck(&cids)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unlike the old Python code, this also marks the cards as modified.
|
|
||||||
fn return_cards_to_home_deck(&mut self, cids: &[CardID]) -> Result<()> {
|
|
||||||
let sched = self.scheduler_version();
|
|
||||||
let usn = self.usn()?;
|
|
||||||
for cid in cids {
|
|
||||||
if let Some(mut card) = self.storage.get_card(*cid)? {
|
|
||||||
let original = card.clone();
|
|
||||||
card.remove_from_filtered_deck_restoring_queue(sched);
|
|
||||||
self.update_card_inner(&mut card, original, usn)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unlike the old Python code, this also marks the cards as modified.
|
|
||||||
pub fn rebuild_filtered_deck(&mut self, did: DeckID) -> Result<u32> {
|
|
||||||
let deck = self.get_deck(did)?.ok_or(AnkiError::NotFound)?;
|
|
||||||
let config = if let DeckKind::Filtered(kind) = &deck.kind {
|
|
||||||
kind
|
|
||||||
} else {
|
|
||||||
return Err(AnkiError::invalid_input("not filtered"));
|
|
||||||
};
|
|
||||||
let ctx = DeckFilterContext {
|
|
||||||
target_deck: did,
|
|
||||||
config,
|
|
||||||
scheduler: self.scheduler_version(),
|
|
||||||
usn: self.usn()?,
|
|
||||||
today: self.timing_today()?.days_elapsed,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.transact_no_undo(|col| {
|
|
||||||
col.return_all_cards_in_filtered_deck(did)?;
|
|
||||||
col.build_filtered_deck(ctx)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_filtered_deck(&mut self, ctx: DeckFilterContext) -> Result<u32> {
|
|
||||||
let start = -100_000;
|
|
||||||
let mut position = start;
|
|
||||||
for term in &ctx.config.search_terms {
|
|
||||||
position = self.move_cards_matching_term(&ctx, term, position)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((position - start) as u32)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Move matching cards into filtered deck.
|
|
||||||
/// Returns the new starting position.
|
|
||||||
fn move_cards_matching_term(
|
|
||||||
&mut self,
|
|
||||||
ctx: &DeckFilterContext,
|
|
||||||
term: &FilteredSearchTerm,
|
|
||||||
mut position: i32,
|
|
||||||
) -> Result<i32> {
|
|
||||||
let search = format!(
|
|
||||||
"{} -is:suspended -is:buried -deck:filtered {}",
|
|
||||||
if term.search.trim().is_empty() {
|
|
||||||
"".to_string()
|
|
||||||
} else {
|
|
||||||
format!("({})", term.search)
|
|
||||||
},
|
|
||||||
if ctx.scheduler == SchedulerVersion::V1 {
|
|
||||||
"-is:learn"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
);
|
|
||||||
let order = order_and_limit_for_search(term, ctx.today);
|
|
||||||
|
|
||||||
self.search_cards_into_table(&search, SortMode::Custom(order))?;
|
|
||||||
for mut card in self.storage.all_searched_cards_in_search_order()? {
|
|
||||||
let original = card.clone();
|
|
||||||
card.move_into_filtered_deck(ctx, position);
|
|
||||||
self.update_card_inner(&mut card, original, ctx.usn)?;
|
|
||||||
position += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn order_and_limit_for_search(term: &FilteredSearchTerm, today: u32) -> String {
|
|
||||||
let temp_string;
|
|
||||||
let order = match term.order() {
|
|
||||||
FilteredSearchOrder::OldestFirst => "(select max(id) from revlog where cid=c.id)",
|
|
||||||
FilteredSearchOrder::Random => "random()",
|
|
||||||
FilteredSearchOrder::IntervalsAscending => "ivl",
|
|
||||||
FilteredSearchOrder::IntervalsDescending => "ivl desc",
|
|
||||||
FilteredSearchOrder::Lapses => "lapses desc",
|
|
||||||
FilteredSearchOrder::Added => "n.id",
|
|
||||||
FilteredSearchOrder::ReverseAdded => "n.id desc",
|
|
||||||
FilteredSearchOrder::Due => "c.due, c.ord",
|
|
||||||
FilteredSearchOrder::DuePriority => {
|
|
||||||
temp_string = format!(
|
|
||||||
"
|
|
||||||
(case when queue={rev_queue} and due <= {today}
|
|
||||||
then (ivl / cast({today}-due+0.001 as real)) else 100000+due end)",
|
|
||||||
rev_queue = CardQueue::Review as i8,
|
|
||||||
today = today
|
|
||||||
);
|
|
||||||
&temp_string
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
format!("{} limit {}", order, term.limit)
|
|
||||||
}
|
|
123
rslib/src/scheduler/filtered/card.rs
Normal file
123
rslib/src/scheduler/filtered/card.rs
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
card::{CardQueue, CardType},
|
||||||
|
config::SchedulerVersion,
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::DeckFilterContext;
|
||||||
|
|
||||||
|
impl Card {
|
||||||
|
pub(crate) fn restore_queue_from_type(&mut self) {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 original_or_current_deck_id(&self) -> DeckID {
|
||||||
|
if self.original_deck_id.0 > 0 {
|
||||||
|
self.original_deck_id
|
||||||
|
} else {
|
||||||
|
self.deck_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.restore_queue_from_type();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.original_due = 0;
|
||||||
|
}
|
||||||
|
}
|
218
rslib/src/scheduler/filtered/mod.rs
Normal file
218
rslib/src/scheduler/filtered/mod.rs
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
mod card;
|
||||||
|
|
||||||
|
use std::convert::{TryFrom, TryInto};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::ConfigKey,
|
||||||
|
decks::{human_deck_name_to_native, FilteredDeck, FilteredSearchTerm},
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
config::SchedulerVersion, prelude::*, search::SortMode,
|
||||||
|
storage::card::filtered::order_and_limit_for_search,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Contains the parts of a filtered deck required for modifying its settings in
|
||||||
|
/// the UI.
|
||||||
|
pub struct FilteredDeckForUpdate {
|
||||||
|
pub id: DeckID,
|
||||||
|
pub human_name: String,
|
||||||
|
pub settings: FilteredDeck,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct DeckFilterContext<'a> {
|
||||||
|
pub target_deck: DeckID,
|
||||||
|
pub config: &'a FilteredDeck,
|
||||||
|
pub scheduler: SchedulerVersion,
|
||||||
|
pub usn: Usn,
|
||||||
|
pub today: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Collection {
|
||||||
|
/// Get an existing filtered deck, or create a new one if `deck_id` is 0. The new deck
|
||||||
|
/// will not be added to the DB.
|
||||||
|
pub fn get_or_create_filtered_deck(&self, deck_id: DeckID) -> Result<FilteredDeckForUpdate> {
|
||||||
|
let deck = if deck_id.0 == 0 {
|
||||||
|
Deck {
|
||||||
|
name: self.get_next_filtered_deck_name()?,
|
||||||
|
..Deck::new_filtered()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.storage.get_deck(deck_id)?.ok_or(AnkiError::NotFound)?
|
||||||
|
};
|
||||||
|
|
||||||
|
deck.try_into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If the provided `deck_id` is 0, add provided deck to the DB, and rebuild it. If the
|
||||||
|
/// searches are invalid or do not match anything, adding is aborted.
|
||||||
|
/// If an existing deck is provided, it will be updated. Invalid searches or an empty
|
||||||
|
/// match will abort the update.
|
||||||
|
/// Returns the deck_id, which will have changed if the id was 0.
|
||||||
|
pub fn add_or_update_filtered_deck(
|
||||||
|
&mut self,
|
||||||
|
deck: FilteredDeckForUpdate,
|
||||||
|
) -> Result<OpOutput<DeckID>> {
|
||||||
|
self.transact(Op::BuildFilteredDeck, |col| {
|
||||||
|
col.add_or_update_filtered_deck_inner(deck)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn empty_filtered_deck(&mut self, did: DeckID) -> Result<OpOutput<()>> {
|
||||||
|
self.transact(Op::EmptyFilteredDeck, |col| {
|
||||||
|
col.return_all_cards_in_filtered_deck(did)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlike the old Python code, this also marks the cards as modified.
|
||||||
|
pub fn rebuild_filtered_deck(&mut self, did: DeckID) -> Result<OpOutput<usize>> {
|
||||||
|
self.transact(Op::RebuildFilteredDeck, |col| {
|
||||||
|
let deck = col.get_deck(did)?.ok_or(AnkiError::NotFound)?;
|
||||||
|
col.rebuild_filtered_deck_inner(&deck, col.usn()?)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Collection {
|
||||||
|
pub(crate) fn return_all_cards_in_filtered_deck(&mut self, did: DeckID) -> Result<()> {
|
||||||
|
let cids = self.storage.all_cards_in_single_deck(did)?;
|
||||||
|
self.return_cards_to_home_deck(&cids)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlike the old Python code, this also marks the cards as modified.
|
||||||
|
fn return_cards_to_home_deck(&mut self, cids: &[CardID]) -> Result<()> {
|
||||||
|
let sched = self.scheduler_version();
|
||||||
|
let usn = self.usn()?;
|
||||||
|
for cid in cids {
|
||||||
|
if let Some(mut card) = self.storage.get_card(*cid)? {
|
||||||
|
let original = card.clone();
|
||||||
|
card.remove_from_filtered_deck_restoring_queue(sched);
|
||||||
|
self.update_card_inner(&mut card, original, usn)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_filtered_deck(&mut self, ctx: DeckFilterContext) -> Result<usize> {
|
||||||
|
let start = -100_000;
|
||||||
|
let mut position = start;
|
||||||
|
for term in &ctx.config.search_terms {
|
||||||
|
position = self.move_cards_matching_term(&ctx, term, position)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((position - start) as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move matching cards into filtered deck.
|
||||||
|
/// Returns the new starting position.
|
||||||
|
fn move_cards_matching_term(
|
||||||
|
&mut self,
|
||||||
|
ctx: &DeckFilterContext,
|
||||||
|
term: &FilteredSearchTerm,
|
||||||
|
mut position: i32,
|
||||||
|
) -> Result<i32> {
|
||||||
|
let search = format!(
|
||||||
|
"{} -is:suspended -is:buried -deck:filtered {}",
|
||||||
|
if term.search.trim().is_empty() {
|
||||||
|
"".to_string()
|
||||||
|
} else {
|
||||||
|
format!("({})", term.search)
|
||||||
|
},
|
||||||
|
if ctx.scheduler == SchedulerVersion::V1 {
|
||||||
|
"-is:learn"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let order = order_and_limit_for_search(term, ctx.today);
|
||||||
|
|
||||||
|
self.search_cards_into_table(&search, SortMode::Custom(order))?;
|
||||||
|
for mut card in self.storage.all_searched_cards_in_search_order()? {
|
||||||
|
let original = card.clone();
|
||||||
|
card.move_into_filtered_deck(ctx, position);
|
||||||
|
self.update_card_inner(&mut card, original, ctx.usn)?;
|
||||||
|
position += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_next_filtered_deck_name(&self) -> Result<String> {
|
||||||
|
// fixme:
|
||||||
|
Ok("Filtered Deck 1".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_or_update_filtered_deck_inner(
|
||||||
|
&mut self,
|
||||||
|
update: FilteredDeckForUpdate,
|
||||||
|
) -> Result<DeckID> {
|
||||||
|
let usn = self.usn()?;
|
||||||
|
|
||||||
|
// add or update the deck
|
||||||
|
let mut deck: Deck;
|
||||||
|
if update.id.0 == 0 {
|
||||||
|
deck = Deck::new_filtered();
|
||||||
|
apply_update_to_filtered_deck(&mut deck, update);
|
||||||
|
self.add_deck_inner(&mut deck, usn)?;
|
||||||
|
} else {
|
||||||
|
let original = self
|
||||||
|
.storage
|
||||||
|
.get_deck(update.id)?
|
||||||
|
.ok_or(AnkiError::NotFound)?;
|
||||||
|
deck = original.clone();
|
||||||
|
apply_update_to_filtered_deck(&mut deck, update);
|
||||||
|
self.update_deck_inner(&mut deck, original, usn)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// rebuild it
|
||||||
|
let count = self.rebuild_filtered_deck_inner(&deck, usn)?;
|
||||||
|
|
||||||
|
// if it failed to match any cards, we revert the changes
|
||||||
|
if count == 0 {
|
||||||
|
Err(AnkiError::FilteredDeckEmpty)
|
||||||
|
} else {
|
||||||
|
// update current deck and return id
|
||||||
|
self.set_config(ConfigKey::CurrentDeckID, &deck.id)?;
|
||||||
|
Ok(deck.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rebuild_filtered_deck_inner(&mut self, deck: &Deck, usn: Usn) -> Result<usize> {
|
||||||
|
let config = deck.filtered()?;
|
||||||
|
let ctx = DeckFilterContext {
|
||||||
|
target_deck: deck.id,
|
||||||
|
config,
|
||||||
|
scheduler: self.scheduler_version(),
|
||||||
|
usn,
|
||||||
|
today: self.timing_today()?.days_elapsed,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.return_all_cards_in_filtered_deck(deck.id)?;
|
||||||
|
self.build_filtered_deck(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Deck> for FilteredDeckForUpdate {
|
||||||
|
type Error = AnkiError;
|
||||||
|
|
||||||
|
fn try_from(value: Deck) -> Result<Self, Self::Error> {
|
||||||
|
let human_name = value.human_name();
|
||||||
|
if let DeckKind::Filtered(filtered) = value.kind {
|
||||||
|
Ok(FilteredDeckForUpdate {
|
||||||
|
id: value.id,
|
||||||
|
human_name,
|
||||||
|
settings: filtered,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(AnkiError::invalid_input("not filtered"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_update_to_filtered_deck(deck: &mut Deck, update: FilteredDeckForUpdate) {
|
||||||
|
deck.id = update.id;
|
||||||
|
deck.name = human_deck_name_to_native(&update.human_name);
|
||||||
|
deck.kind = DeckKind::Filtered(update.settings);
|
||||||
|
}
|
33
rslib/src/storage/card/filtered.rs
Normal file
33
rslib/src/storage/card/filtered.rs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
card::CardQueue,
|
||||||
|
decks::{FilteredSearchOrder, FilteredSearchTerm},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) fn order_and_limit_for_search(term: &FilteredSearchTerm, today: u32) -> String {
|
||||||
|
let temp_string;
|
||||||
|
let order = match term.order() {
|
||||||
|
FilteredSearchOrder::OldestFirst => "(select max(id) from revlog where cid=c.id)",
|
||||||
|
FilteredSearchOrder::Random => "random()",
|
||||||
|
FilteredSearchOrder::IntervalsAscending => "ivl",
|
||||||
|
FilteredSearchOrder::IntervalsDescending => "ivl desc",
|
||||||
|
FilteredSearchOrder::Lapses => "lapses desc",
|
||||||
|
FilteredSearchOrder::Added => "n.id",
|
||||||
|
FilteredSearchOrder::ReverseAdded => "n.id desc",
|
||||||
|
FilteredSearchOrder::Due => "c.due, c.ord",
|
||||||
|
FilteredSearchOrder::DuePriority => {
|
||||||
|
temp_string = format!(
|
||||||
|
"
|
||||||
|
(case when queue={rev_queue} and due <= {today}
|
||||||
|
then (ivl / cast({today}-due+0.001 as real)) else 100000+due end)",
|
||||||
|
rev_queue = CardQueue::Review as i8,
|
||||||
|
today = today
|
||||||
|
);
|
||||||
|
&temp_string
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
format!("{} limit {}", order, term.limit)
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
// 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
|
||||||
|
|
||||||
|
pub(crate) mod filtered;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
card::{Card, CardID, CardQueue, CardType},
|
card::{Card, CardID, CardQueue, CardType},
|
||||||
deckconf::DeckConfID,
|
deckconf::DeckConfID,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// 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
|
||||||
|
|
||||||
mod card;
|
pub(crate) mod card;
|
||||||
mod config;
|
mod config;
|
||||||
mod deck;
|
mod deck;
|
||||||
mod deckconf;
|
mod deckconf;
|
||||||
|
Loading…
Reference in New Issue
Block a user