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:
Abdo 2023-10-14 03:50:59 +03:00 committed by GitHub
parent e1e0f2e1bd
commit 5cde4b6941
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 230 additions and 775 deletions

@ -1 +1 @@
Subproject commit efd2e6edaf1e2b8e1d52c45ebd09d67ac035050f Subproject commit 3d820846d847f8ed866971e3a4304c715325d01b

@ -1 +1 @@
Subproject commit 41663c8bd0f6386ee98de03ca2c3e668c4f6194d Subproject commit ef4f4ffee68a3d3ebb2458b8b55bfdffa9a21c4b

View file

@ -44,6 +44,7 @@ message BackendError {
ANKIDROID_PANIC_ERROR = 19; ANKIDROID_PANIC_ERROR = 19;
// Originated from and usually specific to the OS. // Originated from and usually specific to the OS.
OS_ERROR = 20; OS_ERROR = 20;
SCHEDULER_UPGRADE_REQUIRED = 21;
} }
// error description, usually localized, suitable for displaying to the user // error description, usually localized, suitable for displaying to the user

View file

@ -100,9 +100,6 @@ message Preferences {
NEW_FIRST = 2; NEW_FIRST = 2;
} }
// read only; 1-3
uint32 scheduler_version = 1;
uint32 rollover = 2; uint32 rollover = 2;
uint32 learn_ahead_secs = 3; uint32 learn_ahead_secs = 3;
NewReviewMix new_review_mix = 4; NewReviewMix new_review_mix = 4;

View file

