From 5cde4b6941064b93badcddbda88c5f6aa3273042 Mon Sep 17 00:00:00 2001 From: Abdo Date: Sat, 14 Oct 2023 03:50:59 +0300 Subject: [PATCH] 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) --- ftl/core-repo | 2 +- ftl/qt-repo | 2 +- proto/anki/backend.proto | 1 + proto/anki/config.proto | 3 - proto/anki/deck_config.proto | 1 - pylib/anki/_backend.py | 4 + pylib/anki/collection.py | 90 +------------- pylib/anki/errors.py | 4 + pylib/anki/exporting.py | 14 +-- pylib/anki/importing/anki2.py | 2 +- pylib/anki/stats.py | 15 +-- pylib/tests/test_schedv3.py | 6 - pylib/tools/genhooks.py | 24 ---- qt/aqt/browser/browser.py | 2 +- qt/aqt/deckconf.py | 3 - qt/aqt/deckoptions.py | 2 +- qt/aqt/filtered_deck.py | 36 +----- qt/aqt/operations/collection.py | 34 +---- qt/aqt/overview.py | 35 +++--- qt/aqt/reviewer.py | 84 ++++--------- qt/tools/genhooks_gui.py | 1 - rslib/src/backend/error.rs | 1 + rslib/src/card/mod.rs | 11 +- rslib/src/deckconfig/update.rs | 1 - rslib/src/decks/counts.rs | 3 +- rslib/src/decks/limits.rs | 24 +--- rslib/src/decks/tree.rs | 74 +---------- rslib/src/error/mod.rs | 4 + .../package/apkg/import/cards.rs | 9 +- rslib/src/preferences.rs | 10 -- rslib/src/scheduler/answering/preview.rs | 4 +- rslib/src/scheduler/bury_and_suspend.rs | 16 +-- rslib/src/scheduler/filtered/card.rs | 61 ++------- rslib/src/scheduler/filtered/mod.rs | 23 ++-- rslib/src/scheduler/learning.rs | 35 ------ rslib/src/scheduler/mod.rs | 9 +- rslib/src/scheduler/new.rs | 16 +-- rslib/src/scheduler/timing.rs | 34 ----- rslib/src/storage/card/mod.rs | 4 +- rslib/src/storage/deck/due_counts.sql | 30 ++--- rslib/src/storage/deck/mod.rs | 4 - ts/deck-options/AdvancedOptions.svelte | 46 +++---- ts/deck-options/BuryOptions.svelte | 38 +++--- ts/deck-options/DailyLimits.svelte | 116 +++++++----------- ts/deck-options/DeckOptionsPage.svelte | 12 +- ts/deck-options/NewOptions.svelte | 1 - ts/deck-options/lib.test.ts | 22 ---- ts/deck-options/lib.ts | 32 ----- 48 files changed, 230 insertions(+), 775 deletions(-) delete mode 100644 rslib/src/scheduler/learning.rs diff --git a/ftl/core-repo b/ftl/core-repo index efd2e6eda..3d820846d 160000 --- a/ftl/core-repo +++ b/ftl/core-repo @@ -1 +1 @@ -Subproject commit efd2e6edaf1e2b8e1d52c45ebd09d67ac035050f +Subproject commit 3d820846d847f8ed866971e3a4304c715325d01b diff --git a/ftl/qt-repo b/ftl/qt-repo index 41663c8bd..ef4f4ffee 160000 --- a/ftl/qt-repo +++ b/ftl/qt-repo @@ -1 +1 @@ -Subproject commit 41663c8bd0f6386ee98de03ca2c3e668c4f6194d +Subproject commit ef4f4ffee68a3d3ebb2458b8b55bfdffa9a21c4b diff --git a/proto/anki/backend.proto b/proto/anki/backend.proto index 4de1f0d02..262fe961c 100644 --- a/proto/anki/backend.proto +++ b/proto/anki/backend.proto @@ -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 diff --git a/proto/anki/config.proto b/proto/anki/config.proto index 9cbd9f3b5..1eda78e03 100644 --- a/proto/anki/config.proto +++ b/proto/anki/config.proto @@ -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; diff --git a/proto/anki/deck_config.proto b/proto/anki/deck_config.proto index fe9ba7a50..0239c9a2f 100644 --- a/proto/anki/deck_config.proto +++ b/proto/anki/deck_config.proto @@ -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 diff --git a/pylib/anki/_backend.py b/pylib/anki/_backend.py index 617754578..a57d9a2fe 100644 --- a/pylib/anki/_backend.py +++ b/pylib/anki/_backend.py @@ -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) diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 1ffc7d656..5b79969f1 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -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: diff --git a/pylib/anki/errors.py b/pylib/anki/errors.py index a3778a65d..5641b630d 100644 --- a/pylib/anki/errors.py +++ b/pylib/anki/errors.py @@ -119,6 +119,10 @@ class SearchError(BackendError): pass +class SchedulerUpgradeRequired(BackendError): + pass + + class AbortSchemaModification(AnkiException): pass diff --git a/pylib/anki/exporting.py b/pylib/anki/exporting.py index 72de4fea3..90b1170b5 100644 --- a/pylib/anki/exporting.py +++ b/pylib/anki/exporting.py @@ -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() diff --git a/pylib/anki/importing/anki2.py b/pylib/anki/importing/anki2.py index 21ca6800b..51e439967 100644 --- a/pylib/anki/importing/anki2.py +++ b/pylib/anki/importing/anki2.py @@ -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 diff --git a/pylib/anki/stats.py b/pylib/anki/stats.py index 4c23643ee..4c1e3a9e3 100644 --- a/pylib/anki/stats.py +++ b/pylib/anki/stats.py @@ -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) diff --git a/pylib/tests/test_schedv3.py b/pylib/tests/test_schedv3.py index ab540938d..6a7ead2ba 100644 --- a/pylib/tests/test_schedv3.py +++ b/pylib/tests/test_schedv3.py @@ -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() diff --git a/pylib/tools/genhooks.py b/pylib/tools/genhooks.py index 388a182f1..e14f92fc4 100644 --- a/pylib/tools/genhooks.py +++ b/pylib/tools/genhooks.py @@ -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]]"], diff --git a/qt/aqt/browser/browser.py b/qt/aqt/browser/browser.py index 2dbf2934e..bee9a5665 100644 --- a/qt/aqt/browser/browser.py +++ b/qt/aqt/browser/browser.py @@ -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) diff --git a/qt/aqt/deckconf.py b/qt/aqt/deckconf.py index 9988664fb..56b6fcaba 100644 --- a/qt/aqt/deckconf.py +++ b/qt/aqt/deckconf.py @@ -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"])) diff --git a/qt/aqt/deckoptions.py b/qt/aqt/deckoptions.py index fb4428f9e..e0805367d 100644 --- a/qt/aqt/deckoptions.py +++ b/qt/aqt/deckoptions.py @@ -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: diff --git a/qt/aqt/filtered_deck.py b/qt/aqt/filtered_deck.py index bba0626db..489928743 100644 --- a/qt/aqt/filtered_deck.py +++ b/qt/aqt/filtered_deck.py @@ -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(), diff --git a/qt/aqt/operations/collection.py b/qt/aqt/operations/collection.py index f93c6e06a..6984bf42b 100644 --- a/qt/aqt/operations/collection.py +++ b/qt/aqt/operations/collection.py @@ -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) diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py index 97251775d..6bfb58f79 100644 --- a/qt/aqt/overview.py +++ b/qt/aqt/overview.py @@ -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 diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 48b479e71..0aea5ce06 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -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: diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index 6cdd35431..13660c029 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -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"], diff --git a/rslib/src/backend/error.rs b/rslib/src/backend/error.rs index d06efd9ca..7895d7204 100644 --- a/rslib/src/backend/error.rs +++ b/rslib/src/backend/error.rs @@ -46,6 +46,7 @@ impl AnkiError { | AnkiError::FsrsInsufficientData => Kind::InvalidInput, #[cfg(windows)] AnkiError::WindowsError { .. } => Kind::OsError, + AnkiError::SchedulerUpgradeRequired => Kind::SchedulerUpgradeRequired, }; anki_proto::backend::BackendError { diff --git a/rslib/src/card/mod.rs b/rslib/src/card/mod.rs index a76ce0e60..a14f3b176 100644 --- a/rslib/src/card/mod.rs +++ b/rslib/src/card/mod.rs @@ -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> { + 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) diff --git a/rslib/src/deckconfig/update.rs b/rslib/src/deckconfig/update.rs index a619b766b..45480fd3f 100644 --- a/rslib/src/deckconfig/update.rs +++ b/rslib/src/deckconfig/update.rs @@ -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), diff --git a/rslib/src/decks/counts.rs b/rslib/src/decks/counts.rs index 8cd55c115..b1907b01e 100644 --- a/rslib/src/decks/counts.rs +++ b/rslib/src/decks/counts.rs @@ -35,8 +35,7 @@ impl Collection { days_elapsed: u32, learn_cutoff: u32, ) -> Result> { - 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( diff --git a/rslib/src/decks/limits.rs b/rslib/src/decks/limits.rs index ec39a9c62..d8e020451 100644 --- a/rslib/src/decks/limits.rs +++ b/rslib/src/decks/limits.rs @@ -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, config: &'a HashMap, today: u32, - v3: bool, new_cards_ignore_review_limit: bool, ) -> HashMap { 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, ), } diff --git a/rslib/src/decks/tree.rs b/rslib/src/decks/tree.rs index bd2725ee4..e2f7cca71 100644 --- a/rslib/src/decks/tree.rs +++ b/rslib/src/decks/tree.rs @@ -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) { } } -/// Apply parent limits to children, and add child counts to parents. -fn sum_counts_and_apply_limits_v1( - node: &mut DeckTreeNode, - limits: &HashMap, - 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, - 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) diff --git a/rslib/src/error/mod.rs b/rslib/src/error/mod.rs index e180f6c85..87c128b45 100644 --- a/rslib/src/error/mod.rs +++ b/rslib/src/error/mod.rs @@ -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:?}"), } diff --git a/rslib/src/import_export/package/apkg/import/cards.rs b/rslib/src/import_export/package/apkg/import/cards.rs index bba483f31..e04bbaf23 100644 --- a/rslib/src/import_export/package/apkg/import/cards.rs +++ b/rslib/src/import_export/package/apkg/import/cards.rs @@ -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(); } } } diff --git a/rslib/src/preferences.rs b/rslib/src/preferences.rs index 4f7d7ba6e..6ee564d3f 100644 --- a/rslib/src/preferences.rs +++ b/rslib/src/preferences.rs @@ -48,16 +48,6 @@ impl Collection { pub fn get_scheduling_preferences(&self) -> Result { 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() { diff --git a/rslib/src/scheduler/answering/preview.rs b/rslib/src/scheduler/answering/preview.rs index f8e795600..c52920ce9 100644 --- a/rslib/src/scheduler/answering/preview.rs +++ b/rslib/src/scheduler/answering/preview.rs @@ -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; } diff --git a/rslib/src/scheduler/bury_and_suspend.rs b/rslib/src/scheduler/bury_and_suspend.rs index 1b1c55934..851199508 100644 --- a/rslib/src/scheduler/bury_and_suspend.rs +++ b/rslib/src/scheduler/bury_and_suspend.rs @@ -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)?; diff --git a/rslib/src/scheduler/filtered/card.rs b/rslib/src/scheduler/filtered/card.rs index f3d5915c5..bf1ff417b 100644 --- a/rslib/src/scheduler/filtered/card.rs +++ b/rslib/src/scheduler/filtered/card.rs @@ -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; diff --git a/rslib/src/scheduler/filtered/mod.rs b/rslib/src/scheduler/filtered/mod.rs index a97a36a7e..5a158dff8 100644 --- a/rslib/src/scheduler/filtered/mod.rs +++ b/rslib/src/scheduler/filtered/mod.rs @@ -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 { 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 { 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 { + 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, }; diff --git a/rslib/src/scheduler/learning.rs b/rslib/src/scheduler/learning.rs deleted file mode 100644 index 08c3139c3..000000000 --- a/rslib/src/scheduler/learning.rs +++ /dev/null @@ -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; - } - } -} diff --git a/rslib/src/scheduler/mod.rs b/rslib/src/scheduler/mod.rs index 0e70a89a2..b2cd8049e 100644 --- a/rslib/src/scheduler/mod.rs +++ b/rslib/src/scheduler/mod.rs @@ -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 { 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), } } diff --git a/rslib/src/scheduler/new.rs b/rslib/src/scheduler/new.rs index e50bda328..7b6f7e9a0 100644 --- a/rslib/src/scheduler/new.rs +++ b/rslib/src/scheduler/new.rs @@ -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 { - 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(()) diff --git a/rslib/src/scheduler/timing.rs b/rslib/src/scheduler/timing.rs index 36744ca8a..61b819894 100644 --- a/rslib/src/scheduler/timing.rs +++ b/rslib/src/scheduler/timing.rs @@ -94,13 +94,6 @@ pub fn local_minutes_west_for_stamp(stamp: TimestampSecs) -> Result { Ok(stamp.local_datetime()?.offset().utc_minus_local() / 60) } -// Legacy code -// ---------------------------------- - -pub(crate) fn v1_rollover_from_creation_stamp(crt: TimestampSecs) -> Result { - 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 { - 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 { - 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) - ); } } diff --git a/rslib/src/storage/card/mod.rs b/rslib/src/storage/card/mod.rs index 3313ca27b..f40005f0f 100644 --- a/rslib/src/storage/card/mod.rs +++ b/rslib/src/storage/card/mod.rs @@ -581,8 +581,8 @@ impl super::SqliteStorage { } pub(crate) fn congrats_info(&self, current: &Deck, today: u32) -> Result { - // 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"))? diff --git a/rslib/src/storage/deck/due_counts.sql b/rslib/src/storage/deck/due_counts.sql index b1fac9781..021b4caf2 100644 --- a/rslib/src/storage/deck/due_counts.sql +++ b/rslib/src/storage/deck/due_counts.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 diff --git a/rslib/src/storage/deck/mod.rs b/rslib/src/storage/deck/mod.rs index 121d32014..1bad0ebb1 100644 --- a/rslib/src/storage/deck/mod.rs +++ b/rslib/src/storage/deck/mod.rs @@ -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> { - 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, diff --git a/ts/deck-options/AdvancedOptions.svelte b/ts/deck-options/AdvancedOptions.svelte index 68f0e1146..7b2c1f6e2 100644 --- a/ts/deck-options/AdvancedOptions.svelte +++ b/ts/deck-options/AdvancedOptions.svelte @@ -133,20 +133,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html }} /> - {#if state.v3Scheduler} - - - - openHelpModal(Object.keys(settings).indexOf("fsrs"))} - > - FSRS - - - + + + + openHelpModal(Object.keys(settings).indexOf("fsrs"))} + > + FSRS + + + - - {/if} + - {#if !$fsrs || !state.v3Scheduler} + {#if !$fsrs} {/if} - {#if state.v3Scheduler} - - - openHelpModal( - Object.keys(settings).indexOf("customScheduling"), - )} - bind:value={$cardStateCustomizer} - /> - - {/if} + + + openHelpModal(Object.keys(settings).indexOf("customScheduling"))} + bind:value={$cardStateCustomizer} + /> + diff --git a/ts/deck-options/BuryOptions.svelte b/ts/deck-options/BuryOptions.svelte index dece6ed95..d14d26ad4 100644 --- a/ts/deck-options/BuryOptions.svelte +++ b/ts/deck-options/BuryOptions.svelte @@ -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 - {#if state.v3Scheduler} - - + + + openHelpModal( + Object.keys(settings).indexOf( + "buryInterdayLearningSiblings", + ), + )} > - - openHelpModal( - Object.keys(settings).indexOf( - "buryInterdayLearningSiblings", - ), - )} - > - {settings.buryInterdayLearningSiblings.title} - - - - {/if} + {settings.buryInterdayLearningSiblings.title} + + + diff --git a/ts/deck-options/DailyLimits.svelte b/ts/deck-options/DailyLimits.svelte index b60ea8e53..df8834f75 100644 --- a/ts/deck-options/DailyLimits.svelte +++ b/ts/deck-options/DailyLimits.svelte @@ -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 @@ - - - @@ -203,21 +181,17 @@ - {#if state.v3Scheduler} - - - - openHelpModal( - Object.keys(settings).indexOf( - "newCardsIgnoreReviewLimit", - ), - )} - > - {settings.newCardsIgnoreReviewLimit.title} - - - - {/if} + + + + openHelpModal( + Object.keys(settings).indexOf("newCardsIgnoreReviewLimit"), + )} + > + {settings.newCardsIgnoreReviewLimit.title} + + + diff --git a/ts/deck-options/DeckOptionsPage.svelte b/ts/deck-options/DeckOptionsPage.svelte index 6fd1a6791..e503ea708 100644 --- a/ts/deck-options/DeckOptionsPage.svelte +++ b/ts/deck-options/DeckOptionsPage.svelte @@ -86,13 +86,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - {#if state.v3Scheduler} - - - - - - {/if} + + + + + diff --git a/ts/deck-options/NewOptions.svelte b/ts/deck-options/NewOptions.svelte index b591b67da..697e48567 100644 --- a/ts/deck-options/NewOptions.svelte +++ b/ts/deck-options/NewOptions.svelte @@ -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() : ""; diff --git a/ts/deck-options/lib.test.ts b/ts/deck-options/lib.test.ts index 7965133e6..f291069f0 100644 --- a/ts/deck-options/lib.test.ts +++ b/ts/deck-options/lib.test.ts @@ -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); diff --git a/ts/deck-options/lib.ts b/ts/deck-options/lib.ts index 5ff00b97c..2071445c8 100644 --- a/ts/deck-options/lib.ts +++ b/ts/deck-options/lib.ts @@ -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; readonly currentAuxData: Writable>; readonly configList: Readable; - readonly parentLimits: Readable; readonly cardStateCustomizer: Writable; readonly currentDeck: DeckConfigsForUpdate_CurrentDeck; readonly deckLimits: Writable; readonly defaults: DeckConfig_Config; readonly addonComponents: Writable; - readonly v3Scheduler: boolean; readonly newCardsIgnoreReviewLimit: Writable; readonly fsrs: Writable; readonly currentPresetName: Writable; @@ -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 = 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): 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 {