Remove v1/v2 support from the backend (#2727)
* Remove v1/v2 support from deck list * Remove v1/v2 support from most routines and show error * Remove scheduler_version from preferences * Fix formatting * Remove v1/v2 conditionals from Python code * Fix legacy importer * Remove legacy hooks * Add missing scheduler checks * Remove V2 logic from deck options screen * Remove the review_did_undo hook * Restore ability to open old options with shift (dae)
This commit is contained in:
parent
e1e0f2e1bd
commit
5cde4b6941
@ -1 +1 @@
|
||||
Subproject commit efd2e6edaf1e2b8e1d52c45ebd09d67ac035050f
|
||||
Subproject commit 3d820846d847f8ed866971e3a4304c715325d01b
|
@ -1 +1 @@
|
||||
Subproject commit 41663c8bd0f6386ee98de03ca2c3e668c4f6194d
|
||||
Subproject commit ef4f4ffee68a3d3ebb2458b8b55bfdffa9a21c4b
|
@ -44,6 +44,7 @@ message BackendError {
|
||||
ANKIDROID_PANIC_ERROR = 19;
|
||||
// Originated from and usually specific to the OS.
|
||||
OS_ERROR = 20;
|
||||
SCHEDULER_UPGRADE_REQUIRED = 21;
|
||||
}
|
||||
|
||||
// error description, usually localized, suitable for displaying to the user
|
||||
|
@ -100,9 +100,6 @@ message Preferences {
|
||||
NEW_FIRST = 2;
|
||||
}
|
||||
|
||||
// read only; 1-3
|
||||
uint32 scheduler_version = 1;
|
||||
|
||||
uint32 rollover = 2;
|
||||
uint32 learn_ahead_secs = 3;
|
||||
NewReviewMix new_review_mix = 4;
|
||||
|
@ -180,7 +180,6 @@ message DeckConfigsForUpdate {
|
||||
CurrentDeck current_deck = 2;
|
||||
DeckConfig defaults = 3;
|
||||
bool schema_modified = 4;
|
||||
bool v3_scheduler = 5;
|
||||
// only applies to v3 scheduler
|
||||
string card_state_customizer = 6;
|
||||
// only applies to v3 scheduler
|
||||
|
@ -33,6 +33,7 @@ from .errors import (
|
||||
InvalidInput,
|
||||
NetworkError,
|
||||
NotFoundError,
|
||||
SchedulerUpgradeRequired,
|
||||
SearchError,
|
||||
SyncError,
|
||||
SyncErrorKind,
|
||||
@ -240,6 +241,9 @@ def backend_exception_to_pylib(err: backend_pb2.BackendError) -> Exception:
|
||||
elif val == kind.CUSTOM_STUDY_ERROR:
|
||||
return CustomStudyError(err.message, help_page, context, backtrace)
|
||||
|
||||
elif val == kind.SCHEDULER_UPGRADE_REQUIRED:
|
||||
return SchedulerUpgradeRequired(err.message, help_page, context, backtrace)
|
||||
|
||||
else:
|
||||
# sadly we can't do exhaustiveness checking on protobuf enums
|
||||
# assert_exhaustive(val)
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Generator, Iterable, Literal, Sequence, Union, cast
|
||||
from typing import Any, Generator, Iterable, Literal, Optional, Sequence, Union, cast
|
||||
|
||||
from anki import (
|
||||
ankiweb_pb2,
|
||||
@ -56,13 +56,12 @@ MediaSyncStatus = sync_pb2.MediaSyncStatusResponse
|
||||
FsrsItem = scheduler_pb2.FsrsItem
|
||||
FsrsReview = scheduler_pb2.FsrsReview
|
||||
|
||||
import copy
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
import weakref
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
|
||||
import anki.latex
|
||||
from anki import hooks
|
||||
@ -98,18 +97,12 @@ anki.latex.setup_hook()
|
||||
SearchJoiner = Literal["AND", "OR"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class LegacyReviewUndo:
|
||||
card: Card
|
||||
was_leech: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class LegacyCheckpoint:
|
||||
name: str
|
||||
|
||||
|
||||
LegacyUndoResult = Union[None, LegacyCheckpoint, LegacyReviewUndo]
|
||||
LegacyUndoResult = Optional[LegacyCheckpoint]
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -1075,9 +1068,7 @@ class Collection(DeprecatedNamesMixin):
|
||||
if not self._undo:
|
||||
return UndoStatus()
|
||||
|
||||
if isinstance(self._undo, _ReviewsUndo):
|
||||
return UndoStatus(undo=self.tr.scheduling_review())
|
||||
elif isinstance(self._undo, LegacyCheckpoint):
|
||||
if isinstance(self._undo, LegacyCheckpoint):
|
||||
return UndoStatus(undo=self._undo.name)
|
||||
else:
|
||||
assert_exhaustive(self._undo)
|
||||
@ -1131,9 +1122,7 @@ class Collection(DeprecatedNamesMixin):
|
||||
|
||||
def undo_legacy(self) -> LegacyUndoResult:
|
||||
"Returns None if the legacy undo queue is empty."
|
||||
if isinstance(self._undo, _ReviewsUndo):
|
||||
return self._undo_review()
|
||||
elif isinstance(self._undo, LegacyCheckpoint):
|
||||
if isinstance(self._undo, LegacyCheckpoint):
|
||||
return self._undo_checkpoint()
|
||||
elif self._undo is None:
|
||||
return None
|
||||
@ -1158,15 +1147,6 @@ class Collection(DeprecatedNamesMixin):
|
||||
else:
|
||||
return None
|
||||
|
||||
def save_card_review_undo_info(self, card: Card) -> None:
|
||||
"Used by V1 and V2 schedulers to record state prior to review."
|
||||
if not isinstance(self._undo, _ReviewsUndo):
|
||||
self._undo = _ReviewsUndo()
|
||||
|
||||
was_leech = card.note().has_tag("leech")
|
||||
entry = LegacyReviewUndo(card=copy.copy(card), was_leech=was_leech)
|
||||
self._undo.entries.append(entry)
|
||||
|
||||
def _have_outstanding_checkpoint(self) -> bool:
|
||||
self._check_backend_undo_status()
|
||||
return isinstance(self._undo, LegacyCheckpoint)
|
||||
@ -1184,59 +1164,8 @@ class Collection(DeprecatedNamesMixin):
|
||||
if name:
|
||||
self._undo = LegacyCheckpoint(name=name)
|
||||
else:
|
||||
# saving disables old checkpoint, but not review undo
|
||||
if not isinstance(self._undo, _ReviewsUndo):
|
||||
self.clear_python_undo()
|
||||
|
||||
def _undo_review(self) -> LegacyReviewUndo:
|
||||
"Undo a v1/v2 review."
|
||||
assert isinstance(self._undo, _ReviewsUndo)
|
||||
entry = self._undo.entries.pop()
|
||||
if not self._undo.entries:
|
||||
self.clear_python_undo()
|
||||
|
||||
card = entry.card
|
||||
|
||||
# remove leech tag if it didn't have it before
|
||||
if not entry.was_leech and card.note().has_tag("leech"):
|
||||
card.note().remove_tag("leech")
|
||||
card.note().flush()
|
||||
|
||||
# write old data
|
||||
card.flush()
|
||||
|
||||
# and delete revlog entry if not previewing
|
||||
conf = self.sched._cardConf(card)
|
||||
previewing = conf["dyn"] and not conf["resched"]
|
||||
if not previewing:
|
||||
last = self.db.scalar(
|
||||
"select id from revlog where cid = ? order by id desc limit 1",
|
||||
card.id,
|
||||
)
|
||||
self.db.execute("delete from revlog where id = ?", last)
|
||||
|
||||
# restore any siblings
|
||||
self.db.execute(
|
||||
"update cards set queue=type,mod=?,usn=? where queue=-2 and nid=?",
|
||||
int_time(),
|
||||
self.usn(),
|
||||
card.nid,
|
||||
)
|
||||
|
||||
# update daily counts
|
||||
idx = card.queue
|
||||
if card.queue in (QUEUE_TYPE_DAY_LEARN_RELEARN, QUEUE_TYPE_PREVIEW):
|
||||
idx = QUEUE_TYPE_LRN
|
||||
type = ("new", "lrn", "rev")[idx]
|
||||
self.sched._updateStats(card, type, -1)
|
||||
self.sched.reps -= 1
|
||||
self._startReps -= 1
|
||||
|
||||
# and refresh the queues
|
||||
self.sched.reset()
|
||||
|
||||
return entry
|
||||
|
||||
# DB maintenance
|
||||
##########################################################################
|
||||
|
||||
@ -1444,7 +1373,6 @@ class Collection(DeprecatedNamesMixin):
|
||||
|
||||
Collection.register_deprecated_aliases(
|
||||
clearUndo=Collection.clear_python_undo,
|
||||
markReview=Collection.save_card_review_undo_info,
|
||||
findReplace=Collection.find_and_replace,
|
||||
remCards=Collection.remove_cards_and_orphaned_notes,
|
||||
)
|
||||
@ -1453,13 +1381,7 @@ Collection.register_deprecated_aliases(
|
||||
# legacy name
|
||||
_Collection = Collection
|
||||
|
||||
|
||||
@dataclass
|
||||
class _ReviewsUndo:
|
||||
entries: list[LegacyReviewUndo] = field(default_factory=list)
|
||||
|
||||
|
||||
_UndoInfo = Union[_ReviewsUndo, LegacyCheckpoint, None]
|
||||
_UndoInfo = Union[LegacyCheckpoint, None]
|
||||
|
||||
|
||||
def pb_export_limit(limit: ExportLimit) -> import_export_pb2.ExportLimit:
|
||||
|
@ -119,6 +119,10 @@ class SearchError(BackendError):
|
||||
pass
|
||||
|
||||
|
||||
class SchedulerUpgradeRequired(BackendError):
|
||||
pass
|
||||
|
||||
|
||||
class AbortSchemaModification(AnkiException):
|
||||
pass
|
||||
|
||||
|
@ -197,9 +197,6 @@ class AnkiExporter(Exporter):
|
||||
return []
|
||||
|
||||
def exportInto(self, path: str) -> None:
|
||||
# sched info+v2 scheduler not compatible w/ older clients
|
||||
self._v2sched = self.col.sched_ver() != 1 and self.includeSched
|
||||
|
||||
# create a new collection at the target
|
||||
try:
|
||||
os.unlink(path)
|
||||
@ -352,13 +349,10 @@ class AnkiPackageExporter(AnkiExporter):
|
||||
# export into the anki2 file
|
||||
colfile = path.replace(".apkg", ".anki2")
|
||||
AnkiExporter.exportInto(self, colfile)
|
||||
if not self._v2sched:
|
||||
z.write(colfile, "collection.anki2")
|
||||
else:
|
||||
# prevent older clients from accessing
|
||||
# pylint: disable=unreachable
|
||||
self._addDummyCollection(z)
|
||||
z.write(colfile, "collection.anki21")
|
||||
# prevent older clients from accessing
|
||||
# pylint: disable=unreachable
|
||||
self._addDummyCollection(z)
|
||||
z.write(colfile, "collection.anki21")
|
||||
|
||||
# and media
|
||||
self.prepareMedia()
|
||||
|
@ -60,7 +60,7 @@ class Anki2Importer(Importer):
|
||||
self.dst = self.col
|
||||
self.src = Collection(self.file)
|
||||
|
||||
if not self._importing_v2 and self.col.sched_ver() != 1:
|
||||
if not self._importing_v2:
|
||||
# any scheduling included?
|
||||
if self.src.db.scalar("select 1 from cards where queue != 0 limit 1"):
|
||||
self.source_needs_upgrade = True
|
||||
|
@ -5,7 +5,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import random
|
||||
import time
|
||||
@ -691,8 +690,7 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = {QUEUE
|
||||
[13, 3],
|
||||
[14, 4],
|
||||
]
|
||||
if self.col.sched_ver() != 1:
|
||||
ticks.insert(3, [4, 4])
|
||||
ticks.insert(3, [4, 4])
|
||||
txt = self._title(
|
||||
"Answer Buttons", "The number of times you have pressed each button."
|
||||
)
|
||||
@ -751,10 +749,7 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = {QUEUE
|
||||
lim = "where " + " and ".join(lims)
|
||||
else:
|
||||
lim = ""
|
||||
if self.col.sched_ver() == 1:
|
||||
ease4repl = "3"
|
||||
else:
|
||||
ease4repl = "ease"
|
||||
ease4repl = "ease"
|
||||
return self.col.db.all(
|
||||
f"""
|
||||
select (case
|
||||
@ -841,11 +836,7 @@ order by thetype, ease"""
|
||||
lim = self._revlogLimit()
|
||||
if lim:
|
||||
lim = " and " + lim
|
||||
if self.col.sched_ver() == 1:
|
||||
sd = datetime.datetime.fromtimestamp(self.col.crt)
|
||||
rolloverHour = sd.hour
|
||||
else:
|
||||
rolloverHour = self.col.conf.get("rollover", 4)
|
||||
rolloverHour = self.col.conf.get("rollover", 4)
|
||||
pd = self._periodDays()
|
||||
if pd:
|
||||
lim += " and id > %d" % ((self.col.sched.day_cutoff - (86400 * pd)) * 1000)
|
||||
|
@ -395,13 +395,7 @@ def test_reviews():
|
||||
c = copy.copy(cardcopy)
|
||||
c.lapses = 7
|
||||
c.flush()
|
||||
# setup hook
|
||||
hooked = []
|
||||
|
||||
def onLeech(card):
|
||||
hooked.append(1)
|
||||
|
||||
hooks.card_did_leech.append(onLeech)
|
||||
col.sched.answerCard(c, 1)
|
||||
assert c.queue == QUEUE_TYPE_SUSPENDED
|
||||
c.load()
|
||||
|
@ -18,12 +18,6 @@ from hookslib import Hook, write_file
|
||||
######################################################################
|
||||
|
||||
hooks = [
|
||||
Hook(
|
||||
name="card_did_leech",
|
||||
args=["card: Card"],
|
||||
legacy_hook="leech",
|
||||
doc="Called by v1/v2 scheduler when a card is marked as a leech.",
|
||||
),
|
||||
Hook(name="card_odue_was_invalid"),
|
||||
Hook(name="schema_will_change", args=["proceed: bool"], return_type="bool"),
|
||||
Hook(
|
||||
@ -98,24 +92,6 @@ hooks = [
|
||||
],
|
||||
doc="Can modify the resulting text after rendering completes.",
|
||||
),
|
||||
Hook(
|
||||
name="schedv2_did_answer_review_card",
|
||||
args=["card: anki.cards.Card", "ease: int", "early: bool"],
|
||||
),
|
||||
Hook(
|
||||
name="scheduler_new_limit_for_single_deck",
|
||||
args=["count: int", "deck: anki.decks.DeckDict"],
|
||||
return_type="int",
|
||||
doc="""Allows changing the number of new card for this deck (without
|
||||
considering descendants).""",
|
||||
),
|
||||
Hook(
|
||||
name="scheduler_review_limit_for_single_deck",
|
||||
args=["count: int", "deck: anki.decks.DeckDict"],
|
||||
return_type="int",
|
||||
doc="""Allows changing the number of rev card for this deck (without
|
||||
considering descendants).""",
|
||||
),
|
||||
Hook(
|
||||
name="importing_importers",
|
||||
args=["importers: list[tuple[str, Any]]"],
|
||||
|
@ -729,7 +729,7 @@ class Browser(QMainWindow):
|
||||
|
||||
def createFilteredDeck(self) -> None:
|
||||
search = self.current_search()
|
||||
if self.mw.col.sched_ver() != 1 and KeyboardModifiersPressed().alt:
|
||||
if KeyboardModifiersPressed().alt:
|
||||
aqt.dialogs.open("FilteredDeckConfigDialog", self.mw, search_2=search)
|
||||
else:
|
||||
aqt.dialogs.open("FilteredDeckConfigDialog", self.mw, search=search)
|
||||
|
@ -219,9 +219,6 @@ class DeckConf(QDialog):
|
||||
f.revplim.setText(self.parentLimText("rev"))
|
||||
f.buryRev.setChecked(c.get("bury", True))
|
||||
f.hardFactor.setValue(int(c.get("hardFactor", 1.2) * 100))
|
||||
if self.mw.col.sched_ver() == 1:
|
||||
f.hardFactor.setVisible(False)
|
||||
f.hardFactorLabel.setVisible(False)
|
||||
# lapse
|
||||
c = self.conf["lapse"]
|
||||
f.lapSteps.setText(self.listToUser(c["delays"]))
|
||||
|
@ -106,7 +106,7 @@ def display_options_for_deck_id(deck_id: DeckId) -> None:
|
||||
|
||||
def display_options_for_deck(deck: DeckDict) -> None:
|
||||
if not deck["dyn"]:
|
||||
if KeyboardModifiersPressed().shift or aqt.mw.col.sched_ver() == 1:
|
||||
if KeyboardModifiersPressed().shift or not aqt.mw.col.v3_scheduler():
|
||||
deck_legacy = aqt.mw.col.decks.get(DeckId(deck["id"]))
|
||||
aqt.deckconf.DeckConf(aqt.mw, deck_legacy)
|
||||
else:
|
||||
|
@ -98,8 +98,6 @@ class FilteredDeckConfigDialog(QDialog):
|
||||
self.form.buttonBox.helpRequested, lambda: openHelp(HelpPage.FILTERED_DECK)
|
||||
)
|
||||
|
||||
if self.col.sched_ver() == 1:
|
||||
self.form.secondFilter.setVisible(False)
|
||||
restoreGeom(self, self.GEOMETRY_KEY)
|
||||
|
||||
def load_deck_and_show(self, deck: FilteredDeckForUpdate) -> None:
|
||||
@ -132,13 +130,8 @@ class FilteredDeckConfigDialog(QDialog):
|
||||
form.order.setCurrentIndex(term1.order)
|
||||
form.limit.setValue(term1.limit)
|
||||
|
||||
if self.col.sched_ver() == 1:
|
||||
if config.delays:
|
||||
form.steps.setText(self.listToUser(list(config.delays)))
|
||||
form.stepsOn.setChecked(True)
|
||||
else:
|
||||
form.steps.setVisible(False)
|
||||
form.stepsOn.setVisible(False)
|
||||
form.steps.setVisible(False)
|
||||
form.stepsOn.setVisible(False)
|
||||
|
||||
form.previewDelay.setValue(config.preview_delay)
|
||||
|
||||
@ -209,7 +202,6 @@ class FilteredDeckConfigDialog(QDialog):
|
||||
implicit_filters = (
|
||||
SearchNode(card_state=SearchNode.CARD_STATE_SUSPENDED),
|
||||
SearchNode(card_state=SearchNode.CARD_STATE_BURIED),
|
||||
*self._learning_search_node(),
|
||||
*self._filtered_search_node(),
|
||||
)
|
||||
manual_filter = self.col.group_searches(*manual_filters, joiner="OR")
|
||||
@ -226,21 +218,6 @@ class FilteredDeckConfigDialog(QDialog):
|
||||
return (self.form.search_2.text(),)
|
||||
return ()
|
||||
|
||||
def _learning_search_node(self) -> tuple[SearchNode, ...]:
|
||||
"""Return a search node that matches learning cards if the old scheduler is enabled.
|
||||
If it's a rebuild, exclude cards from this filtered deck as those will be reset.
|
||||
"""
|
||||
if self.col.sched_ver() == 1:
|
||||
if self.deck.id:
|
||||
return (
|
||||
self.col.group_searches(
|
||||
SearchNode(card_state=SearchNode.CARD_STATE_LEARN),
|
||||
SearchNode(negated=SearchNode(deck=self.deck.name)),
|
||||
),
|
||||
)
|
||||
return (SearchNode(card_state=SearchNode.CARD_STATE_LEARN),)
|
||||
return ()
|
||||
|
||||
def _filtered_search_node(self) -> tuple[SearchNode]:
|
||||
"""Return a search node that matches cards in filtered decks, if applicable excluding those
|
||||
in the deck being rebuild."""
|
||||
@ -254,9 +231,7 @@ class FilteredDeckConfigDialog(QDialog):
|
||||
return (SearchNode(deck="filtered"),)
|
||||
|
||||
def _onReschedToggled(self, _state: int) -> None:
|
||||
self.form.previewDelayWidget.setVisible(
|
||||
not self.form.resched.isChecked() and self.col.sched_ver() > 1
|
||||
)
|
||||
self.form.previewDelayWidget.setVisible(not self.form.resched.isChecked())
|
||||
|
||||
def _update_deck(self) -> bool:
|
||||
"""Update our stored deck with the details from the GUI.
|
||||
@ -269,11 +244,6 @@ class FilteredDeckConfigDialog(QDialog):
|
||||
config.reschedule = form.resched.isChecked()
|
||||
|
||||
del config.delays[:]
|
||||
if self.col.sched_ver() == 1 and form.stepsOn.isChecked():
|
||||
if (delays := self.userToList(form.steps)) is None:
|
||||
return False
|
||||
config.delays.extend(delays)
|
||||
|
||||
terms = [
|
||||
FilteredDeckConfig.SearchTerm(
|
||||
search=form.search.text(),
|
||||
|
@ -3,13 +3,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from anki.collection import (
|
||||
LegacyCheckpoint,
|
||||
LegacyReviewUndo,
|
||||
OpChanges,
|
||||
OpChangesAfterUndo,
|
||||
Preferences,
|
||||
)
|
||||
from anki.collection import LegacyCheckpoint, OpChanges, OpChangesAfterUndo, Preferences
|
||||
from anki.errors import UndoEmpty
|
||||
from anki.types import assert_exhaustive
|
||||
from aqt import gui_hooks
|
||||
@ -27,8 +21,7 @@ def undo(*, parent: QWidget) -> None:
|
||||
|
||||
def on_failure(exc: Exception) -> None:
|
||||
if isinstance(exc, UndoEmpty):
|
||||
# backend has no undo, but there may be a checkpoint
|
||||
# or v1/v2 review waiting
|
||||
# backend has no undo, but there may be a checkpoint waiting
|
||||
_legacy_undo(parent=parent)
|
||||
else:
|
||||
showWarning(str(exc), parent=parent)
|
||||
@ -53,9 +46,6 @@ def _legacy_undo(*, parent: QWidget) -> None:
|
||||
assert mw
|
||||
assert mw.col
|
||||
|
||||
reviewing = mw.state == "review"
|
||||
just_refresh_reviewer = False
|
||||
|
||||
result = mw.col.undo_legacy()
|
||||
|
||||
if result is None:
|
||||
@ -64,19 +54,6 @@ def _legacy_undo(*, parent: QWidget) -> None:
|
||||
mw.update_undo_actions()
|
||||
return
|
||||
|
||||
elif isinstance(result, LegacyReviewUndo):
|
||||
name = tr.scheduling_review()
|
||||
|
||||
if reviewing:
|
||||
# push the undone card to the top of the queue
|
||||
cid = result.card.id
|
||||
card = mw.col.get_card(cid)
|
||||
mw.reviewer.cardQueue.append(card)
|
||||
|
||||
gui_hooks.review_did_undo(cid)
|
||||
|
||||
just_refresh_reviewer = True
|
||||
|
||||
elif isinstance(result, LegacyCheckpoint):
|
||||
name = result.name
|
||||
|
||||
@ -84,11 +61,8 @@ def _legacy_undo(*, parent: QWidget) -> None:
|
||||
assert_exhaustive(result)
|
||||
assert False
|
||||
|
||||
if just_refresh_reviewer:
|
||||
mw.reviewer.nextCard()
|
||||
else:
|
||||
# full queue+gui reset required
|
||||
mw.reset()
|
||||
# full queue+gui reset required
|
||||
mw.reset()
|
||||
|
||||
tooltip(tr.undo_action_undone(action=name), parent=parent)
|
||||
gui_hooks.state_did_revert(name)
|
||||
|
@ -150,25 +150,24 @@ class Overview:
|
||||
|
||||
def on_unbury(self) -> None:
|
||||
mode = UnburyDeck.Mode.ALL
|
||||
if self.mw.col.sched_ver() != 1:
|
||||
info = self.mw.col.sched.congratulations_info()
|
||||
if info.have_sched_buried and info.have_user_buried:
|
||||
opts = [
|
||||
tr.studying_manually_buried_cards(),
|
||||
tr.studying_buried_siblings(),
|
||||
tr.studying_all_buried_cards(),
|
||||
tr.actions_cancel(),
|
||||
]
|
||||
info = self.mw.col.sched.congratulations_info()
|
||||
if info.have_sched_buried and info.have_user_buried:
|
||||
opts = [
|
||||
tr.studying_manually_buried_cards(),
|
||||
tr.studying_buried_siblings(),
|
||||
tr.studying_all_buried_cards(),
|
||||
tr.actions_cancel(),
|
||||
]
|
||||
|
||||
diag = askUserDialog(tr.studying_what_would_you_like_to_unbury(), opts)
|
||||
diag.setDefault(0)
|
||||
ret = diag.run()
|
||||
if ret == opts[0]:
|
||||
mode = UnburyDeck.Mode.USER_ONLY
|
||||
elif ret == opts[1]:
|
||||
mode = UnburyDeck.Mode.SCHED_ONLY
|
||||
elif ret == opts[3]:
|
||||
return
|
||||
diag = askUserDialog(tr.studying_what_would_you_like_to_unbury(), opts)
|
||||
diag.setDefault(0)
|
||||
ret = diag.run()
|
||||
if ret == opts[0]:
|
||||
mode = UnburyDeck.Mode.USER_ONLY
|
||||
elif ret == opts[1]:
|
||||
mode = UnburyDeck.Mode.SCHED_ONLY
|
||||
elif ret == opts[3]:
|
||||
return
|
||||
|
||||
unbury_deck(
|
||||
parent=self.mw, deck_id=self.mw.col.decks.get_current_id(), mode=mode
|
||||
|
@ -14,7 +14,6 @@ from typing import Any, Literal, Match, Sequence, cast
|
||||
import aqt
|
||||
import aqt.browser
|
||||
import aqt.operations
|
||||
from anki import hooks
|
||||
from anki.cards import Card, CardId
|
||||
from anki.collection import Config, OpChanges, OpChangesWithCount
|
||||
from anki.scheduler.base import ScheduleCardsAsNew
|
||||
@ -135,9 +134,7 @@ class Reviewer:
|
||||
self.mw = mw
|
||||
self.web = mw.web
|
||||
self.card: Card | None = None
|
||||
self.cardQueue: list[Card] = []
|
||||
self.previous_card: Card | None = None
|
||||
self.hadCardQueue = False
|
||||
self._answeredIds: list[CardId] = []
|
||||
self._recordedAudio: str | None = None
|
||||
self.typeCorrect: str = None # web init happens before this is set
|
||||
@ -150,7 +147,6 @@ class Reviewer:
|
||||
self._previous_card_info = PreviousReviewerCardInfo(self.mw)
|
||||
self._states_mutated = True
|
||||
self._reps: int = None
|
||||
hooks.card_did_leech.append(self.onLeech)
|
||||
|
||||
def show(self) -> None:
|
||||
if self.mw.col.sched_ver() == 1 or not self.mw.col.v3_scheduler():
|
||||
@ -229,11 +225,7 @@ class Reviewer:
|
||||
self.previous_card = self.card
|
||||
self.card = None
|
||||
self._v3 = None
|
||||
|
||||
if self.mw.col.sched.version < 3:
|
||||
self._get_next_v1_v2_card()
|
||||
else:
|
||||
self._get_next_v3_card()
|
||||
self._get_next_v3_card()
|
||||
|
||||
self._previous_card_info.set_card(self.previous_card)
|
||||
self._card_info.set_card(self.card)
|
||||
@ -247,21 +239,6 @@ class Reviewer:
|
||||
|
||||
self._showQuestion()
|
||||
|
||||
def _get_next_v1_v2_card(self) -> None:
|
||||
if self.cardQueue:
|
||||
# undone/edited cards to show
|
||||
card = self.cardQueue.pop()
|
||||
card.start_timer()
|
||||
self.hadCardQueue = True
|
||||
else:
|
||||
if self.hadCardQueue:
|
||||
# the undone/edited cards may be sitting in the regular queue;
|
||||
# need to reset
|
||||
self.mw.col.reset()
|
||||
self.hadCardQueue = False
|
||||
card = self.mw.col.sched.getCard()
|
||||
self.card = card
|
||||
|
||||
def _get_next_v3_card(self) -> None:
|
||||
assert isinstance(self.mw.col.sched, V3Scheduler)
|
||||
output = self.mw.col.sched.get_queued_cards()
|
||||
@ -271,22 +248,17 @@ class Reviewer:
|
||||
self.card = Card(self.mw.col, backend_card=self._v3.top_card().card)
|
||||
self.card.start_timer()
|
||||
|
||||
def get_scheduling_states(self) -> SchedulingStates | None:
|
||||
if v3 := self._v3:
|
||||
return v3.states
|
||||
return None
|
||||
def get_scheduling_states(self) -> SchedulingStates:
|
||||
return self._v3.states
|
||||
|
||||
def get_scheduling_context(self) -> SchedulingContext | None:
|
||||
if v3 := self._v3:
|
||||
return v3.context
|
||||
return None
|
||||
def get_scheduling_context(self) -> SchedulingContext:
|
||||
return self._v3.context
|
||||
|
||||
def set_scheduling_states(self, request: SetSchedulingStatesRequest) -> None:
|
||||
if request.key != self._state_mutation_key:
|
||||
return
|
||||
|
||||
if v3 := self._v3:
|
||||
v3.states = request.states
|
||||
self._v3.states = request.states
|
||||
|
||||
def _run_state_mutation_hook(self) -> None:
|
||||
def on_eval(result: Any) -> None:
|
||||
@ -294,7 +266,7 @@ class Reviewer:
|
||||
# eval failed, usually a syntax error
|
||||
self._states_mutated = True
|
||||
|
||||
if self._v3 and (js := self._state_mutation_js):
|
||||
if js := self._state_mutation_js:
|
||||
self._states_mutated = False
|
||||
self.web.evalWithCallback(
|
||||
RUN_STATE_MUTATION.format(key=self._state_mutation_key, js=js),
|
||||
@ -450,27 +422,24 @@ class Reviewer:
|
||||
if not proceed:
|
||||
return
|
||||
|
||||
if (v3 := self._v3) and (sched := cast(V3Scheduler, self.mw.col.sched)):
|
||||
answer = sched.build_answer(
|
||||
card=self.card,
|
||||
states=v3.states,
|
||||
rating=v3.rating_from_ease(ease),
|
||||
)
|
||||
sched = cast(V3Scheduler, self.mw.col.sched)
|
||||
answer = sched.build_answer(
|
||||
card=self.card,
|
||||
states=self._v3.states,
|
||||
rating=self._v3.rating_from_ease(ease),
|
||||
)
|
||||
|
||||
def after_answer(changes: OpChanges) -> None:
|
||||
if gui_hooks.reviewer_did_answer_card.count() > 0:
|
||||
self.card.load()
|
||||
self._after_answering(ease)
|
||||
if sched.state_is_leech(answer.new_state):
|
||||
self.onLeech()
|
||||
|
||||
self.state = "transition"
|
||||
answer_card(parent=self.mw, answer=answer).success(
|
||||
after_answer
|
||||
).run_in_background(initiator=self)
|
||||
else:
|
||||
self.mw.col.sched.answerCard(self.card, ease)
|
||||
def after_answer(changes: OpChanges) -> None:
|
||||
if gui_hooks.reviewer_did_answer_card.count() > 0:
|
||||
self.card.load()
|
||||
self._after_answering(ease)
|
||||
if sched.state_is_leech(answer.new_state):
|
||||
self.onLeech()
|
||||
|
||||
self.state = "transition"
|
||||
answer_card(parent=self.mw, answer=answer).success(
|
||||
after_answer
|
||||
).run_in_background(initiator=self)
|
||||
|
||||
def _after_answering(self, ease: Literal[1, 2, 3, 4]) -> None:
|
||||
gui_hooks.reviewer_did_answer_card(self, self.card, ease)
|
||||
@ -792,11 +761,8 @@ timerStopped = false;
|
||||
def _answerButtons(self) -> str:
|
||||
default = self._defaultEase()
|
||||
|
||||
if v3 := self._v3:
|
||||
assert isinstance(self.mw.col.sched, V3Scheduler)
|
||||
labels = self.mw.col.sched.describe_next_states(v3.states)
|
||||
else:
|
||||
labels = None
|
||||
assert isinstance(self.mw.col.sched, V3Scheduler)
|
||||
labels = self.mw.col.sched.describe_next_states(self._v3.states)
|
||||
|
||||
def but(i: int, label: str) -> str:
|
||||
if i == default:
|
||||
|
@ -823,7 +823,6 @@ gui_hooks.webview_did_inject_style_into_page.append(mytest)
|
||||
legacy_hook="colLoading",
|
||||
),
|
||||
Hook(name="undo_state_did_change", args=["info: UndoActionsInfo"]),
|
||||
Hook(name="review_did_undo", args=["card_id: int"], legacy_hook="revertedCard"),
|
||||
Hook(
|
||||
name="style_did_init",
|
||||
args=["style: str"],
|
||||
|
@ -46,6 +46,7 @@ impl AnkiError {
|
||||
| AnkiError::FsrsInsufficientData => Kind::InvalidInput,
|
||||
#[cfg(windows)]
|
||||
AnkiError::WindowsError { .. } => Kind::OsError,
|
||||
AnkiError::SchedulerUpgradeRequired => Kind::SchedulerUpgradeRequired,
|
||||
};
|
||||
|
||||
anki_proto::backend::BackendError {
|
||||
|
@ -184,8 +184,8 @@ impl Card {
|
||||
}
|
||||
|
||||
/// 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);
|
||||
fn set_deck(&mut self, deck: DeckId) {
|
||||
self.remove_from_filtered_deck_restoring_queue();
|
||||
self.deck_id = deck;
|
||||
}
|
||||
|
||||
@ -342,13 +342,16 @@ impl Collection {
|
||||
}
|
||||
|
||||
pub fn set_deck(&mut self, cards: &[CardId], deck_id: DeckId) -> Result<OpOutput<usize>> {
|
||||
let sched = self.scheduler_version();
|
||||
if sched == SchedulerVersion::V1 {
|
||||
return Err(AnkiError::SchedulerUpgradeRequired);
|
||||
}
|
||||
let deck = self.get_deck(deck_id)?.or_not_found(deck_id)?;
|
||||
let config_id = deck.config_id().ok_or(AnkiError::FilteredDeckError {
|
||||
source: FilteredDeckError::CanNotMoveCardsInto,
|
||||
})?;
|
||||
let config = self.get_deck_config(config_id, true)?.unwrap();
|
||||
let mut steps_adjuster = RemainingStepsAdjuster::new(&config);
|
||||
let sched = self.scheduler_version();
|
||||
let usn = self.usn()?;
|
||||
self.transact(Op::SetCardDeck, |col| {
|
||||
let mut count = 0;
|
||||
@ -359,7 +362,7 @@ impl Collection {
|
||||
count += 1;
|
||||
let original = card.clone();
|
||||
steps_adjuster.adjust_remaining_steps(col, &mut card)?;
|
||||
card.set_deck(deck_id, sched);
|
||||
card.set_deck(deck_id);
|
||||
col.update_card_inner(&mut card, original, usn)?;
|
||||
}
|
||||
Ok(count)
|
||||
|
@ -50,7 +50,6 @@ impl Collection {
|
||||
.storage
|
||||
.get_collection_timestamps()?
|
||||
.schema_changed_since_sync(),
|
||||
v3_scheduler: self.get_config_bool(BoolKey::Sched2021),
|
||||
card_state_customizer: self.get_config_string(StringKey::CardStateCustomizer),
|
||||
new_cards_ignore_review_limit: self.get_config_bool(BoolKey::NewCardsIgnoreReviewLimit),
|
||||
fsrs: self.get_config_bool(BoolKey::Fsrs),
|
||||
|
@ -35,8 +35,7 @@ impl Collection {
|
||||
days_elapsed: u32,
|
||||
learn_cutoff: u32,
|
||||
) -> Result<HashMap<DeckId, DueCounts>> {
|
||||
self.storage
|
||||
.due_counts(self.scheduler_version(), days_elapsed, learn_cutoff)
|
||||
self.storage.due_counts(days_elapsed, learn_cutoff)
|
||||
}
|
||||
|
||||
pub(crate) fn counts_for_deck_today(
|
||||
|
@ -62,7 +62,6 @@ impl RemainingLimits {
|
||||
deck: &Deck,
|
||||
config: Option<&DeckConfig>,
|
||||
today: u32,
|
||||
v3: bool,
|
||||
new_cards_ignore_review_limit: bool,
|
||||
) -> Self {
|
||||
if let Ok(normal) = deck.normal() {
|
||||
@ -70,7 +69,6 @@ impl RemainingLimits {
|
||||
return Self::new_for_normal_deck(
|
||||
deck,
|
||||
today,
|
||||
v3,
|
||||
new_cards_ignore_review_limit,
|
||||
normal,
|
||||
config,
|
||||
@ -83,28 +81,11 @@ impl RemainingLimits {
|
||||
fn new_for_normal_deck(
|
||||
deck: &Deck,
|
||||
today: u32,
|
||||
v3: bool,
|
||||
new_cards_ignore_review_limit: bool,
|
||||
normal: &NormalDeck,
|
||||
config: &DeckConfig,
|
||||
) -> RemainingLimits {
|
||||
if v3 {
|
||||
Self::new_for_normal_deck_v3(deck, today, new_cards_ignore_review_limit, normal, config)
|
||||
} else {
|
||||
Self::new_for_normal_deck_v2(deck, today, config)
|
||||
}
|
||||
}
|
||||
|
||||
fn new_for_normal_deck_v2(deck: &Deck, today: u32, config: &DeckConfig) -> RemainingLimits {
|
||||
let review_limit = config.inner.reviews_per_day;
|
||||
let new_limit = config.inner.new_per_day;
|
||||
let (new_today_count, review_today_count) = deck.new_rev_counts(today);
|
||||
|
||||
Self {
|
||||
review: (review_limit as i32 - review_today_count).max(0) as u32,
|
||||
new: (new_limit as i32 - new_today_count).max(0) as u32,
|
||||
cap_new_to_review: false,
|
||||
}
|
||||
Self::new_for_normal_deck_v3(deck, today, new_cards_ignore_review_limit, normal, config)
|
||||
}
|
||||
|
||||
fn new_for_normal_deck_v3(
|
||||
@ -189,7 +170,6 @@ pub(crate) fn remaining_limits_map<'a>(
|
||||
decks: impl Iterator<Item = &'a Deck>,
|
||||
config: &'a HashMap<DeckConfigId, DeckConfig>,
|
||||
today: u32,
|
||||
v3: bool,
|
||||
new_cards_ignore_review_limit: bool,
|
||||
) -> HashMap<DeckId, RemainingLimits> {
|
||||
decks
|
||||
@ -200,7 +180,6 @@ pub(crate) fn remaining_limits_map<'a>(
|
||||
deck,
|
||||
deck.config_id().and_then(|id| config.get(&id)),
|
||||
today,
|
||||
v3,
|
||||
new_cards_ignore_review_limit,
|
||||
),
|
||||
)
|
||||
@ -231,7 +210,6 @@ impl NodeLimits {
|
||||
deck,
|
||||
deck.config_id().and_then(|id| config.get(&id)),
|
||||
today,
|
||||
true,
|
||||
new_cards_ignore_review_limit,
|
||||
),
|
||||
}
|
||||
|
@ -14,7 +14,6 @@ use unicase::UniCase;
|
||||
use super::limits::remaining_limits_map;
|
||||
use super::limits::RemainingLimits;
|
||||
use super::DueCounts;
|
||||
use crate::config::SchedulerVersion;
|
||||
use crate::ops::OpOutput;
|
||||
use crate::prelude::*;
|
||||
use crate::undo::Op;
|
||||
@ -100,66 +99,6 @@ fn add_counts(node: &mut DeckTreeNode, counts: &HashMap<DeckId, DueCounts>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply parent limits to children, and add child counts to parents.
|
||||
fn sum_counts_and_apply_limits_v1(
|
||||
node: &mut DeckTreeNode,
|
||||
limits: &HashMap<DeckId, RemainingLimits>,
|
||||
parent_limits: RemainingLimits,
|
||||
) {
|
||||
let mut remaining = limits
|
||||
.get(&DeckId(node.deck_id))
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
remaining.cap_to(parent_limits);
|
||||
|
||||
// apply our limit to children and tally their counts
|
||||
let mut child_new_total = 0;
|
||||
let mut child_rev_total = 0;
|
||||
for child in &mut node.children {
|
||||
sum_counts_and_apply_limits_v1(child, limits, remaining);
|
||||
child_new_total += child.new_count;
|
||||
child_rev_total += child.review_count;
|
||||
// no limit on learning cards
|
||||
node.learn_count += child.learn_count;
|
||||
}
|
||||
|
||||
// add child counts to our count, capped to remaining limit
|
||||
node.new_count = (node.new_count + child_new_total).min(remaining.new);
|
||||
node.review_count = (node.review_count + child_rev_total).min(remaining.review);
|
||||
}
|
||||
|
||||
/// Apply parent new limits to children, and add child counts to parents. Unlike
|
||||
/// v1, reviews are not capped by their parents, and we
|
||||
/// return the uncapped review amount to add to the parent.
|
||||
fn sum_counts_and_apply_limits_v2(
|
||||
node: &mut DeckTreeNode,
|
||||
limits: &HashMap<DeckId, RemainingLimits>,
|
||||
parent_limits: RemainingLimits,
|
||||
) -> u32 {
|
||||
let original_rev_count = node.review_count;
|
||||
let mut remaining = limits
|
||||
.get(&DeckId(node.deck_id))
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
remaining.new = remaining.new.min(parent_limits.new);
|
||||
|
||||
// apply our limit to children and tally their counts
|
||||
let mut child_new_total = 0;
|
||||
let mut child_rev_total = 0;
|
||||
for child in &mut node.children {
|
||||
child_rev_total += sum_counts_and_apply_limits_v2(child, limits, remaining);
|
||||
child_new_total += child.new_count;
|
||||
// no limit on learning cards
|
||||
node.learn_count += child.learn_count;
|
||||
}
|
||||
|
||||
// add child counts to our count, capped to remaining limit
|
||||
node.new_count = (node.new_count + child_new_total).min(remaining.new);
|
||||
node.review_count = (node.review_count + child_rev_total).min(remaining.review);
|
||||
|
||||
original_rev_count + child_rev_total
|
||||
}
|
||||
|
||||
/// A temporary container used during count summation and limit application.
|
||||
#[derive(Default, Clone)]
|
||||
struct NodeCountsV3 {
|
||||
@ -325,8 +264,6 @@ impl Collection {
|
||||
let timing_at_stamp = self.timing_for_timestamp(timestamp)?;
|
||||
let days_elapsed = timing_at_stamp.days_elapsed;
|
||||
let learn_cutoff = (timestamp.0 as u32) + self.learn_ahead_secs();
|
||||
let sched_ver = self.scheduler_version();
|
||||
let v3 = self.get_config_bool(BoolKey::Sched2021);
|
||||
let new_cards_ignore_review_limit =
|
||||
self.get_config_bool(BoolKey::NewCardsIgnoreReviewLimit);
|
||||
let counts = self.due_counts(days_elapsed, learn_cutoff)?;
|
||||
@ -336,18 +273,9 @@ impl Collection {
|
||||
decks_map.values(),
|
||||
&dconf,
|
||||
days_elapsed,
|
||||
v3,
|
||||
new_cards_ignore_review_limit,
|
||||
);
|
||||
if sched_ver == SchedulerVersion::V2 {
|
||||
if v3 {
|
||||
sum_counts_and_apply_limits_v3(&mut tree, &limits);
|
||||
} else {
|
||||
sum_counts_and_apply_limits_v2(&mut tree, &limits, RemainingLimits::default());
|
||||
}
|
||||
} else {
|
||||
sum_counts_and_apply_limits_v1(&mut tree, &limits, RemainingLimits::default());
|
||||
}
|
||||
sum_counts_and_apply_limits_v3(&mut tree, &limits);
|
||||
}
|
||||
|
||||
Ok(tree)
|
||||
|
@ -115,6 +115,7 @@ pub enum AnkiError {
|
||||
InvalidServiceIndex,
|
||||
FsrsWeightsInvalid,
|
||||
FsrsInsufficientData,
|
||||
SchedulerUpgradeRequired,
|
||||
}
|
||||
|
||||
// error helpers
|
||||
@ -168,6 +169,9 @@ impl AnkiError {
|
||||
AnkiError::NotFound { source } => source.message(tr),
|
||||
AnkiError::FsrsInsufficientData => tr.deck_config_not_enough_history().into(),
|
||||
AnkiError::FsrsWeightsInvalid => tr.deck_config_invalid_weights().into(),
|
||||
AnkiError::SchedulerUpgradeRequired => {
|
||||
tr.scheduling_update_required().replace("V2", "v3")
|
||||
}
|
||||
#[cfg(windows)]
|
||||
AnkiError::WindowsError { source } => format!("{source:?}"),
|
||||
}
|
||||
|
@ -89,6 +89,9 @@ impl Context<'_> {
|
||||
remapped_templates,
|
||||
imported_decks,
|
||||
)?;
|
||||
if ctx.scheduler_version == SchedulerVersion::V1 {
|
||||
return Err(AnkiError::SchedulerUpgradeRequired);
|
||||
}
|
||||
ctx.import_cards(mem::take(&mut self.data.cards), keep_filtered)?;
|
||||
ctx.import_revlog(mem::take(&mut self.data.revlog))
|
||||
}
|
||||
@ -136,7 +139,7 @@ impl CardContext<'_> {
|
||||
self.remap_template_index(card);
|
||||
card.shift_collection_relative_dates(self.collection_delta);
|
||||
if !keep_filtered {
|
||||
card.maybe_remove_from_filtered_deck(self.scheduler_version);
|
||||
card.maybe_remove_from_filtered_deck();
|
||||
}
|
||||
let old_id = self.uniquify_card_id(card);
|
||||
|
||||
@ -196,11 +199,11 @@ impl Card {
|
||||
self.ctype == CardType::Review
|
||||
}
|
||||
|
||||
fn maybe_remove_from_filtered_deck(&mut self, version: SchedulerVersion) {
|
||||
fn maybe_remove_from_filtered_deck(&mut self) {
|
||||
if self.is_filtered() {
|
||||
// instead of moving between decks, the deck is converted to a regular one
|
||||
self.original_deck_id = self.deck_id;
|
||||
self.remove_from_filtered_deck_restoring_queue(version);
|
||||
self.remove_from_filtered_deck_restoring_queue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -48,16 +48,6 @@ impl Collection {
|
||||
|
||||
pub fn get_scheduling_preferences(&self) -> Result<Scheduling> {
|
||||
Ok(Scheduling {
|
||||
scheduler_version: match self.scheduler_version() {
|
||||
crate::config::SchedulerVersion::V1 => 1,
|
||||
crate::config::SchedulerVersion::V2 => {
|
||||
if self.get_config_bool(BoolKey::Sched2021) {
|
||||
3
|
||||
} else {
|
||||
2
|
||||
}
|
||||
}
|
||||
},
|
||||
rollover: self.rollover_for_current_scheduler()? as u32,
|
||||
learn_ahead_secs: self.learn_ahead_secs(),
|
||||
new_review_mix: match self.get_new_review_mix() {
|
||||
|
@ -4,7 +4,6 @@
|
||||
use super::CardStateUpdater;
|
||||
use super::RevlogEntryPartial;
|
||||
use crate::card::CardQueue;
|
||||
use crate::config::SchedulerVersion;
|
||||
use crate::scheduler::states::CardState;
|
||||
use crate::scheduler::states::IntervalKind;
|
||||
use crate::scheduler::states::PreviewState;
|
||||
@ -17,8 +16,7 @@ impl CardStateUpdater {
|
||||
) -> RevlogEntryPartial {
|
||||
let revlog = RevlogEntryPartial::new(current, next.into(), 0.0, self.secs_until_rollover());
|
||||
if next.finished {
|
||||
self.card
|
||||
.remove_from_filtered_deck_restoring_queue(SchedulerVersion::V2);
|
||||
self.card.remove_from_filtered_deck_restoring_queue();
|
||||
return revlog;
|
||||
}
|
||||
|
||||
|
@ -90,17 +90,13 @@ impl Collection {
|
||||
let mut count = 0;
|
||||
let usn = self.usn()?;
|
||||
let sched = self.scheduler_version();
|
||||
if sched == SchedulerVersion::V1 {
|
||||
return Err(AnkiError::SchedulerUpgradeRequired);
|
||||
}
|
||||
let desired_queue = match mode {
|
||||
BuryOrSuspendMode::Suspend => CardQueue::Suspended,
|
||||
BuryOrSuspendMode::BurySched => CardQueue::SchedBuried,
|
||||
BuryOrSuspendMode::BuryUser => {
|
||||
if sched == SchedulerVersion::V1 {
|
||||
// v1 scheduler only had one bury type
|
||||
CardQueue::SchedBuried
|
||||
} else {
|
||||
CardQueue::UserBuried
|
||||
}
|
||||
}
|
||||
BuryOrSuspendMode::BuryUser => CardQueue::UserBuried,
|
||||
};
|
||||
|
||||
for original in cards {
|
||||
@ -108,10 +104,6 @@ impl Collection {
|
||||
if card.queue != desired_queue {
|
||||
// do not bury suspended cards as that would unsuspend them
|
||||
if card.queue != CardQueue::Suspended {
|
||||
if sched == SchedulerVersion::V1 {
|
||||
card.remove_from_filtered_deck_restoring_queue(sched);
|
||||
card.remove_from_learning();
|
||||
}
|
||||
card.queue = desired_queue;
|
||||
count += 1;
|
||||
self.update_card_inner(&mut card, original, usn)?;
|
||||
|
@ -4,7 +4,6 @@
|
||||
use super::DeckFilterContext;
|
||||
use crate::card::CardQueue;
|
||||
use crate::card::CardType;
|
||||
use crate::config::SchedulerVersion;
|
||||
use crate::prelude::*;
|
||||
|
||||
impl Card {
|
||||
@ -37,26 +36,12 @@ impl Card {
|
||||
|
||||
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;
|
||||
}
|
||||
// if rescheduling is disabled, all cards go in the review queue
|
||||
if !ctx.config.reschedule {
|
||||
self.queue = CardQueue::Review;
|
||||
}
|
||||
if self.due > 0 {
|
||||
self.due = position;
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,7 +60,7 @@ impl Card {
|
||||
self.original_deck_id.or(self.deck_id)
|
||||
}
|
||||
|
||||
pub(crate) fn remove_from_filtered_deck_restoring_queue(&mut self, sched: SchedulerVersion) {
|
||||
pub(crate) fn remove_from_filtered_deck_restoring_queue(&mut self) {
|
||||
if self.original_deck_id.0 == 0 {
|
||||
// not in a filtered deck
|
||||
return;
|
||||
@ -84,33 +69,13 @@ impl Card {
|
||||
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;
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
if (self.queue as i8) >= 0 {
|
||||
self.restore_queue_from_type();
|
||||
}
|
||||
|
||||
self.original_due = 0;
|
||||
|
@ -26,7 +26,6 @@ pub struct FilteredDeckForUpdate {
|
||||
pub(crate) struct DeckFilterContext<'a> {
|
||||
pub target_deck: DeckId,
|
||||
pub config: &'a FilteredDeck,
|
||||
pub scheduler: SchedulerVersion,
|
||||
pub usn: Usn,
|
||||
pub today: u32,
|
||||
}
|
||||
@ -84,12 +83,11 @@ impl Collection {
|
||||
|
||||
// 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);
|
||||
card.remove_from_filtered_deck_restoring_queue();
|
||||
self.update_card_inner(&mut card, original, usn)?;
|
||||
}
|
||||
}
|
||||
@ -99,12 +97,7 @@ impl Collection {
|
||||
fn build_filtered_deck(&mut self, ctx: DeckFilterContext) -> Result<usize> {
|
||||
let start = -100_000;
|
||||
let mut position = start;
|
||||
let limit = if ctx.scheduler == SchedulerVersion::V1 {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
};
|
||||
for term in ctx.config.search_terms.iter().take(limit) {
|
||||
for term in ctx.config.search_terms.iter().take(2) {
|
||||
position = self.move_cards_matching_term(&ctx, term, position)?;
|
||||
}
|
||||
|
||||
@ -120,16 +113,11 @@ impl Collection {
|
||||
mut position: i32,
|
||||
) -> Result<i32> {
|
||||
let search = format!(
|
||||
"{} -is:suspended -is:buried -deck:filtered {}",
|
||||
"{} -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);
|
||||
@ -189,11 +177,14 @@ impl Collection {
|
||||
}
|
||||
|
||||
fn rebuild_filtered_deck_inner(&mut self, deck: &Deck, usn: Usn) -> Result<usize> {
|
||||
if self.scheduler_version() == SchedulerVersion::V1 {
|
||||
return Err(AnkiError::SchedulerUpgradeRequired);
|
||||
}
|
||||
|
||||
let config = deck.filtered()?;
|
||||
let ctx = DeckFilterContext {
|
||||
target_deck: deck.id,
|
||||
config,
|
||||
scheduler: self.scheduler_version(),
|
||||
usn,
|
||||
today: self.timing_today()?.days_elapsed,
|
||||
};
|
||||
|
@ -1,35 +0,0 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use crate::card::Card;
|
||||
use crate::card::CardQueue;
|
||||
use crate::card::CardType;
|
||||
use crate::deckconfig::INITIAL_EASE_FACTOR_THOUSANDS;
|
||||
|
||||
impl Card {
|
||||
/// Remove the card from the (re)learning queue.
|
||||
/// This will reset cards in learning.
|
||||
/// Only used in the V1 scheduler.
|
||||
/// Unlike the legacy Python code, this sets the due# to 0 instead of
|
||||
/// one past the previous max due number.
|
||||
pub(crate) fn remove_from_learning(&mut self) {
|
||||
if !matches!(self.queue, CardQueue::Learn | CardQueue::DayLearn) {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.ctype == CardType::Review {
|
||||
// reviews are removed from relearning
|
||||
self.due = self.original_due;
|
||||
self.original_due = 0;
|
||||
self.queue = CardQueue::Review;
|
||||
} else {
|
||||
// other cards are reset to new
|
||||
self.ctype = CardType::New;
|
||||
self.queue = CardQueue::New;
|
||||
self.interval = 0;
|
||||
self.due = 0;
|
||||
self.original_due = 0;
|
||||
self.ease_factor = INITIAL_EASE_FACTOR_THOUSANDS;
|
||||
}
|
||||
}
|
||||
}
|
@ -11,7 +11,6 @@ pub mod bury_and_suspend;
|
||||
pub(crate) mod congrats;
|
||||
pub(crate) mod filtered;
|
||||
pub mod fsrs;
|
||||
mod learning;
|
||||
pub mod new;
|
||||
pub(crate) mod queue;
|
||||
mod reviews;
|
||||
@ -24,8 +23,6 @@ mod upgrade;
|
||||
use chrono::FixedOffset;
|
||||
pub use reviews::parse_due_date_str;
|
||||
use timing::sched_timing_today;
|
||||
use timing::v1_creation_date_adjusted_to_hour;
|
||||
use timing::v1_rollover_from_creation_stamp;
|
||||
use timing::SchedTimingToday;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@ -118,16 +115,14 @@ impl Collection {
|
||||
|
||||
pub fn rollover_for_current_scheduler(&self) -> Result<u8> {
|
||||
match self.scheduler_version() {
|
||||
SchedulerVersion::V1 => v1_rollover_from_creation_stamp(self.storage.creation_stamp()?),
|
||||
SchedulerVersion::V1 => Err(AnkiError::SchedulerUpgradeRequired),
|
||||
SchedulerVersion::V2 => Ok(self.get_v2_rollover().unwrap_or(4)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_rollover_for_current_scheduler(&mut self, hour: u8) -> Result<()> {
|
||||
match self.scheduler_version() {
|
||||
SchedulerVersion::V1 => self.set_creation_stamp(TimestampSecs(
|
||||
v1_creation_date_adjusted_to_hour(self.storage.creation_stamp()?, hour)?,
|
||||
)),
|
||||
SchedulerVersion::V1 => Err(AnkiError::SchedulerUpgradeRequired),
|
||||
SchedulerVersion::V2 => self.set_v2_rollover(hour as u32),
|
||||
}
|
||||
}
|
||||
|
@ -63,8 +63,8 @@ impl Card {
|
||||
}
|
||||
|
||||
/// If the card is new, change its position, and return true.
|
||||
fn set_new_position(&mut self, position: u32, v2: bool) -> bool {
|
||||
if v2 && self.ctype == CardType::New {
|
||||
fn set_new_position(&mut self, position: u32) -> bool {
|
||||
if self.ctype == CardType::New {
|
||||
if self.is_filtered() {
|
||||
self.original_due = position as i32;
|
||||
} else {
|
||||
@ -234,16 +234,18 @@ impl Collection {
|
||||
shift: bool,
|
||||
usn: Usn,
|
||||
) -> Result<usize> {
|
||||
let v2 = self.scheduler_version() != SchedulerVersion::V1;
|
||||
if self.scheduler_version() == SchedulerVersion::V1 {
|
||||
return Err(AnkiError::SchedulerUpgradeRequired);
|
||||
}
|
||||
if shift {
|
||||
self.shift_existing_cards(starting_from, step * cids.len() as u32, usn, v2)?;
|
||||
self.shift_existing_cards(starting_from, step * cids.len() as u32, usn)?;
|
||||
}
|
||||
let cards = self.all_cards_for_ids(cids, true)?;
|
||||
let sorter = NewCardSorter::new(&cards, starting_from, step, order);
|
||||
let mut count = 0;
|
||||
for mut card in cards {
|
||||
let original = card.clone();
|
||||
if card.set_new_position(sorter.position(&card), v2) {
|
||||
if card.set_new_position(sorter.position(&card)) {
|
||||
count += 1;
|
||||
self.update_card_inner(&mut card, original, usn)?;
|
||||
}
|
||||
@ -287,10 +289,10 @@ impl Collection {
|
||||
self.sort_cards_inner(&cids, 1, 1, order.into(), false, usn)
|
||||
}
|
||||
|
||||
fn shift_existing_cards(&mut self, start: u32, by: u32, usn: Usn, v2: bool) -> Result<()> {
|
||||
fn shift_existing_cards(&mut self, start: u32, by: u32, usn: Usn) -> Result<()> {
|
||||
for mut card in self.storage.all_cards_at_or_above_position(start)? {
|
||||
let original = card.clone();
|
||||
card.set_new_position(card.due as u32 + by, v2);
|
||||
card.set_new_position(card.due as u32 + by);
|
||||
self.update_card_inner(&mut card, original, usn)?;
|
||||
}
|
||||
Ok(())
|
||||
|
@ -94,13 +94,6 @@ pub fn local_minutes_west_for_stamp(stamp: TimestampSecs) -> Result<i32> {
|
||||
Ok(stamp.local_datetime()?.offset().utc_minus_local() / 60)
|
||||
}
|
||||
|
||||
// Legacy code
|
||||
// ----------------------------------
|
||||
|
||||
pub(crate) fn v1_rollover_from_creation_stamp(crt: TimestampSecs) -> Result<u8> {
|
||||
crt.local_datetime().map(|dt| dt.hour() as u8)
|
||||
}
|
||||
|
||||
pub(crate) fn v1_creation_date() -> i64 {
|
||||
let now = TimestampSecs::now();
|
||||
v1_creation_date_inner(now, local_minutes_west_for_stamp(now).unwrap())
|
||||
@ -119,19 +112,6 @@ fn v1_creation_date_inner(now: TimestampSecs, mins_west: i32) -> i64 {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn v1_creation_date_adjusted_to_hour(crt: TimestampSecs, hour: u8) -> Result<i64> {
|
||||
let offset = fixed_offset_from_minutes(local_minutes_west_for_stamp(crt)?);
|
||||
v1_creation_date_adjusted_to_hour_inner(crt, hour, offset)
|
||||
}
|
||||
|
||||
fn v1_creation_date_adjusted_to_hour_inner(
|
||||
crt: TimestampSecs,
|
||||
hour: u8,
|
||||
offset: FixedOffset,
|
||||
) -> Result<i64> {
|
||||
Ok(rollover_datetime(crt.datetime(offset)?, hour).timestamp())
|
||||
}
|
||||
|
||||
fn sched_timing_today_v1(crt: TimestampSecs, now: TimestampSecs) -> SchedTimingToday {
|
||||
let days_elapsed = (now.0 - crt.0) / 86_400;
|
||||
let next_day_at = TimestampSecs(crt.0 + (days_elapsed + 1) * 86_400);
|
||||
@ -494,19 +474,5 @@ mod test {
|
||||
.unwrap()
|
||||
.timestamp()
|
||||
);
|
||||
|
||||
let crt = TimestampSecs(v1_creation_date_inner(now, AEST_MINS_WEST));
|
||||
assert_eq!(
|
||||
Ok(crt.0),
|
||||
v1_creation_date_adjusted_to_hour_inner(crt, 4, offset)
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(crt.0 + 3600),
|
||||
v1_creation_date_adjusted_to_hour_inner(crt, 5, offset)
|
||||
);
|
||||
assert_eq!(
|
||||
Ok(crt.0 - 3600 * 4),
|
||||
v1_creation_date_adjusted_to_hour_inner(crt, 0, offset)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -581,8 +581,8 @@ impl super::SqliteStorage {
|
||||
}
|
||||
|
||||
pub(crate) fn congrats_info(&self, current: &Deck, today: u32) -> Result<CongratsInfo> {
|
||||
// FIXME: when v1/v2 are dropped, this line will become obsolete, as it's run
|
||||
// on queue build by v3
|
||||
// NOTE: this line is obsolete in v3 as it's run on queue build, but kept to
|
||||
// prevent errors for v1/v2 users before they upgrade
|
||||
self.update_active_decks(current)?;
|
||||
self.db
|
||||
.prepare(include_str!("congrats.sql"))?
|
||||
|
@ -14,28 +14,14 @@ SELECT did,
|
||||
-- intraday learning
|
||||
sum(
|
||||
(
|
||||
CASE
|
||||
:sched_ver
|
||||
WHEN 2 THEN (
|
||||
-- v2 scheduler
|
||||
(
|
||||
queue = :learn_queue
|
||||
AND due < :learn_cutoff
|
||||
)
|
||||
OR (
|
||||
queue = :preview_queue
|
||||
AND due <= :learn_cutoff
|
||||
)
|
||||
)
|
||||
ELSE (
|
||||
-- v1 scheduler
|
||||
CASE
|
||||
WHEN queue = :learn_queue
|
||||
AND due < :learn_cutoff THEN left / 1000
|
||||
ELSE 0
|
||||
END
|
||||
)
|
||||
END
|
||||
(
|
||||
queue = :learn_queue
|
||||
AND due < :learn_cutoff
|
||||
)
|
||||
OR (
|
||||
queue = :preview_queue
|
||||
AND due <= :learn_cutoff
|
||||
)
|
||||
)
|
||||
),
|
||||
-- total
|
||||
|
@ -13,7 +13,6 @@ use unicase::UniCase;
|
||||
|
||||
use super::SqliteStorage;
|
||||
use crate::card::CardQueue;
|
||||
use crate::config::SchedulerVersion;
|
||||
use crate::decks::immediate_parent_name;
|
||||
use crate::decks::DeckCommon;
|
||||
use crate::decks::DeckKindContainer;
|
||||
@ -297,16 +296,13 @@ impl SqliteStorage {
|
||||
|
||||
pub(crate) fn due_counts(
|
||||
&self,
|
||||
sched: SchedulerVersion,
|
||||
day_cutoff: u32,
|
||||
learn_cutoff: u32,
|
||||
) -> Result<HashMap<DeckId, DueCounts>> {
|
||||
let sched_ver = sched as u8;
|
||||
let params = named_params! {
|
||||
":new_queue": CardQueue::New as u8,
|
||||
":review_queue": CardQueue::Review as u8,
|
||||
":day_cutoff": day_cutoff,
|
||||
":sched_ver": sched_ver,
|
||||
":learn_queue": CardQueue::Learn as u8,
|
||||
":learn_cutoff": learn_cutoff,
|
||||
":daylearn_queue": CardQueue::DayLearn as u8,
|
||||
|
@ -133,20 +133,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
}}
|
||||
/>
|
||||
<DynamicallySlottable slotHost={Item} {api}>
|
||||
{#if state.v3Scheduler}
|
||||
<Item>
|
||||
<SwitchRow bind:value={$fsrs} defaultValue={false}>
|
||||
<SettingTitle
|
||||
on:click={() =>
|
||||
openHelpModal(Object.keys(settings).indexOf("fsrs"))}
|
||||
>
|
||||
FSRS
|
||||
</SettingTitle>
|
||||
</SwitchRow>
|
||||
</Item>
|
||||
<Item>
|
||||
<SwitchRow bind:value={$fsrs} defaultValue={false}>
|
||||
<SettingTitle
|
||||
on:click={() =>
|
||||
openHelpModal(Object.keys(settings).indexOf("fsrs"))}
|
||||
>
|
||||
FSRS
|
||||
</SettingTitle>
|
||||
</SwitchRow>
|
||||
</Item>
|
||||
|
||||
<Warning warning={fsrsClientWarning} />
|
||||
{/if}
|
||||
<Warning warning={fsrsClientWarning} />
|
||||
|
||||
<Item>
|
||||
<SpinBoxRow
|
||||
@ -164,7 +162,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
</SpinBoxRow>
|
||||
</Item>
|
||||
|
||||
{#if !$fsrs || !state.v3Scheduler}
|
||||
{#if !$fsrs}
|
||||
<Item>
|
||||
<SpinBoxFloatRow
|
||||
bind:value={$config.initialEase}
|
||||
@ -257,17 +255,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if state.v3Scheduler}
|
||||
<Item>
|
||||
<CardStateCustomizer
|
||||
title={settings.customScheduling.title}
|
||||
on:click={() =>
|
||||
openHelpModal(
|
||||
Object.keys(settings).indexOf("customScheduling"),
|
||||
)}
|
||||
bind:value={$cardStateCustomizer}
|
||||
/>
|
||||
</Item>
|
||||
{/if}
|
||||
<Item>
|
||||
<CardStateCustomizer
|
||||
title={settings.customScheduling.title}
|
||||
on:click={() =>
|
||||
openHelpModal(Object.keys(settings).indexOf("customScheduling"))}
|
||||
bind:value={$cardStateCustomizer}
|
||||
/>
|
||||
</Item>
|
||||
</DynamicallySlottable>
|
||||
</TitledContainer>
|
||||
|
@ -23,9 +23,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
const config = state.currentConfig;
|
||||
const defaults = state.defaults;
|
||||
|
||||
const priorityTooltip = state.v3Scheduler
|
||||
? "\n\n" + tr.deckConfigBuryPriorityTooltip()
|
||||
: "";
|
||||
const priorityTooltip = "\n\n" + tr.deckConfigBuryPriorityTooltip();
|
||||
|
||||
const settings = {
|
||||
buryNewSiblings: {
|
||||
@ -91,24 +89,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
</SwitchRow>
|
||||
</Item>
|
||||
|
||||
{#if state.v3Scheduler}
|
||||
<Item>
|
||||
<SwitchRow
|
||||
bind:value={$config.buryInterdayLearning}
|
||||
defaultValue={defaults.buryInterdayLearning}
|
||||
<Item>
|
||||
<SwitchRow
|
||||
bind:value={$config.buryInterdayLearning}
|
||||
defaultValue={defaults.buryInterdayLearning}
|
||||
>
|
||||
<SettingTitle
|
||||
on:click={() =>
|
||||
openHelpModal(
|
||||
Object.keys(settings).indexOf(
|
||||
"buryInterdayLearningSiblings",
|
||||
),
|
||||
)}
|
||||
>
|
||||
<SettingTitle
|
||||
on:click={() =>
|
||||
openHelpModal(
|
||||
Object.keys(settings).indexOf(
|
||||
"buryInterdayLearningSiblings",
|
||||
),
|
||||
)}
|
||||
>
|
||||
{settings.buryInterdayLearningSiblings.title}
|
||||
</SettingTitle>
|
||||
</SwitchRow>
|
||||
</Item>
|
||||
{/if}
|
||||
{settings.buryInterdayLearningSiblings.title}
|
||||
</SettingTitle>
|
||||
</SwitchRow>
|
||||
</Item>
|
||||
</DynamicallySlottable>
|
||||
</TitledContainer>
|
||||
|
@ -44,25 +44,16 @@
|
||||
const config = state.currentConfig;
|
||||
const limits = state.deckLimits;
|
||||
const defaults = state.defaults;
|
||||
const parentLimits = state.parentLimits;
|
||||
const newCardsIgnoreReviewLimit = state.newCardsIgnoreReviewLimit;
|
||||
|
||||
const v3Extra = state.v3Scheduler
|
||||
? "\n\n" + tr.deckConfigLimitDeckV3() + "\n\n" + tr.deckConfigTabDescription()
|
||||
: "";
|
||||
const reviewV3Extra = state.v3Scheduler
|
||||
? "\n\n" + tr.deckConfigLimitInterdayBoundByReviews() + v3Extra
|
||||
: "";
|
||||
const v3Extra =
|
||||
"\n\n" + tr.deckConfigLimitDeckV3() + "\n\n" + tr.deckConfigTabDescription();
|
||||
const reviewV3Extra = "\n\n" + tr.deckConfigLimitInterdayBoundByReviews() + v3Extra;
|
||||
const newCardsIgnoreReviewLimitHelp =
|
||||
tr.deckConfigAffectsEntireCollection() +
|
||||
"\n\n" +
|
||||
tr.deckConfigNewCardsIgnoreReviewLimitTooltip();
|
||||
|
||||
$: newCardsGreaterThanParent =
|
||||
!state.v3Scheduler && newValue > $parentLimits.newCards
|
||||
? tr.deckConfigDailyLimitWillBeCapped({ cards: $parentLimits.newCards })
|
||||
: "";
|
||||
|
||||
$: reviewsTooLow =
|
||||
Math.min(9999, newValue * 10) > reviewsValue
|
||||
? tr.deckConfigReviewsTooLow({
|
||||
@ -79,26 +70,21 @@
|
||||
$config.newPerDay,
|
||||
null,
|
||||
),
|
||||
].concat(
|
||||
state.v3Scheduler
|
||||
? [
|
||||
new ValueTab(
|
||||
tr.deckConfigDeckOnly(),
|
||||
$limits.new ?? null,
|
||||
(value) => ($limits.new = value ?? undefined),
|
||||
null,
|
||||
null,
|
||||
),
|
||||
new ValueTab(
|
||||
tr.deckConfigTodayOnly(),
|
||||
$limits.newTodayActive ? $limits.newToday ?? null : null,
|
||||
(value) => ($limits.newToday = value ?? undefined),
|
||||
null,
|
||||
$limits.newToday ?? null,
|
||||
),
|
||||
]
|
||||
: [],
|
||||
);
|
||||
new ValueTab(
|
||||
tr.deckConfigDeckOnly(),
|
||||
$limits.new ?? null,
|
||||
(value) => ($limits.new = value ?? undefined),
|
||||
null,
|
||||
null,
|
||||
),
|
||||
new ValueTab(
|
||||
tr.deckConfigTodayOnly(),
|
||||
$limits.newTodayActive ? $limits.newToday ?? null : null,
|
||||
(value) => ($limits.newToday = value ?? undefined),
|
||||
null,
|
||||
$limits.newToday ?? null,
|
||||
),
|
||||
];
|
||||
|
||||
const reviewTabs: ValueTab[] = [
|
||||
new ValueTab(
|
||||
@ -108,26 +94,21 @@
|
||||
$config.reviewsPerDay,
|
||||
null,
|
||||
),
|
||||
].concat(
|
||||
state.v3Scheduler
|
||||
? [
|
||||
new ValueTab(
|
||||
tr.deckConfigDeckOnly(),
|
||||
$limits.review ?? null,
|
||||
(value) => ($limits.review = value ?? undefined),
|
||||
null,
|
||||
null,
|
||||
),
|
||||
new ValueTab(
|
||||
tr.deckConfigTodayOnly(),
|
||||
$limits.reviewTodayActive ? $limits.reviewToday ?? null : null,
|
||||
(value) => ($limits.reviewToday = value ?? undefined),
|
||||
null,
|
||||
$limits.reviewToday ?? null,
|
||||
),
|
||||
]
|
||||
: [],
|
||||
);
|
||||
new ValueTab(
|
||||
tr.deckConfigDeckOnly(),
|
||||
$limits.review ?? null,
|
||||
(value) => ($limits.review = value ?? undefined),
|
||||
null,
|
||||
null,
|
||||
),
|
||||
new ValueTab(
|
||||
tr.deckConfigTodayOnly(),
|
||||
$limits.reviewTodayActive ? $limits.reviewToday ?? null : null,
|
||||
(value) => ($limits.reviewToday = value ?? undefined),
|
||||
null,
|
||||
$limits.reviewToday ?? null,
|
||||
),
|
||||
];
|
||||
|
||||
let newValue = 0;
|
||||
let reviewsValue = 0;
|
||||
@ -184,9 +165,6 @@
|
||||
</SpinBoxRow>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Warning warning={newCardsGreaterThanParent} />
|
||||
</Item>
|
||||
<Item>
|
||||
<SpinBoxRow bind:value={reviewsValue} defaultValue={defaults.reviewsPerDay}>
|
||||
<TabbedValue slot="tabs" tabs={reviewTabs} bind:value={reviewsValue} />
|
||||
@ -203,21 +181,17 @@
|
||||
<Warning warning={reviewsTooLow} />
|
||||
</Item>
|
||||
|
||||
{#if state.v3Scheduler}
|
||||
<Item>
|
||||
<SwitchRow bind:value={$newCardsIgnoreReviewLimit} defaultValue={false}>
|
||||
<SettingTitle
|
||||
on:click={() =>
|
||||
openHelpModal(
|
||||
Object.keys(settings).indexOf(
|
||||
"newCardsIgnoreReviewLimit",
|
||||
),
|
||||
)}
|
||||
>
|
||||
{settings.newCardsIgnoreReviewLimit.title}
|
||||
</SettingTitle>
|
||||
</SwitchRow>
|
||||
</Item>
|
||||
{/if}
|
||||
<Item>
|
||||
<SwitchRow bind:value={$newCardsIgnoreReviewLimit} defaultValue={false}>
|
||||
<SettingTitle
|
||||
on:click={() =>
|
||||
openHelpModal(
|
||||
Object.keys(settings).indexOf("newCardsIgnoreReviewLimit"),
|
||||
)}
|
||||
>
|
||||
{settings.newCardsIgnoreReviewLimit.title}
|
||||
</SettingTitle>
|
||||
</SwitchRow>
|
||||
</Item>
|
||||
</DynamicallySlottable>
|
||||
</TitledContainer>
|
||||
|
@ -86,13 +86,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
</Row>
|
||||
</Item>
|
||||
|
||||
{#if state.v3Scheduler}
|
||||
<Item>
|
||||
<Row class="row-columns">
|
||||
<DisplayOrder {state} api={displayOrder} />
|
||||
</Row>
|
||||
</Item>
|
||||
{/if}
|
||||
<Item>
|
||||
<Row class="row-columns">
|
||||
<DisplayOrder {state} api={displayOrder} />
|
||||
</Row>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Row class="row-columns">
|
||||
|
@ -51,7 +51,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
: "";
|
||||
|
||||
$: insertionOrderRandom =
|
||||
state.v3Scheduler &&
|
||||
$config.newCardInsertOrder == DeckConfig_Config_NewCardInsertOrder.RANDOM
|
||||
? tr.deckConfigNewInsertionOrderRandomWithV3()
|
||||
: "";
|
||||
|
@ -72,7 +72,6 @@ const exampleData = {
|
||||
currentDeck: {
|
||||
name: "Default::child",
|
||||
configId: 1618570764780n,
|
||||
parentConfigIds: [1n],
|
||||
},
|
||||
defaults: {
|
||||
config: {
|
||||
@ -220,27 +219,6 @@ test("duplicate name", () => {
|
||||
expect(get(state.configList).find((e) => e.current)?.name).toMatch(/Default\d+$/);
|
||||
});
|
||||
|
||||
test("parent counts", () => {
|
||||
const state = startingState();
|
||||
|
||||
expect(get(state.parentLimits)).toStrictEqual({ newCards: 10, reviews: 200 });
|
||||
|
||||
// adjusting the current deck config won't alter parent
|
||||
state.currentConfig.update((c) => {
|
||||
c.newPerDay = 123;
|
||||
return c;
|
||||
});
|
||||
expect(get(state.parentLimits)).toStrictEqual({ newCards: 10, reviews: 200 });
|
||||
|
||||
// but adjusting the default config will, since the parent deck uses it
|
||||
state.setCurrentIndex(1);
|
||||
state.currentConfig.update((c) => {
|
||||
c.newPerDay = 123;
|
||||
return c;
|
||||
});
|
||||
expect(get(state.parentLimits)).toStrictEqual({ newCards: 123, reviews: 200 });
|
||||
});
|
||||
|
||||
test("saving", () => {
|
||||
let state = startingState();
|
||||
let out = state.dataForSaving(false);
|
||||
|
@ -23,11 +23,6 @@ export interface ConfigWithCount {
|
||||
useCount: number;
|
||||
}
|
||||
|
||||
export interface ParentLimits {
|
||||
newCards: number;
|
||||
reviews: number;
|
||||
}
|
||||
|
||||
/** Info for showing the top selector */
|
||||
export interface ConfigListEntry {
|
||||
idx: number;
|
||||
@ -40,13 +35,11 @@ export class DeckOptionsState {
|
||||
readonly currentConfig: Writable<DeckConfig_Config>;
|
||||
readonly currentAuxData: Writable<Record<string, unknown>>;
|
||||
readonly configList: Readable<ConfigListEntry[]>;
|
||||
readonly parentLimits: Readable<ParentLimits>;
|
||||
readonly cardStateCustomizer: Writable<string>;
|
||||
readonly currentDeck: DeckConfigsForUpdate_CurrentDeck;
|
||||
readonly deckLimits: Writable<DeckConfigsForUpdate_CurrentDeck_Limits>;
|
||||
readonly defaults: DeckConfig_Config;
|
||||
readonly addonComponents: Writable<DynamicSvelteComponent[]>;
|
||||
readonly v3Scheduler: boolean;
|
||||
readonly newCardsIgnoreReviewLimit: Writable<boolean>;
|
||||
readonly fsrs: Writable<boolean>;
|
||||
readonly currentPresetName: Writable<string>;
|
||||
@ -55,7 +48,6 @@ export class DeckOptionsState {
|
||||
private configs: ConfigWithCount[];
|
||||
private selectedIdx: number;
|
||||
private configListSetter!: (val: ConfigListEntry[]) => void;
|
||||
private parentLimitsSetter!: (val: ParentLimits) => void;
|
||||
private modifiedConfigs: Set<DeckOptionsId> = new Set();
|
||||
private removedConfigs: DeckOptionsId[] = [];
|
||||
private schemaModified: boolean;
|
||||
@ -77,7 +69,6 @@ export class DeckOptionsState {
|
||||
this.configs.findIndex((c) => c.config.id === this.currentDeck.configId),
|
||||
);
|
||||
this.sortConfigs();
|
||||
this.v3Scheduler = data.v3Scheduler;
|
||||
this.cardStateCustomizer = writable(data.cardStateCustomizer);
|
||||
this.deckLimits = writable(data.currentDeck?.limits ?? createLimits());
|
||||
this.newCardsIgnoreReviewLimit = writable(data.newCardsIgnoreReviewLimit);
|
||||
@ -93,17 +84,12 @@ export class DeckOptionsState {
|
||||
this.configListSetter = set;
|
||||
return;
|
||||
});
|
||||
this.parentLimits = readable(this.getParentLimits(), (set) => {
|
||||
this.parentLimitsSetter = set;
|
||||
return;
|
||||
});
|
||||
this.schemaModified = data.schemaModified;
|
||||
this.addonComponents = writable([]);
|
||||
|
||||
// create a temporary subscription to force our setters to be set immediately,
|
||||
// so unit tests don't get stale results
|
||||
get(this.configList);
|
||||
get(this.parentLimits);
|
||||
|
||||
// update our state when the current config is changed
|
||||
this.currentConfig.subscribe((val) => this.onCurrentConfigChanged(val));
|
||||
@ -227,7 +213,6 @@ export class DeckOptionsState {
|
||||
this.modifiedConfigs.add(configOuter.id);
|
||||
}
|
||||
}
|
||||
this.parentLimitsSetter?.(this.getParentLimits());
|
||||
}
|
||||
|
||||
private onCurrentAuxDataChanged(data: Record<string, unknown>): void {
|
||||
@ -253,7 +238,6 @@ export class DeckOptionsState {
|
||||
private updateCurrentConfig(): void {
|
||||
this.currentConfig.set(this.getCurrentConfig());
|
||||
this.currentAuxData.set(this.getCurrentAuxData());
|
||||
this.parentLimitsSetter?.(this.getParentLimits());
|
||||
}
|
||||
|
||||
private updateConfigList(): void {
|
||||
@ -292,22 +276,6 @@ export class DeckOptionsState {
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
private getParentLimits(): ParentLimits {
|
||||
const parentConfigs = this.configs.filter((c) => this.currentDeck.parentConfigIds.includes(c.config.id));
|
||||
const newCards = parentConfigs.reduce(
|
||||
(previous, current) => Math.min(previous, current.config.config?.newPerDay ?? 0),
|
||||
2 ** 31,
|
||||
);
|
||||
const reviews = parentConfigs.reduce(
|
||||
(previous, current) => Math.min(previous, current.config.config?.reviewsPerDay ?? 0),
|
||||
2 ** 31,
|
||||
);
|
||||
return {
|
||||
newCards,
|
||||
reviews,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function bytesToObject(bytes: Uint8Array): Record<string, unknown> {
|
||||
|
Loading…
Reference in New Issue
Block a user