@ -180,7 +180,6 @@ message DeckConfigsForUpdate {
CurrentDeck current_deck = 2; CurrentDeck current_deck = 2;
DeckConfig defaults = 3; DeckConfig defaults = 3;
bool schema_modified = 4; bool schema_modified = 4;
bool v3_scheduler = 5;
// only applies to v3 scheduler // only applies to v3 scheduler
string card_state_customizer = 6; string card_state_customizer = 6;
// only applies to v3 scheduler // only applies to v3 scheduler

View file

@ -33,6 +33,7 @@ from .errors import (
InvalidInput, InvalidInput,
NetworkError, NetworkError,
NotFoundError, NotFoundError,
SchedulerUpgradeRequired,
SearchError, SearchError,
SyncError, SyncError,
SyncErrorKind, SyncErrorKind,
@ -240,6 +241,9 @@ def backend_exception_to_pylib(err: backend_pb2.BackendError) -> Exception:
elif val == kind.CUSTOM_STUDY_ERROR: elif val == kind.CUSTOM_STUDY_ERROR:
return CustomStudyError(err.message, help_page, context, backtrace) return CustomStudyError(err.message, help_page, context, backtrace)
elif val == kind.SCHEDULER_UPGRADE_REQUIRED:
return SchedulerUpgradeRequired(err.message, help_page, context, backtrace)
else: else:
# sadly we can't do exhaustiveness checking on protobuf enums # sadly we can't do exhaustiveness checking on protobuf enums
# assert_exhaustive(val) # assert_exhaustive(val)

View file

@ -3,7 +3,7 @@
from __future__ import annotations 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 ( from anki import (
ankiweb_pb2, ankiweb_pb2,
@ -56,13 +56,12 @@ MediaSyncStatus = sync_pb2.MediaSyncStatusResponse
FsrsItem = scheduler_pb2.FsrsItem FsrsItem = scheduler_pb2.FsrsItem
FsrsReview = scheduler_pb2.FsrsReview FsrsReview = scheduler_pb2.FsrsReview
import copy
import os import os
import sys import sys
import time import time
import traceback import traceback
import weakref import weakref
from dataclasses import dataclass, field from dataclasses import dataclass
import anki.latex import anki.latex
from anki import hooks from anki import hooks
@ -98,18 +97,12 @@ anki.latex.setup_hook()
SearchJoiner = Literal["AND", "OR"] SearchJoiner = Literal["AND", "OR"]
@dataclass
class LegacyReviewUndo:
card: Card
was_leech: bool
@dataclass @dataclass
class LegacyCheckpoint: class LegacyCheckpoint:
name: str name: str
LegacyUndoResult = Union[None, LegacyCheckpoint, LegacyReviewUndo] LegacyUndoResult = Optional[LegacyCheckpoint]
@dataclass @dataclass
@ -1075,9 +1068,7 @@ class Collection(DeprecatedNamesMixin):
if not self._undo: if not self._undo:
return UndoStatus() return UndoStatus()
if isinstance(self._undo, _ReviewsUndo): if isinstance(self._undo, LegacyCheckpoint):
return UndoStatus(undo=self.tr.scheduling_review())
elif isinstance(self._undo, LegacyCheckpoint):
return UndoStatus(undo=self._undo.name) return UndoStatus(undo=self._undo.name)
else: else:
assert_exhaustive(self._undo) assert_exhaustive(self._undo)
@ -1131,9 +1122,7 @@ class Collection(DeprecatedNamesMixin):
def undo_legacy(self) -> LegacyUndoResult: def undo_legacy(self) -> LegacyUndoResult:
"Returns None if the legacy undo queue is empty." "Returns None if the legacy undo queue is empty."
if isinstance(self._undo, _ReviewsUndo): if isinstance(self._undo, LegacyCheckpoint):
return self._undo_review()
elif isinstance(self._undo, LegacyCheckpoint):
return self._undo_checkpoint() return self._undo_checkpoint()
elif self._undo is None: elif self._undo is None:
return None return None
@ -1158,15 +1147,6 @@ class Collection(DeprecatedNamesMixin):
else: else:
return None 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: def _have_outstanding_checkpoint(self) -> bool:
self._check_backend_undo_status() self._check_backend_undo_status()
return isinstance(self._undo, LegacyCheckpoint) return isinstance(self._undo, LegacyCheckpoint)
@ -1184,59 +1164,8 @@ class Collection(DeprecatedNamesMixin):
if name: if name:
self._undo = LegacyCheckpoint(name=name) self._undo = LegacyCheckpoint(name=name)
else: 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() 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 # DB maintenance
########################################################################## ##########################################################################
@ -1444,7 +1373,6 @@ class Collection(DeprecatedNamesMixin):
Collection.register_deprecated_aliases( Collection.register_deprecated_aliases(
clearUndo=Collection.clear_python_undo, clearUndo=Collection.clear_python_undo,
markReview=Collection.save_card_review_undo_info,
findReplace=Collection.find_and_replace, findReplace=Collection.find_and_replace,
remCards=Collection.remove_cards_and_orphaned_notes, remCards=Collection.remove_cards_and_orphaned_notes,
) )
@ -1453,13 +1381,7 @@ Collection.register_deprecated_aliases(
# legacy name # legacy name
_Collection = Collection _Collection = Collection
_UndoInfo = Union[LegacyCheckpoint, None]
@dataclass
class _ReviewsUndo:
entries: list[LegacyReviewUndo] = field(default_factory=list)
_UndoInfo = Union[_ReviewsUndo, LegacyCheckpoint, None]
def pb_export_limit(limit: ExportLimit) -> import_export_pb2.ExportLimit: def pb_export_limit(limit: ExportLimit) -> import_export_pb2.ExportLimit:

View file

@ -119,6 +119,10 @@ class SearchError(BackendError):
pass pass
class SchedulerUpgradeRequired(BackendError):
pass
class AbortSchemaModification(AnkiException): class AbortSchemaModification(AnkiException):
pass pass

View file

@ -197,9 +197,6 @@ class AnkiExporter(Exporter):
return [] return []
def exportInto(self, path: str) -> None: 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 # create a new collection at the target
try: try:
os.unlink(path) os.unlink(path)
@ -352,13 +349,10 @@ class AnkiPackageExporter(AnkiExporter):
# export into the anki2 file # export into the anki2 file
colfile = path.replace(".apkg", ".anki2") colfile = path.replace(".apkg", ".anki2")
AnkiExporter.exportInto(self, colfile) AnkiExporter.exportInto(self, colfile)
if not self._v2sched: # prevent older clients from accessing
z.write(colfile, "collection.anki2") # pylint: disable=unreachable
else: self._addDummyCollection(z)
# prevent older clients from accessing z.write(colfile, "collection.anki21")
# pylint: disable=unreachable
self._addDummyCollection(z)
z.write(colfile, "collection.anki21")
# and media # and media
self.prepareMedia() self.prepareMedia()

View file

@ -60,7 +60,7 @@ class Anki2Importer(Importer):
self.dst = self.col self.dst = self.col
self.src = Collection(self.file) self.src = Collection(self.file)
if not self._importing_v2 and self.col.sched_ver() != 1: if not self._importing_v2:
# any scheduling included? # any scheduling included?
if self.src.db.scalar("select 1 from cards where queue != 0 limit 1"): if self.src.db.scalar("select 1 from cards where queue != 0 limit 1"):
self.source_needs_upgrade = True self.source_needs_upgrade = True

View file

@ -5,7 +5,6 @@
from __future__ import annotations from __future__ import annotations
import datetime
import json import json
import random import random
import time import time
@ -691,8 +690,7 @@ select count(), avg(ivl), max(ivl) from cards where did in %s and queue = {QUEUE
[13, 3], [13, 3],
[14, 4], [14, 4],
] ]
if self.col.sched_ver() != 1: ticks.insert(3, [4, 4])
ticks.insert(3, [4, 4])
txt = self._title( txt = self._title(
"Answer Buttons", "The number of times you have pressed each button." "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) lim = "where " + " and ".join(lims)
else: else:
lim = "" lim = ""
if self.col.sched_ver() == 1: ease4repl = "ease"
ease4repl = "3"
else:
ease4repl = "ease"
return self.col.db.all( return self.col.db.all(
f""" f"""
select (case select (case
@ -841,11 +836,7 @@ order by thetype, ease"""
lim = self._revlogLimit() lim = self._revlogLimit()
if lim: if lim:
lim = " and " + lim lim = " and " + lim
if self.col.sched_ver() == 1: rolloverHour = self.col.conf.get("rollover", 4)
sd = datetime.datetime.fromtimestamp(self.col.crt)
rolloverHour = sd.hour
else:
rolloverHour = self.col.conf.get("rollover", 4)
pd = self._periodDays() pd = self._periodDays()
if pd: if pd:
lim += " and id > %d" % ((self.col.sched.day_cutoff - (86400 * pd)) * 1000) lim += " and id > %d" % ((self.col.sched.day_cutoff - (86400 * pd)) * 1000)

View file

@ -395,13 +395,7 @@ def test_reviews():
c = copy.copy(cardcopy) c = copy.copy(cardcopy)
c.lapses = 7 c.lapses = 7
c.flush() c.flush()
# setup hook
hooked = []
def onLeech(card):
hooked.append(1)
hooks.card_did_leech.append(onLeech)
col.sched.answerCard(c, 1) col.sched.answerCard(c, 1)
assert c.queue == QUEUE_TYPE_SUSPENDED assert c.queue == QUEUE_TYPE_SUSPENDED
c.load() c.load()

View file

@ -18,12 +18,6 @@ from hookslib import Hook, write_file
###################################################################### ######################################################################
hooks = [ 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="card_odue_was_invalid"),
Hook(name="schema_will_change", args=["proceed: bool"], return_type="bool"), Hook(name="schema_will_change", args=["proceed: bool"], return_type="bool"),
Hook( Hook(
@ -98,24 +92,6 @@ hooks = [
], ],
doc="Can modify the resulting text after rendering completes.", 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( Hook(
name="importing_importers", name="importing_importers",
args=["importers: list[tuple[str, Any]]"], args=["importers: list[tuple[str, Any]]"],

View file

@ -729,7 +729,7 @@ class Browser(QMainWindow):
def createFilteredDeck(self) -> None: def createFilteredDeck(self) -> None:
search = self.current_search() 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) aqt.dialogs.open("FilteredDeckConfigDialog", self.mw, search_2=search)
else: else:
aqt.dialogs.open("FilteredDeckConfigDialog", self.mw, search=search) aqt.dialogs.open("FilteredDeckConfigDialog", self.mw, search=search)

View file

@ -219,9 +219,6 @@ class DeckConf(QDialog):
f.revplim.setText(self.parentLimText("rev")) f.revplim.setText(self.parentLimText("rev"))
f.buryRev.setChecked(c.get("bury", True)) f.buryRev.setChecked(c.get("bury", True))
f.hardFactor.setValue(int(c.get("hardFactor", 1.2) * 100)) 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 # lapse
c = self.conf["lapse"] c = self.conf["lapse"]
f.lapSteps.setText(self.listToUser(c["delays"])) f.lapSteps.setText(self.listToUser(c["delays"]))

View file

@ -106,7 +106,7 @@ def display_options_for_deck_id(deck_id: DeckId) -> None:
def display_options_for_deck(deck: DeckDict) -> None: def display_options_for_deck(deck: DeckDict) -> None:
if not deck["dyn"]: 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"])) deck_legacy = aqt.mw.col.decks.get(DeckId(deck["id"]))
aqt.deckconf.DeckConf(aqt.mw, deck_legacy) aqt.deckconf.DeckConf(aqt.mw, deck_legacy)
else: else:

View file

@ -98,8 +98,6 @@ class FilteredDeckConfigDialog(QDialog):
self.form.buttonBox.helpRequested, lambda: openHelp(HelpPage.FILTERED_DECK) 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) restoreGeom(self, self.GEOMETRY_KEY)
def load_deck_and_show(self, deck: FilteredDeckForUpdate) -> None: def load_deck_and_show(self, deck: FilteredDeckForUpdate) -> None:
@ -132,13 +130,8 @@ class FilteredDeckConfigDialog(QDialog):
form.order.setCurrentIndex(term1.order) form.order.setCurrentIndex(term1.order)
form.limit.setValue(term1.limit) form.limit.setValue(term1.limit)
if self.col.sched_ver() == 1: form.steps.setVisible(False)
if config.delays: form.stepsOn.setVisible(False)
form.steps.setText(self.listToUser(list(config.delays)))
form.stepsOn.setChecked(True)
else:
form.steps.setVisible(False)
form.stepsOn.setVisible(False)
form.previewDelay.setValue(config.preview_delay) form.previewDelay.setValue(config.preview_delay)
@ -209,7 +202,6 @@ class FilteredDeckConfigDialog(QDialog):
implicit_filters = ( implicit_filters = (
SearchNode(card_state=SearchNode.CARD_STATE_SUSPENDED), SearchNode(card_state=SearchNode.CARD_STATE_SUSPENDED),
SearchNode(card_state=SearchNode.CARD_STATE_BURIED), SearchNode(card_state=SearchNode.CARD_STATE_BURIED),
*self._learning_search_node(),
*self._filtered_search_node(), *self._filtered_search_node(),
) )
manual_filter = self.col.group_searches(*manual_filters, joiner="OR") manual_filter = self.col.group_searches(*manual_filters, joiner="OR")
@ -226,21 +218,6 @@ class FilteredDeckConfigDialog(QDialog):
return (self.form.search_2.text(),) return (self.form.search_2.text(),)
return () 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]: def _filtered_search_node(self) -> tuple[SearchNode]:
"""Return a search node that matches cards in filtered decks, if applicable excluding those """Return a search node that matches cards in filtered decks, if applicable excluding those
in the deck being rebuild.""" in the deck being rebuild."""
@ -254,9 +231,7 @@ class FilteredDeckConfigDialog(QDialog):
return (SearchNode(deck="filtered"),) return (SearchNode(deck="filtered"),)
def _onReschedToggled(self, _state: int) -> None: def _onReschedToggled(self, _state: int) -> None:
self.form.previewDelayWidget.setVisible( self.form.previewDelayWidget.setVisible(not self.form.resched.isChecked())
not self.form.resched.isChecked() and self.col.sched_ver() > 1
)
def _update_deck(self) -> bool: def _update_deck(self) -> bool:
"""Update our stored deck with the details from the GUI. """Update our stored deck with the details from the GUI.
@ -269,11 +244,6 @@ class FilteredDeckConfigDialog(QDialog):
config.reschedule = form.resched.isChecked() config.reschedule = form.resched.isChecked()
del config.delays[:] 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 = [ terms = [
FilteredDeckConfig.SearchTerm( FilteredDeckConfig.SearchTerm(
search=form.search.text(), search=form.search.text(),

View file

@ -3,13 +3,7 @@
from __future__ import annotations from __future__ import annotations
from anki.collection import ( from anki.collection import LegacyCheckpoint, OpChanges, OpChangesAfterUndo, Preferences
LegacyCheckpoint,
LegacyReviewUndo,
OpChanges,
OpChangesAfterUndo,
Preferences,
)
from anki.errors import UndoEmpty from anki.errors import UndoEmpty
from anki.types import assert_exhaustive from anki.types import assert_exhaustive
from aqt import gui_hooks from aqt import gui_hooks
@ -27,8 +21,7 @@ def undo(*, parent: QWidget) -> None:
def on_failure(exc: Exception) -> None: def on_failure(exc: Exception) -> None:
if isinstance(exc, UndoEmpty): if isinstance(exc, UndoEmpty):
# backend has no undo, but there may be a checkpoint # backend has no undo, but there may be a checkpoint waiting
# or v1/v2 review waiting
_legacy_undo(parent=parent) _legacy_undo(parent=parent)
else: else:
showWarning(str(exc), parent=parent) showWarning(str(exc), parent=parent)
@ -53,9 +46,6 @@ def _legacy_undo(*, parent: QWidget) -> None:
assert mw assert mw
assert mw.col assert mw.col
reviewing = mw.state == "review"
just_refresh_reviewer = False
result = mw.col.undo_legacy() result = mw.col.undo_legacy()
if result is None: if result is None:
@ -64,19 +54,6 @@ def _legacy_undo(*, parent: QWidget) -> None:
mw.update_undo_actions() mw.update_undo_actions()
return 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): elif isinstance(result, LegacyCheckpoint):
name = result.name name = result.name
@ -84,11 +61,8 @@ def _legacy_undo(*, parent: QWidget) -> None:
assert_exhaustive(result) assert_exhaustive(result)
assert False assert False
if just_refresh_reviewer: # full queue+gui reset required
mw.reviewer.nextCard() mw.reset()
else:
# full queue+gui reset required
mw.reset()
tooltip(tr.undo_action_undone(action=name), parent=parent) tooltip(tr.undo_action_undone(action=name), parent=parent)
gui_hooks.state_did_revert(name) gui_hooks.state_did_revert(name)

View file

@ -150,25 +150,24 @@ class Overview:
def on_unbury(self) -> None: def on_unbury(self) -> None:
mode = UnburyDeck.Mode.ALL mode = UnburyDeck.Mode.ALL
if self.mw.col.sched_ver() != 1: info = self.mw.col.sched.congratulations_info()
info = self.mw.col.sched.congratulations_info() if info.have_sched_buried and info.have_user_buried:
if info.have_sched_buried and info.have_user_buried: opts = [
opts = [ tr.studying_manually_buried_cards(),
tr.studying_manually_buried_cards(), tr.studying_buried_siblings(),
tr.studying_buried_siblings(), tr.studying_all_buried_cards(),
tr.studying_all_buried_cards(), tr.actions_cancel(),
tr.actions_cancel(), ]
]
diag = askUserDialog(tr.studying_what_would_you_like_to_unbury(), opts) diag = askUserDialog(tr.studying_what_would_you_like_to_unbury(), opts)
diag.setDefault(0) diag.setDefault(0)
ret = diag.run() ret = diag.run()
if ret == opts[0]: if ret == opts[0]:
mode = UnburyDeck.Mode.USER_ONLY mode = UnburyDeck.Mode.USER_ONLY
elif ret == opts[1]: elif ret == opts[1]:
mode = UnburyDeck.Mode.SCHED_ONLY mode = UnburyDeck.Mode.SCHED_ONLY
elif ret == opts[3]: elif ret == opts[3]:
return return
unbury_deck( unbury_deck(
parent=self.mw, deck_id=self.mw.col.decks.get_current_id(), mode=mode parent=self.mw, deck_id=self.mw.col.decks.get_current_id(), mode=mode

View file

@ -14,7 +14,6 @@ from typing import Any, Literal, Match, Sequence, cast
import aqt import aqt
import aqt.browser import aqt.browser
import aqt.operations import aqt.operations
from anki import hooks
from anki.cards import Card, CardId from anki.cards import Card, CardId
from anki.collection import Config, OpChanges, OpChangesWithCount from anki.collection import Config, OpChanges, OpChangesWithCount
from anki.scheduler.base import ScheduleCardsAsNew from anki.scheduler.base import ScheduleCardsAsNew
@ -135,9 +134,7 @@ class Reviewer:
self.mw = mw self.mw = mw
self.web = mw.web self.web = mw.web
self.card: Card | None = None self.card: Card | None = None
self.cardQueue: list[Card] = []
self.previous_card: Card | None = None self.previous_card: Card | None = None
self.hadCardQueue = False
self._answeredIds: list[CardId] = [] self._answeredIds: list[CardId] = []
self._recordedAudio: str | None = None self._recordedAudio: str | None = None
self.typeCorrect: str = None # web init happens before this is set 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._previous_card_info = PreviousReviewerCardInfo(self.mw)
self._states_mutated = True self._states_mutated = True
self._reps: int = None self._reps: int = None
hooks.card_did_leech.append(self.onLeech)
def show(self) -> None: def show(self) -> None:
if self.mw.col.sched_ver() == 1 or not self.mw.col.v3_scheduler(): 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.previous_card = self.card
self.card = None self.card = None
self._v3 = None self._v3 = None
self._get_next_v3_card()
if self.mw.col.sched.version < 3:
self._get_next_v1_v2_card()
else:
self._get_next_v3_card()
self._previous_card_info.set_card(self.previous_card) self._previous_card_info.set_card(self.previous_card)
self._card_info.set_card(self.card) self._card_info.set_card(self.card)
@ -247,21 +239,6 @@ class Reviewer:
self._showQuestion() 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: def _get_next_v3_card(self) -> None:
assert isinstance(self.mw.col.sched, V3Scheduler) assert isinstance(self.mw.col.sched, V3Scheduler)
output = self.mw.col.sched.get_queued_cards() 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 = Card(self.mw.col, backend_card=self._v3.top_card().card)
self.card.start_timer() self.card.start_timer()
def get_scheduling_states(self) -> SchedulingStates | None: def get_scheduling_states(self) -> SchedulingStates:
if v3 := self._v3: return self._v3.states
return v3.states
return None
def get_scheduling_context(self) -> SchedulingContext | None: def get_scheduling_context(self) -> SchedulingContext:
if v3 := self._v3: return self._v3.context
return v3.context
return None
def set_scheduling_states(self, request: SetSchedulingStatesRequest) -> None: def set_scheduling_states(self, request: SetSchedulingStatesRequest) -> None:
if request.key != self._state_mutation_key: if request.key != self._state_mutation_key:
return return
if v3 := self._v3: self._v3.states = request.states
v3.states = request.states
def _run_state_mutation_hook(self) -> None: def _run_state_mutation_hook(self) -> None:
def on_eval(result: Any) -> None: def on_eval(result: Any) -> None:
@ -294,7 +266,7 @@ class Reviewer:
# eval failed, usually a syntax error # eval failed, usually a syntax error
self._states_mutated = True self._states_mutated = True
if self._v3 and (js := self._state_mutation_js): if js := self._state_mutation_js:
self._states_mutated = False self._states_mutated = False
self.web.evalWithCallback( self.web.evalWithCallback(
RUN_STATE_MUTATION.format(key=self._state_mutation_key, js=js), RUN_STATE_MUTATION.format(key=self._state_mutation_key, js=js),
@ -450,27 +422,24 @@ class Reviewer:
if not proceed: if not proceed:
return return
if (v3 := self._v3) and (sched := cast(V3Scheduler, self.mw.col.sched)): sched = cast(V3Scheduler, self.mw.col.sched)
answer = sched.build_answer( answer = sched.build_answer(
card=self.card, card=self.card,
states=v3.states, states=self._v3.states,
rating=v3.rating_from_ease(ease), rating=self._v3.rating_from_ease(ease),
) )
def after_answer(changes: OpChanges) -> None: def after_answer(changes: OpChanges) -> None:
if gui_hooks.reviewer_did_answer_card.count() > 0: if gui_hooks.reviewer_did_answer_card.count() > 0:
self.card.load() 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)
self._after_answering(ease) 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: def _after_answering(self, ease: Literal[1, 2, 3, 4]) -> None:
gui_hooks.reviewer_did_answer_card(self, self.card, ease) gui_hooks.reviewer_did_answer_card(self, self.card, ease)
@ -792,11 +761,8 @@ timerStopped = false;
def _answerButtons(self) -> str: def _answerButtons(self) -> str:
default = self._defaultEase() default = self._defaultEase()
if v3 := self._v3: assert isinstance(self.mw.col.sched, V3Scheduler)
assert isinstance(self.mw.col.sched, V3Scheduler) labels = self.mw.col.sched.describe_next_states(self._v3.states)
labels = self.mw.col.sched.describe_next_states(v3.states)
else:
labels = None
def but(i: int, label: str) -> str: def but(i: int, label: str) -> str:
if i == default: if i == default:

View file

@ -823,7 +823,6 @@ gui_hooks.webview_did_inject_style_into_page.append(mytest)
legacy_hook="colLoading", legacy_hook="colLoading",
), ),
Hook(name="undo_state_did_change", args=["info: UndoActionsInfo"]), Hook(name="undo_state_did_change", args=["info: UndoActionsInfo"]),
Hook(name="review_did_undo", args=["card_id: int"], legacy_hook="revertedCard"),
Hook( Hook(
name="style_did_init", name="style_did_init",
args=["style: str"], args=["style: str"],

View file

@ -46,6 +46,7 @@ impl AnkiError {
| AnkiError::FsrsInsufficientData => Kind::InvalidInput, | AnkiError::FsrsInsufficientData => Kind::InvalidInput,
#[cfg(windows)] #[cfg(windows)]
AnkiError::WindowsError { .. } => Kind::OsError, AnkiError::WindowsError { .. } => Kind::OsError,
AnkiError::SchedulerUpgradeRequired => Kind::SchedulerUpgradeRequired,
}; };
anki_proto::backend::BackendError { anki_proto::backend::BackendError {

View file

@ -184,8 +184,8 @@ impl Card {
} }
/// Caller must ensure provided deck exists and is not filtered. /// Caller must ensure provided deck exists and is not filtered.
fn set_deck(&mut self, deck: DeckId, sched: SchedulerVersion) { fn set_deck(&mut self, deck: DeckId) {
self.remove_from_filtered_deck_restoring_queue(sched); self.remove_from_filtered_deck_restoring_queue();
self.deck_id = deck; self.deck_id = deck;
} }
@ -342,13 +342,16 @@ impl Collection {
} }
pub fn set_deck(&mut self, cards: &[CardId], deck_id: DeckId) -> Result<OpOutput<usize>> { 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 deck = self.get_deck(deck_id)?.or_not_found(deck_id)?;
let config_id = deck.config_id().ok_or(AnkiError::FilteredDeckError { let config_id = deck.config_id().ok_or(AnkiError::FilteredDeckError {
source: FilteredDeckError::CanNotMoveCardsInto, source: FilteredDeckError::CanNotMoveCardsInto,
})?; })?;
let config = self.get_deck_config(config_id, true)?.unwrap(); let config = self.get_deck_config(config_id, true)?.unwrap();
let mut steps_adjuster = RemainingStepsAdjuster::new(&config); let mut steps_adjuster = RemainingStepsAdjuster::new(&config);
let sched = self.scheduler_version();
let usn = self.usn()?; let usn = self.usn()?;
self.transact(Op::SetCardDeck, |col| { self.transact(Op::SetCardDeck, |col| {
let mut count = 0; let mut count = 0;
@ -359,7 +362,7 @@ impl Collection {
count += 1; count += 1;
let original = card.clone(); let original = card.clone();
steps_adjuster.adjust_remaining_steps(col, &mut card)?; 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)?; col.update_card_inner(&mut card, original, usn)?;
} }
Ok(count) Ok(count)

View file

@ -50,7 +50,6 @@ impl Collection {
.storage .storage
.get_collection_timestamps()? .get_collection_timestamps()?
.schema_changed_since_sync(), .schema_changed_since_sync(),
v3_scheduler: self.get_config_bool(BoolKey::Sched2021),
card_state_customizer: self.get_config_string(StringKey::CardStateCustomizer), card_state_customizer: self.get_config_string(StringKey::CardStateCustomizer),
new_cards_ignore_review_limit: self.get_config_bool(BoolKey::NewCardsIgnoreReviewLimit), new_cards_ignore_review_limit: self.get_config_bool(BoolKey::NewCardsIgnoreReviewLimit),
fsrs: self.get_config_bool(BoolKey::Fsrs), fsrs: self.get_config_bool(BoolKey::Fsrs),

View file

@ -35,8 +35,7 @@ impl Collection {
days_elapsed: u32, days_elapsed: u32,
learn_cutoff: u32, learn_cutoff: u32,
) -> Result<HashMap<DeckId, DueCounts>> { ) -> Result<HashMap<DeckId, DueCounts>> {
self.storage self.storage.due_counts(days_elapsed, learn_cutoff)
.due_counts(self.scheduler_version(), days_elapsed, learn_cutoff)
} }
pub(crate) fn counts_for_deck_today( pub(crate) fn counts_for_deck_today(

View file

@ -62,7 +62,6 @@ impl RemainingLimits {
deck: &Deck, deck: &Deck,
config: Option<&DeckConfig>, config: Option<&DeckConfig>,
today: u32, today: u32,
v3: bool,
new_cards_ignore_review_limit: bool, new_cards_ignore_review_limit: bool,
) -> Self { ) -> Self {
if let Ok(normal) = deck.normal() { if let Ok(normal) = deck.normal() {
@ -70,7 +69,6 @@ impl RemainingLimits {
return Self::new_for_normal_deck( return Self::new_for_normal_deck(
deck, deck,
today, today,
v3,
new_cards_ignore_review_limit, new_cards_ignore_review_limit,
normal, normal,
config, config,
@ -83,28 +81,11 @@ impl RemainingLimits {
fn new_for_normal_deck( fn new_for_normal_deck(
deck: &Deck, deck: &Deck,
today: u32, today: u32,
v3: bool,
new_cards_ignore_review_limit: bool, new_cards_ignore_review_limit: bool,
normal: &NormalDeck, normal: &NormalDeck,
config: &DeckConfig, config: &DeckConfig,
) -> RemainingLimits { ) -> RemainingLimits {
if v3 { Self::new_for_normal_deck_v3(deck, today, new_cards_ignore_review_limit, normal, config)
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,
}
} }
fn new_for_normal_deck_v3( fn new_for_normal_deck_v3(
@ -189,7 +170,6 @@ pub(crate) fn remaining_limits_map<'a>(
decks: impl Iterator<Item = &'a Deck>, decks: impl Iterator<Item = &'a Deck>,
config: &'a HashMap<DeckConfigId, DeckConfig>, config: &'a HashMap<DeckConfigId, DeckConfig>,
today: u32, today: u32,
v3: bool,
new_cards_ignore_review_limit: bool, new_cards_ignore_review_limit: bool,
) -> HashMap<DeckId, RemainingLimits> { ) -> HashMap<DeckId, RemainingLimits> {
decks decks
@ -200,7 +180,6 @@ pub(crate) fn remaining_limits_map<'a>(
deck, deck,
deck.config_id().and_then(|id| config.get(&id)), deck.config_id().and_then(|id| config.get(&id)),
today, today,
v3,
new_cards_ignore_review_limit, new_cards_ignore_review_limit,
), ),
) )
@ -231,7 +210,6 @@ impl NodeLimits {
deck, deck,
deck.config_id().and_then(|id| config.get(&id)), deck.config_id().and_then(|id| config.get(&id)),
today, today,
true,
new_cards_ignore_review_limit, new_cards_ignore_review_limit,
), ),
} }

View file

@ -14,7 +14,6 @@ use unicase::UniCase;
use super::limits::remaining_limits_map; use super::limits::remaining_limits_map;
use super::limits::RemainingLimits; use super::limits::RemainingLimits;
use super::DueCounts; use super::DueCounts;
use crate::config::SchedulerVersion;
use crate::ops::OpOutput; use crate::ops::OpOutput;
use crate::prelude::*; use crate::prelude::*;
use crate::undo::Op; 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. /// A temporary container used during count summation and limit application.
#[derive(Default, Clone)] #[derive(Default, Clone)]
struct NodeCountsV3 { struct NodeCountsV3 {
@ -325,8 +264,6 @@ impl Collection {
let timing_at_stamp = self.timing_for_timestamp(timestamp)?; let timing_at_stamp = self.timing_for_timestamp(timestamp)?;
let days_elapsed = timing_at_stamp.days_elapsed; let days_elapsed = timing_at_stamp.days_elapsed;
let learn_cutoff = (timestamp.0 as u32) + self.learn_ahead_secs(); 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 = let new_cards_ignore_review_limit =
self.get_config_bool(BoolKey::NewCardsIgnoreReviewLimit); self.get_config_bool(BoolKey::NewCardsIgnoreReviewLimit);
let counts = self.due_counts(days_elapsed, learn_cutoff)?; let counts = self.due_counts(days_elapsed, learn_cutoff)?;
@ -336,18 +273,9 @@ impl Collection {
decks_map.values(), decks_map.values(),
&dconf, &dconf,
days_elapsed, days_elapsed,
v3,
new_cards_ignore_review_limit, new_cards_ignore_review_limit,
); );
if sched_ver == SchedulerVersion::V2 { sum_counts_and_apply_limits_v3(&mut tree, &limits);
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());
}
} }
Ok(tree) Ok(tree)

View file

@ -115,6 +115,7 @@ pub enum AnkiError {
InvalidServiceIndex, InvalidServiceIndex,
FsrsWeightsInvalid, FsrsWeightsInvalid,
FsrsInsufficientData, FsrsInsufficientData,
SchedulerUpgradeRequired,
} }
// error helpers // error helpers
@ -168,6 +169,9 @@ impl AnkiError {
AnkiError::NotFound { source } => source.message(tr), AnkiError::NotFound { source } => source.message(tr),
AnkiError::FsrsInsufficientData => tr.deck_config_not_enough_history().into(), AnkiError::FsrsInsufficientData => tr.deck_config_not_enough_history().into(),
AnkiError::FsrsWeightsInvalid => tr.deck_config_invalid_weights().into(), AnkiError::FsrsWeightsInvalid => tr.deck_config_invalid_weights().into(),
AnkiError::SchedulerUpgradeRequired => {
tr.scheduling_update_required().replace("V2", "v3")
}
#[cfg(windows)] #[cfg(windows)]
AnkiError::WindowsError { source } => format!("{source:?}"), AnkiError::WindowsError { source } => format!("{source:?}"),
} }

View file

@ -89,6 +89,9 @@ impl Context<'_> {
remapped_templates, remapped_templates,
imported_decks, 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_cards(mem::take(&mut self.data.cards), keep_filtered)?;
ctx.import_revlog(mem::take(&mut self.data.revlog)) ctx.import_revlog(mem::take(&mut self.data.revlog))
} }
@ -136,7 +139,7 @@ impl CardContext<'_> {
self.remap_template_index(card); self.remap_template_index(card);
card.shift_collection_relative_dates(self.collection_delta); card.shift_collection_relative_dates(self.collection_delta);
if !keep_filtered { 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); let old_id = self.uniquify_card_id(card);
@ -196,11 +199,11 @@ impl Card {
self.ctype == CardType::Review 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() { if self.is_filtered() {
// instead of moving between decks, the deck is converted to a regular one // instead of moving between decks, the deck is converted to a regular one
self.original_deck_id = self.deck_id; self.original_deck_id = self.deck_id;
self.remove_from_filtered_deck_restoring_queue(version); self.remove_from_filtered_deck_restoring_queue();
} }
} }
} }

View file

@ -48,16 +48,6 @@ impl Collection {
pub fn get_scheduling_preferences(&self) -> Result<Scheduling> { pub fn get_scheduling_preferences(&self) -> Result<Scheduling> {
Ok(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, rollover: self.rollover_for_current_scheduler()? as u32,
learn_ahead_secs: self.learn_ahead_secs(), learn_ahead_secs: self.learn_ahead_secs(),
new_review_mix: match self.get_new_review_mix() { new_review_mix: match self.get_new_review_mix() {

View file

@ -4,7 +4,6 @@
use super::CardStateUpdater; use super::CardStateUpdater;
use super::RevlogEntryPartial; use super::RevlogEntryPartial;
use crate::card::CardQueue; use crate::card::CardQueue;
use crate::config::SchedulerVersion;
use crate::scheduler::states::CardState; use crate::scheduler::states::CardState;
use crate::scheduler::states::IntervalKind; use crate::scheduler::states::IntervalKind;
use crate::scheduler::states::PreviewState; use crate::scheduler::states::PreviewState;
@ -17,8 +16,7 @@ impl CardStateUpdater {
) -> RevlogEntryPartial { ) -> RevlogEntryPartial {
let revlog = RevlogEntryPartial::new(current, next.into(), 0.0, self.secs_until_rollover()); let revlog = RevlogEntryPartial::new(current, next.into(), 0.0, self.secs_until_rollover());
if next.finished { if next.finished {
self.card self.card.remove_from_filtered_deck_restoring_queue();
.remove_from_filtered_deck_restoring_queue(SchedulerVersion::V2);
return revlog; return revlog;
} }

View file

@ -90,17 +90,13 @@ impl Collection {
let mut count = 0; let mut count = 0;
let usn = self.usn()?; let usn = self.usn()?;
let sched = self.scheduler_version(); let sched = self.scheduler_version();
if sched == SchedulerVersion::V1 {
return Err(AnkiError::SchedulerUpgradeRequired);
}
let desired_queue = match mode { let desired_queue = match mode {
BuryOrSuspendMode::Suspend => CardQueue::Suspended, BuryOrSuspendMode::Suspend => CardQueue::Suspended,
BuryOrSuspendMode::BurySched => CardQueue::SchedBuried, BuryOrSuspendMode::BurySched => CardQueue::SchedBuried,
BuryOrSuspendMode::BuryUser => { BuryOrSuspendMode::BuryUser => CardQueue::UserBuried,
if sched == SchedulerVersion::V1 {
// v1 scheduler only had one bury type
CardQueue::SchedBuried
} else {
CardQueue::UserBuried
}
}
}; };
for original in cards { for original in cards {
@ -108,10 +104,6 @@ impl Collection {
if card.queue != desired_queue { if card.queue != desired_queue {
// do not bury suspended cards as that would unsuspend them // do not bury suspended cards as that would unsuspend them
if card.queue != CardQueue::Suspended { 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; card.queue = desired_queue;
count += 1; count += 1;
self.update_card_inner(&mut card, original, usn)?; self.update_card_inner(&mut card, original, usn)?;

View file

@ -4,7 +4,6 @@
use super::DeckFilterContext; use super::DeckFilterContext;
use crate::card::CardQueue; use crate::card::CardQueue;
use crate::card::CardType; use crate::card::CardType;
use crate::config::SchedulerVersion;
use crate::prelude::*; use crate::prelude::*;
impl Card { impl Card {
@ -37,26 +36,12 @@ impl Card {
self.original_due = self.due; self.original_due = self.due;
if ctx.scheduler == SchedulerVersion::V1 { // if rescheduling is disabled, all cards go in the review queue
if self.ctype == CardType::Review && self.due <= ctx.today as i32 { if !ctx.config.reschedule {
// review cards that are due are left in the review queue self.queue = CardQueue::Review;
} else { }
// new + non-due go into new queue if self.due > 0 {
self.queue = CardQueue::New; self.due = position;
}
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;
}
} }
} }
@ -75,7 +60,7 @@ impl Card {
self.original_deck_id.or(self.deck_id) 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 { if self.original_deck_id.0 == 0 {
// not in a filtered deck // not in a filtered deck
return; return;
@ -84,33 +69,13 @@ impl Card {
self.deck_id = self.original_deck_id; self.deck_id = self.original_deck_id;
self.original_deck_id.0 = 0; self.original_deck_id.0 = 0;
match sched { // original_due is cleared if card answered in filtered deck
SchedulerVersion::V1 => { if self.original_due != 0 {
self.due = self.original_due; 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 { if (self.queue as i8) >= 0 {
self.restore_queue_from_type(); self.restore_queue_from_type();
}
}
} }
self.original_due = 0; self.original_due = 0;

View file

@ -26,7 +26,6 @@ pub struct FilteredDeckForUpdate {
pub(crate) struct DeckFilterContext<'a> { pub(crate) struct DeckFilterContext<'a> {
pub target_deck: DeckId, pub target_deck: DeckId,
pub config: &'a FilteredDeck, pub config: &'a FilteredDeck,
pub scheduler: SchedulerVersion,
pub usn: Usn, pub usn: Usn,
pub today: u32, pub today: u32,
} }
@ -84,12 +83,11 @@ impl Collection {
// Unlike the old Python code, this also marks the cards as modified. // Unlike the old Python code, this also marks the cards as modified.
fn return_cards_to_home_deck(&mut self, cids: &[CardId]) -> Result<()> { fn return_cards_to_home_deck(&mut self, cids: &[CardId]) -> Result<()> {
let sched = self.scheduler_version();
let usn = self.usn()?; let usn = self.usn()?;
for cid in cids { for cid in cids {
if let Some(mut card) = self.storage.get_card(*cid)? { if let Some(mut card) = self.storage.get_card(*cid)? {
let original = card.clone(); 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)?; self.update_card_inner(&mut card, original, usn)?;
} }
} }
@ -99,12 +97,7 @@ impl Collection {
fn build_filtered_deck(&mut self, ctx: DeckFilterContext) -> Result<usize> { fn build_filtered_deck(&mut self, ctx: DeckFilterContext) -> Result<usize> {
let start = -100_000; let start = -100_000;
let mut position = start; let mut position = start;
let limit = if ctx.scheduler == SchedulerVersion::V1 { for term in ctx.config.search_terms.iter().take(2) {
1
} else {
2
};
for term in ctx.config.search_terms.iter().take(limit) {
position = self.move_cards_matching_term(&ctx, term, position)?; position = self.move_cards_matching_term(&ctx, term, position)?;
} }
@ -120,16 +113,11 @@ impl Collection {
mut position: i32, mut position: i32,
) -> Result<i32> { ) -> Result<i32> {
let search = format!( let search = format!(
"{} -is:suspended -is:buried -deck:filtered {}", "{} -is:suspended -is:buried -deck:filtered",
if term.search.trim().is_empty() { if term.search.trim().is_empty() {
"".to_string() "".to_string()
} else { } else {
format!("({})", term.search) format!("({})", term.search)
},
if ctx.scheduler == SchedulerVersion::V1 {
"-is:learn"
} else {
""
} }
); );
let order = order_and_limit_for_search(term, ctx.today); 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> { 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 config = deck.filtered()?;
let ctx = DeckFilterContext { let ctx = DeckFilterContext {
target_deck: deck.id, target_deck: deck.id,
config, config,
scheduler: self.scheduler_version(),
usn, usn,
today: self.timing_today()?.days_elapsed, today: self.timing_today()?.days_elapsed,
}; };

View file

@ -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;
}
}
}

View file

@ -11,7 +11,6 @@ pub mod bury_and_suspend;
pub(crate) mod congrats; pub(crate) mod congrats;
pub(crate) mod filtered; pub(crate) mod filtered;
pub mod fsrs; pub mod fsrs;
mod learning;
pub mod new; pub mod new;
pub(crate) mod queue; pub(crate) mod queue;
mod reviews; mod reviews;
@ -24,8 +23,6 @@ mod upgrade;
use chrono::FixedOffset; use chrono::FixedOffset;
pub use reviews::parse_due_date_str; pub use reviews::parse_due_date_str;
use timing::sched_timing_today; use timing::sched_timing_today;
use timing::v1_creation_date_adjusted_to_hour;
use timing::v1_rollover_from_creation_stamp;
use timing::SchedTimingToday; use timing::SchedTimingToday;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -118,16 +115,14 @@ impl Collection {
pub fn rollover_for_current_scheduler(&self) -> Result<u8> { pub fn rollover_for_current_scheduler(&self) -> Result<u8> {
match self.scheduler_version() { 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)), SchedulerVersion::V2 => Ok(self.get_v2_rollover().unwrap_or(4)),
} }
} }
pub(crate) fn set_rollover_for_current_scheduler(&mut self, hour: u8) -> Result<()> { pub(crate) fn set_rollover_for_current_scheduler(&mut self, hour: u8) -> Result<()> {
match self.scheduler_version() { match self.scheduler_version() {
SchedulerVersion::V1 => self.set_creation_stamp(TimestampSecs( SchedulerVersion::V1 => Err(AnkiError::SchedulerUpgradeRequired),
v1_creation_date_adjusted_to_hour(self.storage.creation_stamp()?, hour)?,
)),
SchedulerVersion::V2 => self.set_v2_rollover(hour as u32), SchedulerVersion::V2 => self.set_v2_rollover(hour as u32),
} }
} }

View file

@ -63,8 +63,8 @@ impl Card {
} }
/// If the card is new, change its position, and return true. /// If the card is new, change its position, and return true.
fn set_new_position(&mut self, position: u32, v2: bool) -> bool { fn set_new_position(&mut self, position: u32) -> bool {
if v2 && self.ctype == CardType::New { if self.ctype == CardType::New {
if self.is_filtered() { if self.is_filtered() {
self.original_due = position as i32; self.original_due = position as i32;
} else { } else {
@ -234,16 +234,18 @@ impl Collection {
shift: bool, shift: bool,
usn: Usn, usn: Usn,
) -> Result<usize> { ) -> Result<usize> {
let v2 = self.scheduler_version() != SchedulerVersion::V1; if self.scheduler_version() == SchedulerVersion::V1 {
return Err(AnkiError::SchedulerUpgradeRequired);
}
if shift { 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 cards = self.all_cards_for_ids(cids, true)?;
let sorter = NewCardSorter::new(&cards, starting_from, step, order); let sorter = NewCardSorter::new(&cards, starting_from, step, order);
let mut count = 0; let mut count = 0;
for mut card in cards { for mut card in cards {
let original = card.clone(); let original = card.clone();
if card.set_new_position(sorter.position(&card), v2) { if card.set_new_position(sorter.position(&card)) {
count += 1; count += 1;
self.update_card_inner(&mut card, original, usn)?; 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) 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)? { for mut card in self.storage.all_cards_at_or_above_position(start)? {
let original = card.clone(); 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)?; self.update_card_inner(&mut card, original, usn)?;
} }
Ok(()) Ok(())

View file

@ -94,13 +94,6 @@ pub fn local_minutes_west_for_stamp(stamp: TimestampSecs) -> Result<i32> {
Ok(stamp.local_datetime()?.offset().utc_minus_local() / 60) 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 { pub(crate) fn v1_creation_date() -> i64 {
let now = TimestampSecs::now(); let now = TimestampSecs::now();
v1_creation_date_inner(now, local_minutes_west_for_stamp(now).unwrap()) 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 { fn sched_timing_today_v1(crt: TimestampSecs, now: TimestampSecs) -> SchedTimingToday {
let days_elapsed = (now.0 - crt.0) / 86_400; let days_elapsed = (now.0 - crt.0) / 86_400;
let next_day_at = TimestampSecs(crt.0 + (days_elapsed + 1) * 86_400); let next_day_at = TimestampSecs(crt.0 + (days_elapsed + 1) * 86_400);
@ -494,19 +474,5 @@ mod test {
.unwrap() .unwrap()
.timestamp() .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)
);
} }
} }

View file

@ -581,8 +581,8 @@ impl super::SqliteStorage {
} }
pub(crate) fn congrats_info(&self, current: &Deck, today: u32) -> Result<CongratsInfo> { 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 // NOTE: this line is obsolete in v3 as it's run on queue build, but kept to
// on queue build by v3 // prevent errors for v1/v2 users before they upgrade
self.update_active_decks(current)?; self.update_active_decks(current)?;
self.db self.db
.prepare(include_str!("congrats.sql"))? .prepare(include_str!("congrats.sql"))?

View file

@ -14,28 +14,14 @@ SELECT did,
-- intraday learning -- intraday learning
sum( sum(
( (
CASE (
:sched_ver queue = :learn_queue
WHEN 2 THEN ( AND due < :learn_cutoff
-- v2 scheduler )
( OR (
queue = :learn_queue queue = :preview_queue
AND due < :learn_cutoff 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
) )
), ),
-- total -- total

View file

@ -13,7 +13,6 @@ use unicase::UniCase;
use super::SqliteStorage; use super::SqliteStorage;
use crate::card::CardQueue; use crate::card::CardQueue;
use crate::config::SchedulerVersion;
use crate::decks::immediate_parent_name; use crate::decks::immediate_parent_name;
use crate::decks::DeckCommon; use crate::decks::DeckCommon;
use crate::decks::DeckKindContainer; use crate::decks::DeckKindContainer;
@ -297,16 +296,13 @@ impl SqliteStorage {
pub(crate) fn due_counts( pub(crate) fn due_counts(
&self, &self,
sched: SchedulerVersion,
day_cutoff: u32, day_cutoff: u32,
learn_cutoff: u32, learn_cutoff: u32,
) -> Result<HashMap<DeckId, DueCounts>> { ) -> Result<HashMap<DeckId, DueCounts>> {
let sched_ver = sched as u8;
let params = named_params! { let params = named_params! {
":new_queue": CardQueue::New as u8, ":new_queue": CardQueue::New as u8,
":review_queue": CardQueue::Review as u8, ":review_queue": CardQueue::Review as u8,
":day_cutoff": day_cutoff, ":day_cutoff": day_cutoff,
":sched_ver": sched_ver,
":learn_queue": CardQueue::Learn as u8, ":learn_queue": CardQueue::Learn as u8,
":learn_cutoff": learn_cutoff, ":learn_cutoff": learn_cutoff,
":daylearn_queue": CardQueue::DayLearn as u8, ":daylearn_queue": CardQueue::DayLearn as u8,

View file

@ -133,20 +133,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}} }}
/> />
<DynamicallySlottable slotHost={Item} {api}> <DynamicallySlottable slotHost={Item} {api}>
{#if state.v3Scheduler} <Item>
<Item> <SwitchRow bind:value={$fsrs} defaultValue={false}>
<SwitchRow bind:value={$fsrs} defaultValue={false}> <SettingTitle
<SettingTitle on:click={() =>
on:click={() => openHelpModal(Object.keys(settings).indexOf("fsrs"))}
openHelpModal(Object.keys(settings).indexOf("fsrs"))} >
> FSRS
FSRS </SettingTitle>
</SettingTitle> </SwitchRow>
</SwitchRow> </Item>
</Item>
<Warning warning={fsrsClientWarning} /> <Warning warning={fsrsClientWarning} />
{/if}
<Item> <Item>
<SpinBoxRow <SpinBoxRow
@ -164,7 +162,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</SpinBoxRow> </SpinBoxRow>
</Item> </Item>
{#if !$fsrs || !state.v3Scheduler} {#if !$fsrs}
<Item> <Item>
<SpinBoxFloatRow <SpinBoxFloatRow
bind:value={$config.initialEase} bind:value={$config.initialEase}
@ -257,17 +255,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
/> />
{/if} {/if}
{#if state.v3Scheduler} <Item>
<Item> <CardStateCustomizer
<CardStateCustomizer title={settings.customScheduling.title}
title={settings.customScheduling.title} on:click={() =>
on:click={() => openHelpModal(Object.keys(settings).indexOf("customScheduling"))}
openHelpModal( bind:value={$cardStateCustomizer}
Object.keys(settings).indexOf("customScheduling"), />
)} </Item>
bind:value={$cardStateCustomizer}
/>
</Item>
{/if}
</DynamicallySlottable> </DynamicallySlottable>
</TitledContainer> </TitledContainer>

View file

@ -23,9 +23,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
const config = state.currentConfig; const config = state.currentConfig;
const defaults = state.defaults; const defaults = state.defaults;
const priorityTooltip = state.v3Scheduler const priorityTooltip = "\n\n" + tr.deckConfigBuryPriorityTooltip();
? "\n\n" + tr.deckConfigBuryPriorityTooltip()
: "";
const settings = { const settings = {
buryNewSiblings: { buryNewSiblings: {
@ -91,24 +89,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</SwitchRow> </SwitchRow>
</Item> </Item>
{#if state.v3Scheduler} <Item>
<Item> <SwitchRow
<SwitchRow bind:value={$config.buryInterdayLearning}
bind:value={$config.buryInterdayLearning} defaultValue={defaults.buryInterdayLearning}
defaultValue={defaults.buryInterdayLearning} >
<SettingTitle
on:click={() =>
openHelpModal(
Object.keys(settings).indexOf(
"buryInterdayLearningSiblings",
),
)}
> >
<SettingTitle {settings.buryInterdayLearningSiblings.title}
on:click={() => </SettingTitle>
openHelpModal( </SwitchRow>
Object.keys(settings).indexOf( </Item>
"buryInterdayLearningSiblings",
),
)}
>
{settings.buryInterdayLearningSiblings.title}
</SettingTitle>
</SwitchRow>
</Item>
{/if}
</DynamicallySlottable> </DynamicallySlottable>
</TitledContainer> </TitledContainer>

View file

@ -44,25 +44,16 @@
const config = state.currentConfig; const config = state.currentConfig;
const limits = state.deckLimits; const limits = state.deckLimits;
const defaults = state.defaults; const defaults = state.defaults;
const parentLimits = state.parentLimits;
const newCardsIgnoreReviewLimit = state.newCardsIgnoreReviewLimit; const newCardsIgnoreReviewLimit = state.newCardsIgnoreReviewLimit;
const v3Extra = state.v3Scheduler const v3Extra =
? "\n\n" + tr.deckConfigLimitDeckV3() + "\n\n" + tr.deckConfigTabDescription() "\n\n" + tr.deckConfigLimitDeckV3() + "\n\n" + tr.deckConfigTabDescription();
: ""; const reviewV3Extra = "\n\n" + tr.deckConfigLimitInterdayBoundByReviews() + v3Extra;
const reviewV3Extra = state.v3Scheduler
? "\n\n" + tr.deckConfigLimitInterdayBoundByReviews() + v3Extra
: "";
const newCardsIgnoreReviewLimitHelp = const newCardsIgnoreReviewLimitHelp =
tr.deckConfigAffectsEntireCollection() + tr.deckConfigAffectsEntireCollection() +
"\n\n" + "\n\n" +
tr.deckConfigNewCardsIgnoreReviewLimitTooltip(); tr.deckConfigNewCardsIgnoreReviewLimitTooltip();
$: newCardsGreaterThanParent =
!state.v3Scheduler && newValue > $parentLimits.newCards
? tr.deckConfigDailyLimitWillBeCapped({ cards: $parentLimits.newCards })
: "";
$: reviewsTooLow = $: reviewsTooLow =
Math.min(9999, newValue * 10) > reviewsValue Math.min(9999, newValue * 10) > reviewsValue
? tr.deckConfigReviewsTooLow({ ? tr.deckConfigReviewsTooLow({
@ -79,26 +70,21 @@
$config.newPerDay, $config.newPerDay,
null, null,
), ),
].concat( new ValueTab(
state.v3Scheduler tr.deckConfigDeckOnly(),
? [ $limits.new ?? null,
new ValueTab( (value) => ($limits.new = value ?? undefined),
tr.deckConfigDeckOnly(), null,
$limits.new ?? null, null,
(value) => ($limits.new = value ?? undefined), ),
null, new ValueTab(
null, tr.deckConfigTodayOnly(),
), $limits.newTodayActive ? $limits.newToday ?? null : null,
new ValueTab( (value) => ($limits.newToday = value ?? undefined),
tr.deckConfigTodayOnly(), null,
$limits.newTodayActive ? $limits.newToday ?? null : null, $limits.newToday ?? null,
(value) => ($limits.newToday = value ?? undefined), ),
null, ];
$limits.newToday ?? null,
),
]
: [],
);
const reviewTabs: ValueTab[] = [ const reviewTabs: ValueTab[] = [
new ValueTab( new ValueTab(
@ -108,26 +94,21 @@
$config.reviewsPerDay, $config.reviewsPerDay,
null, null,
), ),
].concat( new ValueTab(
state.v3Scheduler tr.deckConfigDeckOnly(),
? [ $limits.review ?? null,
new ValueTab( (value) => ($limits.review = value ?? undefined),
tr.deckConfigDeckOnly(), null,
$limits.review ?? null, null,
(value) => ($limits.review = value ?? undefined), ),
null, new ValueTab(
null, tr.deckConfigTodayOnly(),
), $limits.reviewTodayActive ? $limits.reviewToday ?? null : null,
new ValueTab( (value) => ($limits.reviewToday = value ?? undefined),
tr.deckConfigTodayOnly(), null,
$limits.reviewTodayActive ? $limits.reviewToday ?? null : null, $limits.reviewToday ?? null,
(value) => ($limits.reviewToday = value ?? undefined), ),
null, ];
$limits.reviewToday ?? null,
),
]
: [],
);
let newValue = 0; let newValue = 0;
let reviewsValue = 0; let reviewsValue = 0;
@ -184,9 +165,6 @@
</SpinBoxRow> </SpinBoxRow>
</Item> </Item>
<Item>
<Warning warning={newCardsGreaterThanParent} />
</Item>
<Item> <Item>
<SpinBoxRow bind:value={reviewsValue} defaultValue={defaults.reviewsPerDay}> <SpinBoxRow bind:value={reviewsValue} defaultValue={defaults.reviewsPerDay}>
<TabbedValue slot="tabs" tabs={reviewTabs} bind:value={reviewsValue} /> <TabbedValue slot="tabs" tabs={reviewTabs} bind:value={reviewsValue} />
@ -203,21 +181,17 @@
<Warning warning={reviewsTooLow} /> <Warning warning={reviewsTooLow} />
</Item> </Item>
{#if state.v3Scheduler} <Item>
<Item> <SwitchRow bind:value={$newCardsIgnoreReviewLimit} defaultValue={false}>
<SwitchRow bind:value={$newCardsIgnoreReviewLimit} defaultValue={false}> <SettingTitle
<SettingTitle on:click={() =>
on:click={() => openHelpModal(
openHelpModal( Object.keys(settings).indexOf("newCardsIgnoreReviewLimit"),
Object.keys(settings).indexOf( )}
"newCardsIgnoreReviewLimit", >
), {settings.newCardsIgnoreReviewLimit.title}
)} </SettingTitle>
> </SwitchRow>
{settings.newCardsIgnoreReviewLimit.title} </Item>
</SettingTitle>
</SwitchRow>
</Item>
{/if}
</DynamicallySlottable> </DynamicallySlottable>
</TitledContainer> </TitledContainer>

View file

@ -86,13 +86,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</Row> </Row>
</Item> </Item>
{#if state.v3Scheduler} <Item>
<Item> <Row class="row-columns">
<Row class="row-columns"> <DisplayOrder {state} api={displayOrder} />
<DisplayOrder {state} api={displayOrder} /> </Row>
</Row> </Item>
</Item>
{/if}
<Item> <Item>
<Row class="row-columns"> <Row class="row-columns">

View file

@ -51,7 +51,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
: ""; : "";
$: insertionOrderRandom = $: insertionOrderRandom =
state.v3Scheduler &&
$config.newCardInsertOrder == DeckConfig_Config_NewCardInsertOrder.RANDOM $config.newCardInsertOrder == DeckConfig_Config_NewCardInsertOrder.RANDOM
? tr.deckConfigNewInsertionOrderRandomWithV3() ? tr.deckConfigNewInsertionOrderRandomWithV3()
: ""; : "";

View file

@ -72,7 +72,6 @@ const exampleData = {
currentDeck: { currentDeck: {
name: "Default::child", name: "Default::child",
configId: 1618570764780n, configId: 1618570764780n,
parentConfigIds: [1n],
}, },
defaults: { defaults: {
config: { config: {
@ -220,27 +219,6 @@ test("duplicate name", () => {
expect(get(state.configList).find((e) => e.current)?.name).toMatch(/Default\d+$/); 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", () => { test("saving", () => {
let state = startingState(); let state = startingState();
let out = state.dataForSaving(false); let out = state.dataForSaving(false);

View file

@ -23,11 +23,6 @@ export interface ConfigWithCount {
useCount: number; useCount: number;
} }
export interface ParentLimits {
newCards: number;
reviews: number;
}
/** Info for showing the top selector */ /** Info for showing the top selector */
export interface ConfigListEntry { export interface ConfigListEntry {
idx: number; idx: number;
@ -40,13 +35,11 @@ export class DeckOptionsState {
readonly currentConfig: Writable<DeckConfig_Config>; readonly currentConfig: Writable<DeckConfig_Config>;
readonly currentAuxData: Writable<Record<string, unknown>>; readonly currentAuxData: Writable<Record<string, unknown>>;
readonly configList: Readable<ConfigListEntry[]>; readonly configList: Readable<ConfigListEntry[]>;
readonly parentLimits: Readable<ParentLimits>;
readonly cardStateCustomizer: Writable<string>; readonly cardStateCustomizer: Writable<string>;
readonly currentDeck: DeckConfigsForUpdate_CurrentDeck; readonly currentDeck: DeckConfigsForUpdate_CurrentDeck;
readonly deckLimits: Writable<DeckConfigsForUpdate_CurrentDeck_Limits>; readonly deckLimits: Writable<DeckConfigsForUpdate_CurrentDeck_Limits>;
readonly defaults: DeckConfig_Config; readonly defaults: DeckConfig_Config;
readonly addonComponents: Writable<DynamicSvelteComponent[]>; readonly addonComponents: Writable<DynamicSvelteComponent[]>;
readonly v3Scheduler: boolean;
readonly newCardsIgnoreReviewLimit: Writable<boolean>; readonly newCardsIgnoreReviewLimit: Writable<boolean>;
readonly fsrs: Writable<boolean>; readonly fsrs: Writable<boolean>;
readonly currentPresetName: Writable<string>; readonly currentPresetName: Writable<string>;
@ -55,7 +48,6 @@ export class DeckOptionsState {
private configs: ConfigWithCount[]; private configs: ConfigWithCount[];
private selectedIdx: number; private selectedIdx: number;
private configListSetter!: (val: ConfigListEntry[]) => void; private configListSetter!: (val: ConfigListEntry[]) => void;
private parentLimitsSetter!: (val: ParentLimits) => void;
private modifiedConfigs: Set<DeckOptionsId> = new Set(); private modifiedConfigs: Set<DeckOptionsId> = new Set();
private removedConfigs: DeckOptionsId[] = []; private removedConfigs: DeckOptionsId[] = [];
private schemaModified: boolean; private schemaModified: boolean;
@ -77,7 +69,6 @@ export class DeckOptionsState {
this.configs.findIndex((c) => c.config.id === this.currentDeck.configId), this.configs.findIndex((c) => c.config.id === this.currentDeck.configId),
); );
this.sortConfigs(); this.sortConfigs();
this.v3Scheduler = data.v3Scheduler;
this.cardStateCustomizer = writable(data.cardStateCustomizer); this.cardStateCustomizer = writable(data.cardStateCustomizer);
this.deckLimits = writable(data.currentDeck?.limits ?? createLimits()); this.deckLimits = writable(data.currentDeck?.limits ?? createLimits());
this.newCardsIgnoreReviewLimit = writable(data.newCardsIgnoreReviewLimit); this.newCardsIgnoreReviewLimit = writable(data.newCardsIgnoreReviewLimit);
@ -93,17 +84,12 @@ export class DeckOptionsState {
this.configListSetter = set; this.configListSetter = set;
return; return;
}); });
this.parentLimits = readable(this.getParentLimits(), (set) => {
this.parentLimitsSetter = set;
return;
});
this.schemaModified = data.schemaModified; this.schemaModified = data.schemaModified;
this.addonComponents = writable([]); this.addonComponents = writable([]);
// create a temporary subscription to force our setters to be set immediately, // create a temporary subscription to force our setters to be set immediately,
// so unit tests don't get stale results // so unit tests don't get stale results
get(this.configList); get(this.configList);
get(this.parentLimits);
// update our state when the current config is changed // update our state when the current config is changed
this.currentConfig.subscribe((val) => this.onCurrentConfigChanged(val)); this.currentConfig.subscribe((val) => this.onCurrentConfigChanged(val));
@ -227,7 +213,6 @@ export class DeckOptionsState {
this.modifiedConfigs.add(configOuter.id); this.modifiedConfigs.add(configOuter.id);
} }
} }
this.parentLimitsSetter?.(this.getParentLimits());
} }
private onCurrentAuxDataChanged(data: Record<string, unknown>): void { private onCurrentAuxDataChanged(data: Record<string, unknown>): void {
@ -253,7 +238,6 @@ export class DeckOptionsState {
private updateCurrentConfig(): void { private updateCurrentConfig(): void {
this.currentConfig.set(this.getCurrentConfig()); this.currentConfig.set(this.getCurrentConfig());
this.currentAuxData.set(this.getCurrentAuxData()); this.currentAuxData.set(this.getCurrentAuxData());
this.parentLimitsSetter?.(this.getParentLimits());
} }
private updateConfigList(): void { private updateConfigList(): void {
@ -292,22 +276,6 @@ export class DeckOptionsState {
}); });
return list; 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> { function bytesToObject(bytes: Uint8Array): Record<string, unknown> {