diff --git a/ftl/core/scheduling.ftl b/ftl/core/scheduling.ftl
index 435e1fb27..6ae1994fd 100644
--- a/ftl/core/scheduling.ftl
+++ b/ftl/core/scheduling.ftl
@@ -135,6 +135,8 @@ scheduling-new-options-group-name = New options group name:
scheduling-options-group = Options group:
scheduling-order = Order
scheduling-parent-limit = (parent limit: { $val })
+scheduling-reset-counts = Reset repetition and lapse counts
+scheduling-restore-position = Restore original position where possible
scheduling-review = Review
scheduling-reviews = Reviews
scheduling-seconds = seconds
diff --git a/proto/anki/config.proto b/proto/anki/config.proto
index f1ad1119d..8c793d6d9 100644
--- a/proto/anki/config.proto
+++ b/proto/anki/config.proto
@@ -41,6 +41,10 @@ message ConfigKey {
PASTE_STRIPS_FORMATTING = 16;
NORMALIZE_NOTE_TEXT = 17;
IGNORE_ACCENTS_IN_SEARCH = 18;
+ RESTORE_POSITION_BROWSER = 19;
+ RESTORE_POSITION_REVIEWER = 20;
+ RESET_COUNTS_BROWSER = 21;
+ RESET_COUNTS_REVIEWER = 22;
}
enum String {
SET_DUE_BROWSER = 0;
diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto
index 58d016c86..ce6860d9f 100644
--- a/proto/anki/scheduler.proto
+++ b/proto/anki/scheduler.proto
@@ -30,6 +30,8 @@ service SchedulerService {
rpc RebuildFilteredDeck(decks.DeckId) returns (collection.OpChangesWithCount);
rpc ScheduleCardsAsNew(ScheduleCardsAsNewRequest)
returns (collection.OpChanges);
+ rpc ScheduleCardsAsNewDefaults(ScheduleCardsAsNewDefaultsRequest)
+ returns (ScheduleCardsAsNewDefaultsResponse);
rpc SetDueDate(SetDueDateRequest) returns (collection.OpChanges);
rpc SortCards(SortCardsRequest) returns (collection.OpChangesWithCount);
rpc SortDeck(SortDeckRequest) returns (collection.OpChangesWithCount);
@@ -172,8 +174,24 @@ message BuryOrSuspendCardsRequest {
}
message ScheduleCardsAsNewRequest {
+ enum Context {
+ BROWSER = 0;
+ REVIEWER = 1;
+ }
repeated int64 card_ids = 1;
bool log = 2;
+ bool restore_position = 3;
+ bool reset_counts = 4;
+ optional Context context = 5;
+}
+
+message ScheduleCardsAsNewDefaultsRequest {
+ ScheduleCardsAsNewRequest.Context context = 1;
+}
+
+message ScheduleCardsAsNewDefaultsResponse {
+ bool restore_position = 1;
+ bool reset_counts = 2;
}
message SetDueDateRequest {
diff --git a/pylib/.pylintrc b/pylib/.pylintrc
index 9901aa66c..c9d947569 100644
--- a/pylib/.pylintrc
+++ b/pylib/.pylintrc
@@ -21,6 +21,7 @@ ignored-classes=
StripHtmlRequest,
CustomStudyRequest,
Cram,
+ ScheduleCardsAsNewRequest,
[REPORTS]
output-format=colorized
diff --git a/pylib/anki/scheduler/base.py b/pylib/anki/scheduler/base.py
index 1fcdb746c..6f3c4f6c6 100644
--- a/pylib/anki/scheduler/base.py
+++ b/pylib/anki/scheduler/base.py
@@ -15,6 +15,8 @@ CongratsInfo = scheduler_pb2.CongratsInfoResponse
UnburyDeck = scheduler_pb2.UnburyDeckRequest
BuryOrSuspend = scheduler_pb2.BuryOrSuspendCardsRequest
CustomStudyRequest = scheduler_pb2.CustomStudyRequest
+ScheduleCardsAsNew = scheduler_pb2.ScheduleCardsAsNewRequest
+ScheduleCardsAsNewDefaults = scheduler_pb2.ScheduleCardsAsNewDefaultsResponse
FilteredDeckForUpdate = decks_pb2.FilteredDeckForUpdate
@@ -163,9 +165,28 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
# Resetting/rescheduling
##########################################################################
- def schedule_cards_as_new(self, card_ids: Sequence[CardId]) -> OpChanges:
- "Put cards at the end of the new queue."
- return self.col._backend.schedule_cards_as_new(card_ids=card_ids, log=True)
+ def schedule_cards_as_new(
+ self,
+ card_ids: Sequence[CardId],
+ *,
+ restore_position: bool = False,
+ reset_counts: bool = False,
+ context: ScheduleCardsAsNew.Context.V | None = None,
+ ) -> OpChanges:
+ "Place cards back into the new queue."
+ request = ScheduleCardsAsNew(
+ card_ids=card_ids,
+ log=True,
+ restore_position=restore_position,
+ reset_counts=reset_counts,
+ context=context,
+ )
+ return self.col._backend.schedule_cards_as_new(request)
+
+ def schedule_cards_as_new_defaults(
+ self, context: ScheduleCardsAsNew.Context.V
+ ) -> ScheduleCardsAsNewDefaults:
+ return self.col._backend.schedule_cards_as_new_defaults(context)
def set_due_date(
self,
@@ -203,7 +224,8 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
" where id in %s" % sids
)
# and forget any non-new cards, changing their due numbers
- self.col._backend.schedule_cards_as_new(card_ids=non_new, log=False)
+ request = ScheduleCardsAsNew(card_ids=non_new, log=False, restore_position=True)
+ self.col._backend.schedule_cards_as_new(request)
# Repositioning new cards
##########################################################################
diff --git a/qt/.pylintrc b/qt/.pylintrc
index 4068ea43e..e03c29813 100644
--- a/qt/.pylintrc
+++ b/qt/.pylintrc
@@ -17,6 +17,7 @@ ignored-classes=
ChangeNotetypeRequest,
CustomStudyRequest,
Cram,
+ ScheduleCardsAsNewRequest,
[REPORTS]
output-format=colorized
diff --git a/qt/aqt/browser/browser.py b/qt/aqt/browser/browser.py
index 978991b4c..4a314feac 100644
--- a/qt/aqt/browser/browser.py
+++ b/qt/aqt/browser/browser.py
@@ -18,6 +18,7 @@ from anki.consts import *
from anki.errors import NotFoundError
from anki.lang import without_unicode_isolation
from anki.notes import NoteId
+from anki.scheduler.base import ScheduleCardsAsNew
from anki.tags import MARKED_TAG
from anki.utils import is_mac
from aqt import AnkiQt, gui_hooks
@@ -862,10 +863,12 @@ class Browser(QMainWindow):
@skip_if_selection_is_empty
@ensure_editor_saved
def forget_cards(self) -> None:
- forget_cards(
+ if op := forget_cards(
parent=self,
card_ids=self.selected_cards(),
- ).run_in_background()
+ context=ScheduleCardsAsNew.Context.BROWSER,
+ ):
+ op.run_in_background()
# Edit: selection
######################################################################
diff --git a/qt/aqt/forms/__init__.py b/qt/aqt/forms/__init__.py
index dd1433a83..d304d6956 100644
--- a/qt/aqt/forms/__init__.py
+++ b/qt/aqt/forms/__init__.py
@@ -14,6 +14,7 @@ from . import customstudy
from . import dconf
from . import debug
from . import filtered_deck
+from . import forget
from . import editaddon
from . import editcurrent
from . import edithtml
diff --git a/qt/aqt/forms/forget.py b/qt/aqt/forms/forget.py
new file mode 100644
index 000000000..8c7231bf3
--- /dev/null
+++ b/qt/aqt/forms/forget.py
@@ -0,0 +1,6 @@
+from aqt.qt import qtmajor
+
+if qtmajor > 5:
+ from .forget_qt6 import *
+else:
+ from .forget_qt5 import * # type: ignore
diff --git a/qt/aqt/forms/forget.ui b/qt/aqt/forms/forget.ui
new file mode 100644
index 000000000..03e44fdda
--- /dev/null
+++ b/qt/aqt/forms/forget.ui
@@ -0,0 +1,94 @@
+
+
+ Dialog
+
+
+
+ 0
+ 0
+ 235
+ 118
+
+
+
+ actions_forget_card
+
+
+ true
+
+
+ -
+
+
+ scheduling_restore_position
+
+
+
+ -
+
+
+ scheduling_reset_counts
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ Dialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ Dialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/qt/aqt/forms/forget_qt6.py b/qt/aqt/forms/forget_qt6.py
new file mode 120000
index 000000000..008c39912
--- /dev/null
+++ b/qt/aqt/forms/forget_qt6.py
@@ -0,0 +1 @@
+../../../.bazel/bin/qt/aqt/forms/forget_qt6.py
\ No newline at end of file
diff --git a/qt/aqt/operations/scheduling.py b/qt/aqt/operations/scheduling.py
index 624496f1a..b921dd680 100644
--- a/qt/aqt/operations/scheduling.py
+++ b/qt/aqt/operations/scheduling.py
@@ -19,6 +19,7 @@ from anki.collection import (
from anki.decks import DeckId
from anki.notes import NoteId
from anki.scheduler import CustomStudyRequest, FilteredDeckForUpdate, UnburyDeck
+from anki.scheduler.base import ScheduleCardsAsNew
from anki.scheduler.v3 import CardAnswer
from anki.scheduler.v3 import Scheduler as V3Scheduler
from aqt.operations import CollectionOp
@@ -65,10 +66,37 @@ def set_due_date_dialog(
def forget_cards(
- *, parent: QWidget, card_ids: Sequence[CardId]
-) -> CollectionOp[OpChanges]:
+ *,
+ parent: QWidget,
+ card_ids: Sequence[CardId],
+ context: ScheduleCardsAsNew.Context.V | None = None,
+) -> CollectionOp[OpChanges] | None:
+ assert aqt.mw
+
+ dialog = QDialog(parent)
+ disable_help_button(dialog)
+ form = aqt.forms.forget.Ui_Dialog()
+ form.setupUi(dialog)
+
+ if context is not None:
+ defaults = aqt.mw.col.sched.schedule_cards_as_new_defaults(context)
+ form.restore_position.setChecked(defaults.restore_position)
+ form.reset_counts.setChecked(defaults.reset_counts)
+
+ if not dialog.exec():
+ return None
+
+ restore_position = form.restore_position.isChecked()
+ reset_counts = form.reset_counts.isChecked()
+
return CollectionOp(
- parent, lambda col: col.sched.schedule_cards_as_new(card_ids)
+ parent,
+ lambda col: col.sched.schedule_cards_as_new(
+ card_ids,
+ restore_position=restore_position,
+ reset_counts=reset_counts,
+ context=context,
+ ),
).success(
lambda _: tooltip(
tr.scheduling_forgot_cards(cards=len(card_ids)), parent=parent
diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py
index b0b0e3b67..a6a35a350 100644
--- a/qt/aqt/reviewer.py
+++ b/qt/aqt/reviewer.py
@@ -19,6 +19,7 @@ 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
from anki.scheduler.v3 import CardAnswer, NextStates, QueuedCards
from anki.scheduler.v3 import Scheduler as V3Scheduler
from anki.tags import MARKED_TAG
@@ -1067,10 +1068,12 @@ time = %(time)d;
).run_in_background()
def forget_current_card(self) -> None:
- forget_cards(
+ if op := forget_cards(
parent=self.mw,
card_ids=[self.card.id],
- ).run_in_background()
+ context=ScheduleCardsAsNew.Context.REVIEWER,
+ ):
+ op.run_in_background()
def on_create_copy(self) -> None:
if self.card:
diff --git a/rslib/src/backend/config.rs b/rslib/src/backend/config.rs
index b4eb8fe4a..912db2f0d 100644
--- a/rslib/src/backend/config.rs
+++ b/rslib/src/backend/config.rs
@@ -32,6 +32,10 @@ impl From for BoolKey {
BoolKeyProto::PasteStripsFormatting => BoolKey::PasteStripsFormatting,
BoolKeyProto::NormalizeNoteText => BoolKey::NormalizeNoteText,
BoolKeyProto::IgnoreAccentsInSearch => BoolKey::IgnoreAccentsInSearch,
+ BoolKeyProto::RestorePositionBrowser => BoolKey::RestorePositionBrowser,
+ BoolKeyProto::RestorePositionReviewer => BoolKey::RestorePositionReviewer,
+ BoolKeyProto::ResetCountsBrowser => BoolKey::ResetCountsBrowser,
+ BoolKeyProto::ResetCountsReviewer => BoolKey::ResetCountsReviewer,
}
}
}
diff --git a/rslib/src/backend/scheduler/mod.rs b/rslib/src/backend/scheduler/mod.rs
index 77625ab1b..341431f3b 100644
--- a/rslib/src/backend/scheduler/mod.rs
+++ b/rslib/src/backend/scheduler/mod.rs
@@ -7,7 +7,7 @@ mod states;
use super::Backend;
pub(super) use crate::backend_proto::scheduler_service::Service as SchedulerService;
use crate::{
- backend_proto::{self as pb},
+ backend_proto as pb,
prelude::*,
scheduler::{
new::NewCardDueOrder,
@@ -111,11 +111,26 @@ impl SchedulerService for Backend {
fn schedule_cards_as_new(&self, input: pb::ScheduleCardsAsNewRequest) -> Result {
self.with_col(|col| {
let cids = input.card_ids.into_newtype(CardId);
- let log = input.log;
- col.reschedule_cards_as_new(&cids, log).map(Into::into)
+ col.reschedule_cards_as_new(
+ &cids,
+ input.log,
+ input.restore_position,
+ input.reset_counts,
+ input
+ .context
+ .and_then(pb::schedule_cards_as_new_request::Context::from_i32),
+ )
+ .map(Into::into)
})
}
+ fn schedule_cards_as_new_defaults(
+ &self,
+ input: pb::ScheduleCardsAsNewDefaultsRequest,
+ ) -> Result {
+ self.with_col(|col| Ok(col.reschedule_cards_as_new_defaults(input.context())))
+ }
+
fn set_due_date(&self, input: pb::SetDueDateRequest) -> Result {
let config = input.config_key.map(|v| v.key().into());
let days = input.days;
diff --git a/rslib/src/config/bool.rs b/rslib/src/config/bool.rs
index 0d5aafc9a..3820e4095 100644
--- a/rslib/src/config/bool.rs
+++ b/rslib/src/config/bool.rs
@@ -27,6 +27,10 @@ pub enum BoolKey {
PreviewBothSides,
Sched2021,
IgnoreAccentsInSearch,
+ RestorePositionBrowser,
+ RestorePositionReviewer,
+ ResetCountsBrowser,
+ ResetCountsReviewer,
#[strum(to_string = "normalize_note_text")]
NormalizeNoteText,
@@ -55,6 +59,8 @@ impl Collection {
| BoolKey::FutureDueShowBacklog
| BoolKey::ShowRemainingDueCountsInStudy
| BoolKey::CardCountsSeparateInactive
+ | BoolKey::RestorePositionBrowser
+ | BoolKey::RestorePositionReviewer
| BoolKey::NormalizeNoteText => self.get_config_optional(key).unwrap_or(true),
// other options default to false
diff --git a/rslib/src/scheduler/new.rs b/rslib/src/scheduler/new.rs
index 5c39344cc..b40d06e8a 100644
--- a/rslib/src/scheduler/new.rs
+++ b/rslib/src/scheduler/new.rs
@@ -5,16 +5,20 @@ use std::collections::{HashMap, HashSet};
use rand::seq::SliceRandom;
+pub use crate::backend_proto::scheduler::{
+ schedule_cards_as_new_request::Context as ScheduleAsNewContext,
+ ScheduleCardsAsNewDefaultsResponse,
+};
use crate::{
card::{CardQueue, CardType},
- config::SchedulerVersion,
+ config::{BoolKey, SchedulerVersion},
deckconfig::NewCardInsertOrder,
prelude::*,
search::{SearchNode, SortMode, StateKind},
};
impl Card {
- fn schedule_as_new(&mut self, position: u32) {
+ fn schedule_as_new(&mut self, position: u32, reset_counts: bool) {
self.remove_from_filtered_deck_before_reschedule();
self.due = position as i32;
self.ctype = CardType::New;
@@ -22,6 +26,10 @@ impl Card {
self.interval = 0;
self.ease_factor = 0;
self.original_position = None;
+ if reset_counts {
+ self.reps = 0;
+ self.lapses = 0;
+ }
}
/// If the card is new, change its position, and return true.
@@ -112,7 +120,14 @@ fn nids_in_preserved_order(cards: &[Card]) -> Vec {
}
impl Collection {
- pub fn reschedule_cards_as_new(&mut self, cids: &[CardId], log: bool) -> Result> {
+ pub fn reschedule_cards_as_new(
+ &mut self,
+ cids: &[CardId],
+ log: bool,
+ restore_position: bool,
+ reset_counts: bool,
+ context: Option,
+ ) -> Result> {
let usn = self.usn()?;
let mut position = self.get_next_card_position();
self.transact(Op::ScheduleAsNew, |col| {
@@ -120,18 +135,52 @@ impl Collection {
let cards = col.storage.all_searched_cards_in_search_order()?;
for mut card in cards {
let original = card.clone();
- card.schedule_as_new(position);
+ if restore_position && card.original_position.is_some() {
+ card.schedule_as_new(card.original_position.unwrap(), reset_counts);
+ } else {
+ card.schedule_as_new(position, reset_counts);
+ position += 1;
+ }
if log {
col.log_manually_scheduled_review(&card, &original, usn)?;
}
col.update_card_inner(&mut card, original, usn)?;
- position += 1;
}
col.set_next_card_position(position)?;
- col.storage.clear_searched_cards_table()
+ col.storage.clear_searched_cards_table()?;
+
+ match context {
+ Some(ScheduleAsNewContext::Browser) => {
+ col.set_config_bool_inner(BoolKey::RestorePositionBrowser, restore_position)?;
+ col.set_config_bool_inner(BoolKey::ResetCountsBrowser, reset_counts)?;
+ }
+ Some(ScheduleAsNewContext::Reviewer) => {
+ col.set_config_bool_inner(BoolKey::RestorePositionReviewer, restore_position)?;
+ col.set_config_bool_inner(BoolKey::ResetCountsReviewer, reset_counts)?;
+ }
+ None => (),
+ }
+
+ Ok(())
})
}
+ pub fn reschedule_cards_as_new_defaults(
+ &self,
+ context: ScheduleAsNewContext,
+ ) -> ScheduleCardsAsNewDefaultsResponse {
+ match context {
+ ScheduleAsNewContext::Browser => ScheduleCardsAsNewDefaultsResponse {
+ restore_position: self.get_config_bool(BoolKey::RestorePositionBrowser),
+ reset_counts: self.get_config_bool(BoolKey::ResetCountsBrowser),
+ },
+ ScheduleAsNewContext::Reviewer => ScheduleCardsAsNewDefaultsResponse {
+ restore_position: self.get_config_bool(BoolKey::RestorePositionReviewer),
+ reset_counts: self.get_config_bool(BoolKey::ResetCountsReviewer),
+ },
+ }
+ }
+
pub fn sort_cards(
&mut self,
cids: &[CardId],