mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
Add card position column and always show position in card info (#3471)
* Expose original position to columns and card info * Fix contributors * Change routine name and return, fix SQL file, utilize coalesce inline * Handle cards without original position * Remove unecessary conversion
This commit is contained in:
parent
a982720a42
commit
7439c657f0
5 changed files with 81 additions and 28 deletions
|
@ -46,6 +46,7 @@ pub enum Column {
|
||||||
NoteMod,
|
NoteMod,
|
||||||
#[strum(serialize = "note")]
|
#[strum(serialize = "note")]
|
||||||
Notetype,
|
Notetype,
|
||||||
|
OriginalPosition,
|
||||||
Question,
|
Question,
|
||||||
#[strum(serialize = "cardReps")]
|
#[strum(serialize = "cardReps")]
|
||||||
Reps,
|
Reps,
|
||||||
|
@ -161,6 +162,7 @@ impl Column {
|
||||||
Self::NoteCreation => tr.browsing_created(),
|
Self::NoteCreation => tr.browsing_created(),
|
||||||
Self::NoteMod => tr.search_note_modified(),
|
Self::NoteMod => tr.search_note_modified(),
|
||||||
Self::Notetype => tr.card_stats_note_type(),
|
Self::Notetype => tr.card_stats_note_type(),
|
||||||
|
Self::OriginalPosition => tr.card_stats_new_card_position(),
|
||||||
Self::Question => tr.browsing_question(),
|
Self::Question => tr.browsing_question(),
|
||||||
Self::Reps => tr.scheduling_reviews(),
|
Self::Reps => tr.scheduling_reviews(),
|
||||||
Self::SortField => tr.browsing_sort_field(),
|
Self::SortField => tr.browsing_sort_field(),
|
||||||
|
@ -226,6 +228,7 @@ impl Column {
|
||||||
| Column::Interval
|
| Column::Interval
|
||||||
| Column::NoteCreation
|
| Column::NoteCreation
|
||||||
| Column::NoteMod
|
| Column::NoteMod
|
||||||
|
| Column::OriginalPosition
|
||||||
| Column::Reps => Sorting::Descending,
|
| Column::Reps => Sorting::Descending,
|
||||||
Column::Stability | Column::Difficulty | Column::Retrievability => {
|
Column::Stability | Column::Difficulty | Column::Retrievability => {
|
||||||
if notes {
|
if notes {
|
||||||
|
@ -432,6 +435,7 @@ impl RowContext {
|
||||||
Column::NoteCreation => self.note_creation_str(),
|
Column::NoteCreation => self.note_creation_str(),
|
||||||
Column::SortField => self.note_field_str(),
|
Column::SortField => self.note_field_str(),
|
||||||
Column::NoteMod => self.note.mtime.date_and_time_string(),
|
Column::NoteMod => self.note.mtime.date_and_time_string(),
|
||||||
|
Column::OriginalPosition => self.card_original_position(),
|
||||||
Column::Tags => self.note.tags.join(" "),
|
Column::Tags => self.note.tags.join(" "),
|
||||||
Column::Notetype => self.notetype.name.to_owned(),
|
Column::Notetype => self.notetype.name.to_owned(),
|
||||||
Column::Stability => self.fsrs_stability_str(),
|
Column::Stability => self.fsrs_stability_str(),
|
||||||
|
@ -441,6 +445,17 @@ impl RowContext {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn card_original_position(&self) -> String {
|
||||||
|
let card = &self.cards[0];
|
||||||
|
if let Some(pos) = &card.original_position {
|
||||||
|
pos.to_string()
|
||||||
|
} else if card.ctype == CardType::New {
|
||||||
|
card.due.to_string()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn note_creation_str(&self) -> String {
|
fn note_creation_str(&self) -> String {
|
||||||
TimestampMillis(self.note.id.into())
|
TimestampMillis(self.note.id.into())
|
||||||
.as_secs()
|
.as_secs()
|
||||||
|
|
|
@ -370,6 +370,7 @@ fn card_order_from_sort_column(column: Column, timing: SchedTimingToday) -> Cow<
|
||||||
Column::NoteCreation => "n.id asc, c.ord asc".into(),
|
Column::NoteCreation => "n.id asc, c.ord asc".into(),
|
||||||
Column::NoteMod => "n.mod asc, c.ord asc".into(),
|
Column::NoteMod => "n.mod asc, c.ord asc".into(),
|
||||||
Column::Notetype => "(select pos from sort_order where ntid = n.mid) asc".into(),
|
Column::Notetype => "(select pos from sort_order where ntid = n.mid) asc".into(),
|
||||||
|
Column::OriginalPosition => "(select pos from sort_order where nid = c.nid) asc".into(),
|
||||||
Column::Reps => "c.reps asc".into(),
|
Column::Reps => "c.reps asc".into(),
|
||||||
Column::SortField => "n.sfld collate nocase asc, c.ord asc".into(),
|
Column::SortField => "n.sfld collate nocase asc, c.ord asc".into(),
|
||||||
Column::Tags => "n.tags asc".into(),
|
Column::Tags => "n.tags asc".into(),
|
||||||
|
@ -394,6 +395,7 @@ fn note_order_from_sort_column(column: Column) -> Cow<'static, str> {
|
||||||
| Column::Ease
|
| Column::Ease
|
||||||
| Column::Interval
|
| Column::Interval
|
||||||
| Column::Lapses
|
| Column::Lapses
|
||||||
|
| Column::OriginalPosition
|
||||||
| Column::Reps => "(select pos from sort_order where nid = n.id) asc".into(),
|
| Column::Reps => "(select pos from sort_order where nid = n.id) asc".into(),
|
||||||
Column::NoteCreation => "n.id asc".into(),
|
Column::NoteCreation => "n.id asc".into(),
|
||||||
Column::NoteMod => "n.mod asc".into(),
|
Column::NoteMod => "n.mod asc".into(),
|
||||||
|
@ -416,6 +418,7 @@ fn prepare_sort(col: &mut Collection, column: Column, item_type: ReturnItemType)
|
||||||
Column::Cards => include_str!("template_order.sql"),
|
Column::Cards => include_str!("template_order.sql"),
|
||||||
Column::Deck => include_str!("deck_order.sql"),
|
Column::Deck => include_str!("deck_order.sql"),
|
||||||
Column::Notetype => include_str!("notetype_order.sql"),
|
Column::Notetype => include_str!("notetype_order.sql"),
|
||||||
|
Column::OriginalPosition => include_str!("note_original_position_order.sql"),
|
||||||
_ => return Ok(()),
|
_ => return Ok(()),
|
||||||
},
|
},
|
||||||
ReturnItemType::Notes => match column {
|
ReturnItemType::Notes => match column {
|
||||||
|
@ -429,6 +432,7 @@ fn prepare_sort(col: &mut Collection, column: Column, item_type: ReturnItemType)
|
||||||
Column::Ease => include_str!("note_ease_order.sql"),
|
Column::Ease => include_str!("note_ease_order.sql"),
|
||||||
Column::Interval => include_str!("note_interval_order.sql"),
|
Column::Interval => include_str!("note_interval_order.sql"),
|
||||||
Column::Lapses => include_str!("note_lapses_order.sql"),
|
Column::Lapses => include_str!("note_lapses_order.sql"),
|
||||||
|
Column::OriginalPosition => include_str!("note_original_position_order.sql"),
|
||||||
Column::Reps => include_str!("note_reps_order.sql"),
|
Column::Reps => include_str!("note_reps_order.sql"),
|
||||||
Column::Notetype => include_str!("notetype_order.sql"),
|
Column::Notetype => include_str!("notetype_order.sql"),
|
||||||
_ => return Ok(()),
|
_ => return Ok(()),
|
||||||
|
|
16
rslib/src/search/note_original_position_order.sql
Normal file
16
rslib/src/search/note_original_position_order.sql
Normal file
|
@ -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
|
||||||
|
GROUP BY nid
|
||||||
|
ORDER BY COALESCE(
|
||||||
|
extract_original_position(data),
|
||||||
|
CASE
|
||||||
|
WHEN type == 0 THEN due
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
);
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
use fsrs::FSRS;
|
use fsrs::FSRS;
|
||||||
|
|
||||||
use crate::card::CardQueue;
|
|
||||||
use crate::card::CardType;
|
use crate::card::CardType;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::revlog::RevlogEntry;
|
use crate::revlog::RevlogEntry;
|
||||||
|
@ -28,7 +27,6 @@ impl Collection {
|
||||||
let revlog = self.storage.get_revlog_entries_for_card(card.id)?;
|
let revlog = self.storage.get_revlog_entries_for_card(card.id)?;
|
||||||
|
|
||||||
let (average_secs, total_secs) = average_and_total_secs_strings(&revlog);
|
let (average_secs, total_secs) = average_and_total_secs_strings(&revlog);
|
||||||
let (due_date, due_position) = self.due_date_and_position(&card)?;
|
|
||||||
let timing = self.timing_today()?;
|
let timing = self.timing_today()?;
|
||||||
let days_elapsed = self
|
let days_elapsed = self
|
||||||
.storage
|
.storage
|
||||||
|
@ -62,8 +60,8 @@ impl Collection {
|
||||||
added: card.id.as_secs().0,
|
added: card.id.as_secs().0,
|
||||||
first_review: revlog.first().map(|entry| entry.id.as_secs().0),
|
first_review: revlog.first().map(|entry| entry.id.as_secs().0),
|
||||||
latest_review: revlog.last().map(|entry| entry.id.as_secs().0),
|
latest_review: revlog.last().map(|entry| entry.id.as_secs().0),
|
||||||
due_date,
|
due_date: self.due_date(&card)?,
|
||||||
due_position,
|
due_position: self.position(&card),
|
||||||
interval: card.interval,
|
interval: card.interval,
|
||||||
ease: card.ease_factor as u32,
|
ease: card.ease_factor as u32,
|
||||||
reviews: card.reps,
|
reviews: card.reps,
|
||||||
|
@ -85,37 +83,33 @@ impl Collection {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn due_date_and_position(&mut self, card: &Card) -> Result<(Option<i64>, Option<i32>)> {
|
fn due_date(&mut self, card: &Card) -> Result<Option<i64>> {
|
||||||
let due = if card.original_due != 0 {
|
|
||||||
card.original_due
|
|
||||||
} else {
|
|
||||||
card.due
|
|
||||||
};
|
|
||||||
Ok(match card.ctype {
|
Ok(match card.ctype {
|
||||||
CardType::New => {
|
CardType::New => None,
|
||||||
if matches!(card.queue, CardQueue::Review | CardQueue::DayLearn) {
|
CardType::Review | CardType::Learn | CardType::Relearn => {
|
||||||
// new preview card not answered yet
|
let due = card.due;
|
||||||
(None, card.original_position.map(|u| u as i32))
|
if !is_unix_epoch_timestamp(due) {
|
||||||
|
let days_remaining = due - (self.timing_today()?.days_elapsed as i32);
|
||||||
|
let mut due_timestamp = TimestampSecs::now();
|
||||||
|
due_timestamp.0 += (days_remaining as i64) * 86_400;
|
||||||
|
Some(due_timestamp.0)
|
||||||
} else {
|
} else {
|
||||||
(None, Some(due))
|
Some(due as i64)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CardType::Review | CardType::Learn | CardType::Relearn => (
|
|
||||||
{
|
|
||||||
if !is_unix_epoch_timestamp(due) {
|
|
||||||
let days_remaining = due - (self.timing_today()?.days_elapsed as i32);
|
|
||||||
let mut due = TimestampSecs::now();
|
|
||||||
due.0 += (days_remaining as i64) * 86_400;
|
|
||||||
Some(due.0)
|
|
||||||
} else {
|
|
||||||
Some(due as i64)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn position(&mut self, card: &Card) -> Option<i32> {
|
||||||
|
if let Some(original_pos) = card.original_position {
|
||||||
|
return Some(original_pos as i32);
|
||||||
|
}
|
||||||
|
match card.ctype {
|
||||||
|
CardType::New => Some(card.due),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn stats_revlog_entries_with_memory_state(
|
fn stats_revlog_entries_with_memory_state(
|
||||||
self: &mut Collection,
|
self: &mut Collection,
|
||||||
card: &Card,
|
card: &Card,
|
||||||
|
|
|
@ -70,6 +70,7 @@ fn open_or_create_collection_db(path: &Path) -> Result<Connection> {
|
||||||
add_regexp_tags_function(&db)?;
|
add_regexp_tags_function(&db)?;
|
||||||
add_without_combining_function(&db)?;
|
add_without_combining_function(&db)?;
|
||||||
add_fnvhash_function(&db)?;
|
add_fnvhash_function(&db)?;
|
||||||
|
add_extract_original_position_function(&db)?;
|
||||||
add_extract_custom_data_function(&db)?;
|
add_extract_custom_data_function(&db)?;
|
||||||
add_extract_fsrs_variable(&db)?;
|
add_extract_fsrs_variable(&db)?;
|
||||||
add_extract_fsrs_retrievability(&db)?;
|
add_extract_fsrs_retrievability(&db)?;
|
||||||
|
@ -205,6 +206,29 @@ fn add_regexp_tags_function(db: &Connection) -> rusqlite::Result<()> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// eg. extract_original_position(c.data) -> number | null
|
||||||
|
/// Parse original card position from c.data (this is only populated after card
|
||||||
|
/// has been reviewed)
|
||||||
|
fn add_extract_original_position_function(db: &Connection) -> rusqlite::Result<()> {
|
||||||
|
db.create_scalar_function(
|
||||||
|
"extract_original_position",
|
||||||
|
1,
|
||||||
|
FunctionFlags::SQLITE_DETERMINISTIC,
|
||||||
|
move |ctx| {
|
||||||
|
assert_eq!(ctx.len(), 1, "called with unexpected number of arguments");
|
||||||
|
|
||||||
|
let Ok(card_data) = ctx.get_raw(0).as_str() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
match &CardData::from_str(card_data).original_position {
|
||||||
|
Some(position) => Ok(Some(*position as i64)),
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// eg. extract_custom_data(card.data, 'r') -> string | null
|
/// eg. extract_custom_data(card.data, 'r') -> string | null
|
||||||
fn add_extract_custom_data_function(db: &Connection) -> rusqlite::Result<()> {
|
fn add_extract_custom_data_function(db: &Connection) -> rusqlite::Result<()> {
|
||||||
db.create_scalar_function(
|
db.create_scalar_function(
|
||||||
|
|
Loading…
Reference in a new issue