diff --git a/ftl/core/browsing.ftl b/ftl/core/browsing.ftl
index e07435e9c..89755266c 100644
--- a/ftl/core/browsing.ftl
+++ b/ftl/core/browsing.ftl
@@ -8,6 +8,7 @@ browsing-answer = Answer
browsing-any-cards-mapped-to-nothing-will = Any cards mapped to nothing will be deleted. If a note has no remaining cards, it will be lost. Are you sure you want to continue?
browsing-any-flag = Any Flag
browsing-average-ease = Average Ease
+browsing-average-interval = Average Interval
browsing-browser-appearance = Browser Appearance
browsing-browser-options = Browser Options
browsing-buried = Buried
@@ -100,6 +101,7 @@ browsing-suspended = Suspended
browsing-tag-duplicates = Tag Duplicates
browsing-tag-rename-warning-empty = You can't rename a tag that has no notes.
browsing-target-field = Target field:
+browsing-toggle-cards-notes-mode = Toggle Cards/Notes Mode
browsing-toggle-mark = Toggle Mark
browsing-toggle-suspend = Toggle Suspend
browsing-treat-input-as-regular-expression = Treat input as regular expression
diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py
index c6f8d09b0..67ebc244c 100644
--- a/qt/aqt/browser.py
+++ b/qt/aqt/browser.py
@@ -167,9 +167,7 @@ class Browser(QMainWindow):
lambda: self.remove_tags_from_selected_notes(),
)
qconnect(f.actionClear_Unused_Tags.triggered, self.clear_unused_tags)
- qconnect(
- f.actionToggle_Mark.triggered, lambda: self.toggle_mark_of_selected_notes()
- )
+ qconnect(f.actionToggle_Mark.triggered, self.toggle_mark_of_selected_notes)
qconnect(f.actionChangeModel.triggered, self.onChangeModel)
qconnect(f.actionFindDuplicates.triggered, self.onFindDupes)
qconnect(f.actionFindReplace.triggered, self.onFindReplace)
@@ -378,6 +376,8 @@ class Browser(QMainWindow):
self.table.set_view(self.form.tableView)
switch = Switch(11, tr.browsing_card_initial(), tr.browsing_note_initial())
switch.setChecked(self.table.is_notes_mode())
+ switch.setToolTip(tr.browsing_toggle_cards_notes_mode())
+ qconnect(self.form.action_toggle_mode.triggered, switch.toggle)
qconnect(switch.toggled, self.on_table_state_changed)
self.form.gridLayout.addWidget(switch, 0, 0)
@@ -408,7 +408,7 @@ class Browser(QMainWindow):
@ensure_editor_saved
def onRowChanged(
- self, current: Optional[QItemSelection], previous: Optional[QItemSelection]
+ self, _current: Optional[QItemSelection], _previous: Optional[QItemSelection]
) -> None:
"""Update current note and hide/show editor. """
if self._closeEventHasCleanedUp:
@@ -428,10 +428,15 @@ class Browser(QMainWindow):
self.editor.card = card
else:
self.editor.set_note(None)
- self._renderPreview()
- self._update_flags_menu()
+ self._renderPreview()
+ self._update_context_actions()
gui_hooks.browser_did_change_row(self)
+ def _update_context_actions(self) -> None:
+ self._update_flags_menu()
+ self._update_toggle_mark_action()
+ self._update_toggle_suspend_action()
+
@ensure_editor_saved
def on_table_state_changed(self, checked: bool) -> None:
self.mw.progress.start()
@@ -725,15 +730,14 @@ where id in %s"""
# Suspending
######################################################################
- def current_card_is_suspended(self) -> bool:
- return bool(self.card and self.card.queue == QUEUE_TYPE_SUSPENDED)
+ def _update_toggle_suspend_action(self) -> None:
+ is_suspended = bool(self.card and self.card.queue == QUEUE_TYPE_SUSPENDED)
+ self.form.actionToggle_Suspend.setChecked(is_suspended)
- @ensure_editor_saved_on_trigger
- def suspend_selected_cards(self) -> None:
- want_suspend = not self.current_card_is_suspended()
+ @ensure_editor_saved
+ def suspend_selected_cards(self, checked: bool) -> None:
cids = self.selected_cards()
-
- if want_suspend:
+ if checked:
suspend_cards(mw=self.mw, card_ids=cids)
else:
unsuspend_cards(mw=self.mw, card_ids=cids)
@@ -776,12 +780,15 @@ where id in %s"""
qtMenuShortcutWorkaround(self.form.menuFlag)
- def toggle_mark_of_selected_notes(self) -> None:
- have_mark = bool(self.card and self.card.note().has_tag(MARKED_TAG))
- if have_mark:
- self.remove_tags_from_selected_notes(tags=MARKED_TAG)
- else:
+ def toggle_mark_of_selected_notes(self, checked: bool) -> None:
+ if checked:
self.add_tags_to_selected_notes(tags=MARKED_TAG)
+ else:
+ self.remove_tags_from_selected_notes(tags=MARKED_TAG)
+
+ def _update_toggle_mark_action(self) -> None:
+ is_marked = bool(self.card and self.card.note().has_tag(MARKED_TAG))
+ self.form.actionToggle_Mark.setChecked(is_marked)
# Scheduling
######################################################################
diff --git a/qt/aqt/forms/browser.ui b/qt/aqt/forms/browser.ui
index 76217999f..810724b33 100644
--- a/qt/aqt/forms/browser.ui
+++ b/qt/aqt/forms/browser.ui
@@ -218,6 +218,8 @@
+
+
@@ -467,6 +469,9 @@
+
+ true
+
browsing_toggle_suspend
@@ -561,6 +566,9 @@
+
+ true
+
browsing_toggle_mark
@@ -597,6 +605,14 @@
qt_accel_forget
+
+
+ browsing_toggle_cards_notes_mode
+
+
+ Ctrl+M
+
+
diff --git a/qt/aqt/switch.py b/qt/aqt/switch.py
index 83afdcd99..cd6590dac 100644
--- a/qt/aqt/switch.py
+++ b/qt/aqt/switch.py
@@ -9,7 +9,9 @@ from aqt.theme import theme_manager
class Switch(QAbstractButton):
"""A horizontal slider to toggle between two states which can be denoted by short strings.
- The left state is the default and corresponds to isChecked=False.
+
+ The left state is the default and corresponds to isChecked()=False.
+ The suppoorted slots are toggle(), for an animated transition, and setChecked().
"""
_margin: int = 2
@@ -91,7 +93,7 @@ class Switch(QAbstractButton):
)
def _paint_knob(self, painter: QPainter) -> None:
- painter.setBrush(QBrush(theme_manager.qcolor(colors.HIGHLIGHT_BG)))
+ painter.setBrush(QBrush(theme_manager.qcolor(colors.LINK)))
painter.drawEllipse(self._current_knob_rectangle())
def _paint_label(self, painter: QPainter) -> None:
@@ -104,12 +106,20 @@ class Switch(QAbstractButton):
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
super().mouseReleaseEvent(event)
if event.button() == Qt.LeftButton:
- animation = QPropertyAnimation(self, b"position", self)
- animation.setDuration(100)
- animation.setStartValue(self.start_position)
- animation.setEndValue(self.end_position)
- animation.start()
+ self._animate_toggle()
def enterEvent(self, event: QEvent) -> None:
self.setCursor(Qt.PointingHandCursor)
super().enterEvent(event)
+
+ def toggle(self) -> None:
+ super().toggle()
+ self._animate_toggle()
+
+ def _animate_toggle(self) -> None:
+ animation = QPropertyAnimation(self, b"position", self)
+ animation.setDuration(100)
+ animation.setStartValue(self.start_position)
+ animation.setEndValue(self.end_position)
+ # make triggered events execute first so the animation runs smoothly afterwards
+ QTimer.singleShot(50, animation.start)
diff --git a/qt/aqt/table.py b/qt/aqt/table.py
index f7141298c..2e41d37e9 100644
--- a/qt/aqt/table.py
+++ b/qt/aqt/table.py
@@ -717,8 +717,10 @@ class NoteState(ItemState):
("note", tr.browsing_note()),
("noteCards", tr.editing_cards()),
("noteCrt", tr.browsing_created()),
+ ("noteDue", tr.statistics_due_date()),
("noteEase", tr.browsing_average_ease()),
("noteFld", tr.browsing_sort_field()),
+ ("noteIvl", tr.browsing_average_interval()),
("noteLapses", tr.scheduling_lapses()),
("noteMod", tr.search_note_modified()),
("noteReps", tr.scheduling_reviews()),
diff --git a/rslib/backend.proto b/rslib/backend.proto
index c8fd113f4..2ca96464b 100644
--- a/rslib/backend.proto
+++ b/rslib/backend.proto
@@ -811,21 +811,23 @@ message SortOrder {
enum Kind {
NOTE_CARDS = 0;
NOTE_CREATION = 1;
- NOTE_EASE = 2;
- NOTE_FIELD = 3;
- NOTE_LAPSES = 4;
- NOTE_MOD = 5;
- NOTE_REPS = 6;
- NOTE_TAGS = 7;
- NOTETYPE = 8;
- CARD_MOD = 9;
- CARD_REPS = 10;
- CARD_DUE = 11;
- CARD_EASE = 12;
- CARD_LAPSES = 13;
- CARD_INTERVAL = 14;
- CARD_DECK = 15;
- CARD_TEMPLATE = 16;
+ NOTE_DUE = 2;
+ NOTE_EASE = 3;
+ NOTE_FIELD = 4;
+ NOTE_INTERVAL = 5;
+ NOTE_LAPSES = 6;
+ NOTE_MOD = 7;
+ NOTE_REPS = 8;
+ NOTE_TAGS = 9;
+ NOTETYPE = 10;
+ CARD_MOD = 11;
+ CARD_REPS = 12;
+ CARD_DUE = 13;
+ CARD_EASE = 14;
+ CARD_LAPSES = 15;
+ CARD_INTERVAL = 16;
+ CARD_DECK = 17;
+ CARD_TEMPLATE = 18;
}
Kind kind = 1;
bool reverse = 2;
diff --git a/rslib/src/backend/search/browser_table.rs b/rslib/src/backend/search/browser_table.rs
index 768de1dad..cb405769a 100644
--- a/rslib/src/backend/search/browser_table.rs
+++ b/rslib/src/backend/search/browser_table.rs
@@ -24,8 +24,10 @@ impl From for browser_table::Column {
"template" => browser_table::Column::CardTemplate,
"noteCards" => browser_table::Column::NoteCards,
"noteCrt" => browser_table::Column::NoteCreation,
+ "noteDue" => browser_table::Column::NoteDue,
"noteEase" => browser_table::Column::NoteEase,
"noteFld" => browser_table::Column::NoteField,
+ "noteIvl" => browser_table::Column::NoteInterval,
"noteLapses" => browser_table::Column::NoteLapses,
"noteMod" => browser_table::Column::NoteMod,
"noteReps" => browser_table::Column::NoteReps,
diff --git a/rslib/src/backend/search/mod.rs b/rslib/src/backend/search/mod.rs
index e5acfb948..5aaf25780 100644
--- a/rslib/src/backend/search/mod.rs
+++ b/rslib/src/backend/search/mod.rs
@@ -109,7 +109,9 @@ impl From for SortKind {
match kind {
SortKindProto::NoteCards => SortKind::NoteCards,
SortKindProto::NoteCreation => SortKind::NoteCreation,
+ SortKindProto::NoteDue => SortKind::NoteDue,
SortKindProto::NoteEase => SortKind::NoteEase,
+ SortKindProto::NoteInterval => SortKind::NoteInterval,
SortKindProto::NoteLapses => SortKind::NoteLapses,
SortKindProto::NoteMod => SortKind::NoteMod,
SortKindProto::NoteField => SortKind::NoteField,
diff --git a/rslib/src/browser_table.rs b/rslib/src/browser_table.rs
index e699c8257..c1fb265cc 100644
--- a/rslib/src/browser_table.rs
+++ b/rslib/src/browser_table.rs
@@ -24,26 +24,28 @@ use crate::{
#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, Clone, Copy)]
#[repr(u8)]
pub enum Column {
- Custom = 0,
- Question = 1,
- Answer = 2,
- CardDeck = 3,
- CardDue = 4,
- CardEase = 5,
- CardLapses = 6,
- CardInterval = 7,
- CardMod = 8,
- CardReps = 9,
- CardTemplate = 10,
- NoteCards = 11,
- NoteCreation = 12,
- NoteEase = 13,
- NoteField = 14,
- NoteLapses = 15,
- NoteMod = 16,
- NoteReps = 17,
- NoteTags = 18,
- Notetype = 19,
+ Custom,
+ Question,
+ Answer,
+ CardDeck,
+ CardDue,
+ CardEase,
+ CardLapses,
+ CardInterval,
+ CardMod,
+ CardReps,
+ CardTemplate,
+ NoteCards,
+ NoteCreation,
+ NoteDue,
+ NoteEase,
+ NoteField,
+ NoteInterval,
+ NoteLapses,
+ NoteMod,
+ NoteReps,
+ NoteTags,
+ Notetype,
}
#[derive(Debug, PartialEq)]
@@ -77,13 +79,13 @@ pub struct Font {
}
trait RowContext {
- fn get_cell_text(&mut self, column: &Column) -> Result;
+ fn get_cell_text(&mut self, column: Column) -> Result;
fn get_row_color(&self) -> Color;
fn get_row_font(&self) -> Result;
fn note(&self) -> &Note;
fn notetype(&self) -> &Notetype;
- fn get_cell(&mut self, column: &Column) -> Result {
+ fn get_cell(&mut self, column: Column) -> Result {
Ok(Cell {
text: self.get_cell_text(column)?,
is_rtl: self.get_is_rtl(column),
@@ -101,7 +103,7 @@ trait RowContext {
html_to_text_line(&self.note().fields()[index]).into()
}
- fn get_is_rtl(&self, column: &Column) -> bool {
+ fn get_is_rtl(&self, column: Column) -> bool {
match column {
Column::NoteField => {
let index = self.notetype().config.sort_field_idx as usize;
@@ -115,7 +117,7 @@ trait RowContext {
Ok(Row {
cells: columns
.iter()
- .map(|column| self.get_cell(column))
+ .map(|&column| self.get_cell(column))
.collect::>()?,
color: self.get_row_color(),
font: self.get_row_font()?,
@@ -147,6 +149,7 @@ struct NoteRowContext<'a> {
notetype: Arc,
cards: Vec,
tr: &'a I18n,
+ timing: SchedTimingToday,
}
fn card_render_required(columns: &[Column]) -> bool {
@@ -155,6 +158,49 @@ fn card_render_required(columns: &[Column]) -> bool {
.any(|c| matches!(c, Column::Question | Column::Answer))
}
+impl Card {
+ fn is_new_type_or_queue(&self) -> bool {
+ self.queue == CardQueue::New || self.ctype == CardType::New
+ }
+
+ fn is_filtered_deck(&self) -> bool {
+ self.original_deck_id != DeckId(0)
+ }
+
+ /// Returns true if the card can not be due as it's buried or suspended.
+ fn is_undue_queue(&self) -> bool {
+ (self.queue as i8) < 0
+ }
+
+ /// Returns true if the card has a due date in terms of days.
+ fn is_due_in_days(&self) -> bool {
+ matches!(self.queue, CardQueue::DayLearn | CardQueue::Review)
+ || (self.ctype == CardType::Review && self.is_undue_queue())
+ }
+
+ /// Returns the card's due date as a timestamp if it has one.
+ fn due_time(&self, timing: &SchedTimingToday) -> Option {
+ if self.queue == CardQueue::Learn {
+ Some(TimestampSecs(self.due as i64))
+ } else if self.is_due_in_days() {
+ Some(
+ TimestampSecs::now()
+ .adding_secs(((self.due - timing.days_elapsed as i32) * 86400) as i64),
+ )
+ } else {
+ None
+ }
+ }
+}
+
+impl Note {
+ fn is_marked(&self) -> bool {
+ self.tags
+ .iter()
+ .any(|tag| tag.eq_ignore_ascii_case("marked"))
+ }
+}
+
impl Collection {
pub fn browser_row_for_id(&mut self, id: i64) -> Result {
if self.get_bool(BoolKey::BrowserTableShowNotesMode) {
@@ -297,25 +343,16 @@ impl<'a> CardRowContext<'a> {
}
fn card_due_str(&mut self) -> String {
- let due = if self.card.original_deck_id != DeckId(0) {
+ let due = if self.card.is_filtered_deck() {
self.tr.browsing_filtered()
- } else if self.card.queue == CardQueue::New || self.card.ctype == CardType::New {
+ } else if self.card.is_new_type_or_queue() {
self.tr.statistics_due_for_new_card(self.card.due)
+ } else if let Some(time) = self.card.due_time(&self.timing) {
+ time.date_string().into()
} else {
- let date = if self.card.queue == CardQueue::Learn {
- TimestampSecs(self.card.due as i64)
- } else if self.card.queue == CardQueue::DayLearn
- || self.card.queue == CardQueue::Review
- || (self.card.ctype == CardType::Review && (self.card.queue as i8) < 0)
- {
- TimestampSecs::now()
- .adding_secs(((self.card.due - self.timing.days_elapsed as i32) * 86400) as i64)
- } else {
- return "".into();
- };
- date.date_string().into()
+ return "".into();
};
- if (self.card.queue as i8) < 0 {
+ if self.card.is_undue_queue() {
format!("({})", due)
} else {
due.into()
@@ -360,7 +397,7 @@ impl<'a> CardRowContext<'a> {
}
impl RowContext for CardRowContext<'_> {
- fn get_cell_text(&mut self, column: &Column) -> Result {
+ fn get_cell_text(&mut self, column: Column) -> Result {
Ok(match column {
Column::Question => self.question_str(),
Column::Answer => self.answer_str(),
@@ -388,12 +425,7 @@ impl RowContext for CardRowContext<'_> {
3 => Color::FlagGreen,
4 => Color::FlagBlue,
_ => {
- if self
- .note
- .tags
- .iter()
- .any(|tag| tag.eq_ignore_ascii_case("marked"))
- {
+ if self.note.is_marked() {
Color::Marked
} else if self.card.queue == CardQueue::Suspended {
Color::Suspended
@@ -427,37 +459,74 @@ impl<'a> NoteRowContext<'a> {
.get_notetype(note.notetype_id)?
.ok_or(AnkiError::NotFound)?;
let cards = col.storage.all_cards_of_note(note.id)?;
+ let timing = col.timing_today()?;
Ok(NoteRowContext {
note,
notetype,
cards,
tr: &col.tr,
+ timing,
})
}
+ /// Returns the average ease of the non-new cards or a hint if there aren't any.
fn note_ease_str(&self) -> String {
- let cards = self
+ let eases: Vec = self
.cards
.iter()
.filter(|c| c.ctype != CardType::New)
- .collect::>();
- if cards.is_empty() {
+ .map(|c| c.ease_factor)
+ .collect();
+ if eases.is_empty() {
self.tr.browsing_new().into()
} else {
- let ease = cards.iter().map(|c| c.ease_factor).sum::() / cards.len() as u16;
- format!("{}%", ease / 10)
+ format!("{}%", eases.iter().sum::() / eases.len() as u16 / 10)
+ }
+ }
+
+ /// Returns the due date of the next due card that is not in a filtered deck, new, suspended or
+ /// buried or the empty string if there is no such card.
+ fn note_due_str(&self) -> String {
+ self.cards
+ .iter()
+ .filter(|c| !(c.is_filtered_deck() || c.is_new_type_or_queue() || c.is_undue_queue()))
+ .filter_map(|c| c.due_time(&self.timing))
+ .min()
+ .map(|time| time.date_string())
+ .unwrap_or_else(|| "".into())
+ }
+
+ /// Returns the average interval of the review and relearn cards or the empty string if there
+ /// aren't any.
+ fn note_interval_str(&self) -> String {
+ let intervals: Vec = self
+ .cards
+ .iter()
+ .filter(|c| matches!(c.ctype, CardType::Review | CardType::Relearn))
+ .map(|c| c.interval)
+ .collect();
+ if intervals.is_empty() {
+ "".into()
+ } else {
+ time_span(
+ (intervals.iter().sum::() * 86400 / (intervals.len() as u32)) as f32,
+ self.tr,
+ false,
+ )
}
}
}
impl RowContext for NoteRowContext<'_> {
- fn get_cell_text(&mut self, column: &Column) -> Result {
+ fn get_cell_text(&mut self, column: Column) -> Result {
Ok(match column {
Column::NoteCards => self.cards.len().to_string(),
Column::NoteCreation => self.note_creation_str(),
+ Column::NoteDue => self.note_due_str(),
Column::NoteEase => self.note_ease_str(),
Column::NoteField => self.note_field_str(),
+ Column::NoteInterval => self.note_interval_str(),
Column::NoteLapses => self.cards.iter().map(|c| c.lapses).sum::().to_string(),
Column::NoteMod => self.note.mtime.date_string(),
Column::NoteReps => self.cards.iter().map(|c| c.reps).sum::().to_string(),
@@ -468,12 +537,7 @@ impl RowContext for NoteRowContext<'_> {
}
fn get_row_color(&self) -> Color {
- if self
- .note
- .tags
- .iter()
- .any(|tag| tag.eq_ignore_ascii_case("marked"))
- {
+ if self.note.is_marked() {
Color::Marked
} else {
Color::Default
diff --git a/rslib/src/config/mod.rs b/rslib/src/config/mod.rs
index 3b39ca46a..a076934f3 100644
--- a/rslib/src/config/mod.rs
+++ b/rslib/src/config/mod.rs
@@ -271,7 +271,10 @@ pub enum SortKind {
NoteCards,
#[serde(rename = "noteCrt")]
NoteCreation,
+ NoteDue,
NoteEase,
+ #[serde(rename = "noteIvl")]
+ NoteInterval,
NoteLapses,
NoteMod,
#[serde(rename = "noteFld")]
diff --git a/rslib/src/search/mod.rs b/rslib/src/search/mod.rs
index 05619cf45..d3fa0a6ec 100644
--- a/rslib/src/search/mod.rs
+++ b/rslib/src/search/mod.rs
@@ -90,8 +90,10 @@ impl SortKind {
match self {
SortKind::NoteCards
| SortKind::NoteCreation
+ | SortKind::NoteDue
| SortKind::NoteEase
| SortKind::NoteField
+ | SortKind::NoteInterval
| SortKind::NoteLapses
| SortKind::NoteMod
| SortKind::NoteReps
@@ -252,9 +254,12 @@ fn card_order_from_sortkind(kind: SortKind) -> Cow<'static, str> {
fn note_order_from_sortkind(kind: SortKind) -> Cow<'static, str> {
match kind {
- SortKind::NoteCards | SortKind::NoteEase | SortKind::NoteLapses | SortKind::NoteReps => {
- "(select pos from sort_order where nid = n.id) asc".into()
- }
+ SortKind::NoteCards
+ | SortKind::NoteDue
+ | SortKind::NoteEase
+ | SortKind::NoteInterval
+ | SortKind::NoteLapses
+ | SortKind::NoteReps => "(select pos from sort_order where nid = n.id) asc".into(),
SortKind::NoteCreation => "n.id asc".into(),
SortKind::NoteField => "n.sfld collate nocase asc".into(),
SortKind::NoteMod => "n.mod asc".into(),
@@ -270,7 +275,9 @@ fn prepare_sort(col: &mut Collection, kind: SortKind) -> Result<()> {
CardDeck => include_str!("deck_order.sql"),
CardTemplate => include_str!("template_order.sql"),
NoteCards => include_str!("note_cards_order.sql"),
+ NoteDue => include_str!("note_due_order.sql"),
NoteEase => include_str!("note_ease_order.sql"),
+ NoteInterval => include_str!("note_interval_order.sql"),
NoteLapses => include_str!("note_lapses_order.sql"),
NoteReps => include_str!("note_reps_order.sql"),
Notetype => include_str!("notetype_order.sql"),
diff --git a/rslib/src/search/note_due_order.sql b/rslib/src/search/note_due_order.sql
new file mode 100644
index 000000000..1648295f4
--- /dev/null
+++ b/rslib/src/search/note_due_order.sql
@@ -0,0 +1,16 @@
+DROP TABLE IF EXISTS sort_order;
+CREATE TEMPORARY TABLE sort_order (
+ pos integer PRIMARY KEY,
+ nid integer NOT NULL UNIQUE
+);
+INSERT INTO sort_order (nid)
+SELECT nid
+FROM cards
+WHERE (
+ odid = 0
+ AND type != 0
+ AND queue > 0
+ )
+GROUP BY nid
+ORDER BY MIN(type),
+ MIN(due);
\ No newline at end of file
diff --git a/rslib/src/search/note_interval_order.sql b/rslib/src/search/note_interval_order.sql
new file mode 100644
index 000000000..375bf10bf
--- /dev/null
+++ b/rslib/src/search/note_interval_order.sql
@@ -0,0 +1,11 @@
+DROP TABLE IF EXISTS sort_order;
+CREATE TEMPORARY TABLE sort_order (
+ pos integer PRIMARY KEY,
+ nid integer NOT NULL UNIQUE
+);
+INSERT INTO sort_order (nid)
+SELECT nid
+FROM cards
+WHERE type IN (2, 3)
+GROUP BY nid
+ORDER BY AVG(ivl);
\ No newline at end of file
| |