mirror of
https://github.com/ankitects/anki.git
synced 2025-09-25 09:16:38 -04:00
Merge branch 'main' of https://github.com/ankitects/anki into fix-duplicate-deck-drag-drop
This commit is contained in:
commit
52347819dd
12 changed files with 131 additions and 59 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -2099,7 +2099,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fsrs"
|
name = "fsrs"
|
||||||
version = "3.0.0"
|
version = "3.0.0"
|
||||||
source = "git+https://github.com/open-spaced-repetition/fsrs-rs.git?rev=22f8e453c120f5bc5996f86558a559c6b7abfc49#22f8e453c120f5bc5996f86558a559c6b7abfc49"
|
source = "git+https://github.com/open-spaced-repetition/fsrs-rs.git?rev=08d90d1363b0c4722422bf0ef71ed8fd7d053f8a#08d90d1363b0c4722422bf0ef71ed8fd7d053f8a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"burn",
|
"burn",
|
||||||
"itertools 0.12.1",
|
"itertools 0.12.1",
|
||||||
|
|
|
@ -37,7 +37,7 @@ rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca"
|
||||||
[workspace.dependencies.fsrs]
|
[workspace.dependencies.fsrs]
|
||||||
# version = "=2.0.3"
|
# version = "=2.0.3"
|
||||||
git = "https://github.com/open-spaced-repetition/fsrs-rs.git"
|
git = "https://github.com/open-spaced-repetition/fsrs-rs.git"
|
||||||
rev = "22f8e453c120f5bc5996f86558a559c6b7abfc49"
|
rev = "08d90d1363b0c4722422bf0ef71ed8fd7d053f8a"
|
||||||
# path = "../open-spaced-repetition/fsrs-rs"
|
# path = "../open-spaced-repetition/fsrs-rs"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "nightly-2023-09-02"
|
channel = "nightly-2025-03-20"
|
||||||
profile = "minimal"
|
profile = "minimal"
|
||||||
components = ["rustfmt"]
|
components = ["rustfmt"]
|
||||||
|
|
|
@ -8,3 +8,5 @@ about-if-you-have-contributed-and-are = If you have contributed and are not on t
|
||||||
about-version = Version { $val }
|
about-version = Version { $val }
|
||||||
about-visit-website = <a href='{ $val }'>Visit website</a>
|
about-visit-website = <a href='{ $val }'>Visit website</a>
|
||||||
about-written-by-damien-elmes-with-patches = Written by Damien Elmes, with patches, translation, testing and design from:<p>{ $cont }
|
about-written-by-damien-elmes-with-patches = Written by Damien Elmes, with patches, translation, testing and design from:<p>{ $cont }
|
||||||
|
# appended to the end of the contributor list in the about screen
|
||||||
|
about-and-others = and others
|
||||||
|
|
|
@ -248,6 +248,10 @@ is_mac = sys.platform.startswith("darwin")
|
||||||
is_win = sys.platform.startswith("win32")
|
is_win = sys.platform.startswith("win32")
|
||||||
# also covers *BSD
|
# also covers *BSD
|
||||||
is_lin = not is_mac and not is_win
|
is_lin = not is_mac and not is_win
|
||||||
|
is_gnome = (
|
||||||
|
"gnome" in os.getenv("XDG_CURRENT_DESKTOP", "").lower()
|
||||||
|
or "gnome" in os.getenv("DESKTOP_SESSION", "").lower()
|
||||||
|
)
|
||||||
dev_mode = os.getenv("ANKIDEV", "")
|
dev_mode = os.getenv("ANKIDEV", "")
|
||||||
hmr_mode = os.getenv("HMR", "")
|
hmr_mode = os.getenv("HMR", "")
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,7 @@ from anki._backend import RustBackend
|
||||||
from anki.buildinfo import version as _version
|
from anki.buildinfo import version as _version
|
||||||
from anki.collection import Collection
|
from anki.collection import Collection
|
||||||
from anki.consts import HELP_SITE
|
from anki.consts import HELP_SITE
|
||||||
from anki.utils import checksum, is_lin, is_mac
|
from anki.utils import checksum, is_gnome, is_lin, is_mac
|
||||||
from aqt import gui_hooks
|
from aqt import gui_hooks
|
||||||
from aqt.log import setup_logging
|
from aqt.log import setup_logging
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
@ -614,7 +614,7 @@ def _run(argv: list[str] | None = None, exec: bool = True) -> AnkiApp | None:
|
||||||
)
|
)
|
||||||
wayland_forced = os.getenv("ANKI_WAYLAND")
|
wayland_forced = os.getenv("ANKI_WAYLAND")
|
||||||
|
|
||||||
if packaged and wayland_configured:
|
if (packaged or is_gnome) and wayland_configured:
|
||||||
if wayland_forced or not x11_available:
|
if wayland_forced or not x11_available:
|
||||||
# Work around broken fractional scaling in Wayland
|
# Work around broken fractional scaling in Wayland
|
||||||
# https://bugreports.qt.io/browse/QTBUG-113574
|
# https://bugreports.qt.io/browse/QTBUG-113574
|
||||||
|
|
|
@ -222,7 +222,7 @@ def show(mw: aqt.AnkiQt) -> QDialog:
|
||||||
)
|
)
|
||||||
|
|
||||||
abouttext += "<p>" + tr.about_written_by_damien_elmes_with_patches(
|
abouttext += "<p>" + tr.about_written_by_damien_elmes_with_patches(
|
||||||
cont=", ".join(allusers)
|
cont=", ".join(allusers) + f", {tr.about_and_others()}"
|
||||||
)
|
)
|
||||||
abouttext += f"<p>{tr.about_if_you_have_contributed_and_are()}"
|
abouttext += f"<p>{tr.about_if_you_have_contributed_and_are()}"
|
||||||
abouttext += f"<p>{tr.about_a_big_thanks_to_all_the()}"
|
abouttext += f"<p>{tr.about_a_big_thanks_to_all_the()}"
|
||||||
|
|
|
@ -121,19 +121,34 @@ impl Collection {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_meta_value(&mut self, key: &str, value: &str, metadata: &mut CsvMetadata) {
|
fn parse_meta_value(&mut self, key: &str, value: &str, metadata: &mut CsvMetadata) {
|
||||||
|
// trim potential delimiters past the first char* if
|
||||||
|
// metadata line was mistakenly exported as a record
|
||||||
|
// *to allow cases like #separator:,
|
||||||
|
// ASSUMPTION: delimiters are not ascii-alphanumeric
|
||||||
|
let trimmed_value = value
|
||||||
|
.char_indices()
|
||||||
|
.nth(1)
|
||||||
|
.and_then(|(i, _)| {
|
||||||
|
value[i..] // SAFETY: char_indices are on char boundaries
|
||||||
|
.find(|c| !char::is_ascii_alphanumeric(&c))
|
||||||
|
.map(|j| value.split_at(i + j).0)
|
||||||
|
})
|
||||||
|
.unwrap_or(value);
|
||||||
|
|
||||||
match key.trim().to_ascii_lowercase().as_str() {
|
match key.trim().to_ascii_lowercase().as_str() {
|
||||||
"separator" => {
|
"separator" => {
|
||||||
if let Some(delimiter) = delimiter_from_value(value) {
|
if let Some(delimiter) = delimiter_from_value(trimmed_value) {
|
||||||
metadata.delimiter = delimiter as i32;
|
metadata.delimiter = delimiter as i32;
|
||||||
metadata.force_delimiter = true;
|
metadata.force_delimiter = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"html" => {
|
"html" => {
|
||||||
if let Ok(is_html) = value.to_lowercase().parse() {
|
if let Ok(is_html) = trimmed_value.to_lowercase().parse() {
|
||||||
metadata.is_html = is_html;
|
metadata.is_html = is_html;
|
||||||
metadata.force_is_html = true;
|
metadata.force_is_html = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// freeform values cannot be trimmed thus without knowing the exact delimiter
|
||||||
"tags" => metadata.global_tags = collect_tags(value),
|
"tags" => metadata.global_tags = collect_tags(value),
|
||||||
"columns" => {
|
"columns" => {
|
||||||
if let Ok(columns) = parse_columns(value, metadata.delimiter()) {
|
if let Ok(columns) = parse_columns(value, metadata.delimiter()) {
|
||||||
|
@ -151,22 +166,22 @@ impl Collection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"notetype column" => {
|
"notetype column" => {
|
||||||
if let Ok(n) = value.trim().parse() {
|
if let Ok(n) = trimmed_value.trim().parse() {
|
||||||
metadata.notetype = Some(CsvNotetype::NotetypeColumn(n));
|
metadata.notetype = Some(CsvNotetype::NotetypeColumn(n));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"deck column" => {
|
"deck column" => {
|
||||||
if let Ok(n) = value.trim().parse() {
|
if let Ok(n) = trimmed_value.trim().parse() {
|
||||||
metadata.deck = Some(CsvDeck::DeckColumn(n));
|
metadata.deck = Some(CsvDeck::DeckColumn(n));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"tags column" => {
|
"tags column" => {
|
||||||
if let Ok(n) = value.trim().parse() {
|
if let Ok(n) = trimmed_value.trim().parse() {
|
||||||
metadata.tags_column = n;
|
metadata.tags_column = n;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"guid column" => {
|
"guid column" => {
|
||||||
if let Ok(n) = value.trim().parse() {
|
if let Ok(n) = trimmed_value.trim().parse() {
|
||||||
metadata.guid_column = n;
|
metadata.guid_column = n;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -891,4 +906,32 @@ pub(in crate::import_export) mod test {
|
||||||
maybe_set_tags_column(&mut metadata, &meta_columns);
|
maybe_set_tags_column(&mut metadata, &meta_columns);
|
||||||
assert_eq!(metadata.tags_column, 4);
|
assert_eq!(metadata.tags_column, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_allow_non_freeform_metadata_lines_to_be_suffixed_by_delimiters() {
|
||||||
|
let mut col = Collection::new();
|
||||||
|
let metadata = metadata!(
|
||||||
|
col,
|
||||||
|
r#"
|
||||||
|
#separator:Pipe,,,,,,,
|
||||||
|
#html:true|||||
|
||||||
|
#tags:foo bar::世界,,,
|
||||||
|
#guid column:8
|
||||||
|
#tags column:123abc
|
||||||
|
"#
|
||||||
|
.trim()
|
||||||
|
);
|
||||||
|
assert_eq!(metadata.delimiter(), Delimiter::Pipe);
|
||||||
|
assert!(metadata.is_html);
|
||||||
|
assert_eq!(metadata.guid_column, 8);
|
||||||
|
// tags is freeform, potential delimiters aren't trimmed
|
||||||
|
assert_eq!(metadata.global_tags, ["foo", "bar::世界,,,"]);
|
||||||
|
// ascii alphanumerics aren't trimmed away
|
||||||
|
assert_eq!(metadata.tags_column, 0);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
metadata!(col, "#separator:\t|,:\n").delimiter(),
|
||||||
|
Delimiter::Tab
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -242,6 +242,7 @@ pub(crate) struct FsrsItemForMemoryState {
|
||||||
/// When revlogs have been truncated, this stores the initial state at first
|
/// When revlogs have been truncated, this stores the initial state at first
|
||||||
/// review
|
/// review
|
||||||
pub starting_state: Option<MemoryState>,
|
pub starting_state: Option<MemoryState>,
|
||||||
|
pub filtered_revlogs: Vec<RevlogEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Like [fsrs_item_for_memory_state], but for updating multiple cards at once.
|
/// Like [fsrs_item_for_memory_state], but for updating multiple cards at once.
|
||||||
|
@ -330,6 +331,7 @@ pub(crate) fn fsrs_item_for_memory_state(
|
||||||
Ok(Some(FsrsItemForMemoryState {
|
Ok(Some(FsrsItemForMemoryState {
|
||||||
item,
|
item,
|
||||||
starting_state: None,
|
starting_state: None,
|
||||||
|
filtered_revlogs: output.filtered_revlogs,
|
||||||
}))
|
}))
|
||||||
} else if let Some(first_user_grade) = output.filtered_revlogs.first() {
|
} else if let Some(first_user_grade) = output.filtered_revlogs.first() {
|
||||||
// the revlog has been truncated, but not fully
|
// the revlog has been truncated, but not fully
|
||||||
|
@ -356,6 +358,7 @@ pub(crate) fn fsrs_item_for_memory_state(
|
||||||
Ok(Some(FsrsItemForMemoryState {
|
Ok(Some(FsrsItemForMemoryState {
|
||||||
item,
|
item,
|
||||||
starting_state: Some(starting_state),
|
starting_state: Some(starting_state),
|
||||||
|
filtered_revlogs: output.filtered_revlogs,
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
// only manual and rescheduled revlogs; treat like empty
|
// only manual and rescheduled revlogs; treat like empty
|
||||||
|
|
|
@ -14,6 +14,7 @@ use anki_proto::stats::DeckEntry;
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use chrono::NaiveTime;
|
use chrono::NaiveTime;
|
||||||
use fsrs::CombinedProgressState;
|
use fsrs::CombinedProgressState;
|
||||||
|
use fsrs::ComputeParametersInput;
|
||||||
use fsrs::FSRSItem;
|
use fsrs::FSRSItem;
|
||||||
use fsrs::FSRSReview;
|
use fsrs::FSRSReview;
|
||||||
use fsrs::MemoryState;
|
use fsrs::MemoryState;
|
||||||
|
@ -107,34 +108,40 @@ impl Collection {
|
||||||
|
|
||||||
let (progress, progress_thread) = create_progress_thread()?;
|
let (progress, progress_thread) = create_progress_thread()?;
|
||||||
let fsrs = FSRS::new(None)?;
|
let fsrs = FSRS::new(None)?;
|
||||||
let mut params = fsrs.compute_parameters(items.clone(), Some(progress.clone()), true)?;
|
let mut params = fsrs.compute_parameters(ComputeParametersInput {
|
||||||
|
train_set: items.clone(),
|
||||||
|
progress: Some(progress.clone()),
|
||||||
|
enable_short_term: true,
|
||||||
|
num_relearning_steps: Some(num_of_relearning_steps),
|
||||||
|
})?;
|
||||||
progress_thread.join().ok();
|
progress_thread.join().ok();
|
||||||
if let Ok(fsrs) = FSRS::new(Some(current_params)) {
|
if let Ok(fsrs) = FSRS::new(Some(current_params)) {
|
||||||
let current_rmse = fsrs.evaluate(items.clone(), |_| true)?.rmse_bins;
|
let current_rmse = fsrs.evaluate(items.clone(), |_| true)?.rmse_bins;
|
||||||
let optimized_fsrs = FSRS::new(Some(¶ms))?;
|
let optimized_fsrs = FSRS::new(Some(¶ms))?;
|
||||||
let optimized_rmse = optimized_fsrs.evaluate(items.clone(), |_| true)?.rmse_bins;
|
let optimized_rmse = optimized_fsrs.evaluate(items.clone(), |_| true)?.rmse_bins;
|
||||||
if current_rmse <= optimized_rmse {
|
if current_rmse <= optimized_rmse {
|
||||||
params = current_params.to_vec();
|
if num_of_relearning_steps <= 1 {
|
||||||
}
|
params = current_params.to_vec();
|
||||||
if num_of_relearning_steps > 1 {
|
} else {
|
||||||
let memory_state = MemoryState {
|
let current_fsrs = FSRS::new(Some(current_params))?;
|
||||||
stability: 1.0,
|
let memory_state = MemoryState {
|
||||||
difficulty: 1.0,
|
stability: 1.0,
|
||||||
};
|
difficulty: 1.0,
|
||||||
let s_fail = optimized_fsrs
|
};
|
||||||
.next_states(Some(memory_state), 0.9, 2)?
|
|
||||||
.again;
|
let s_fail = current_fsrs.next_states(Some(memory_state), 0.9, 2)?.again;
|
||||||
let mut s_short_term = s_fail.memory;
|
let mut s_short_term = s_fail.memory;
|
||||||
for _ in 0..num_of_relearning_steps {
|
|
||||||
s_short_term = optimized_fsrs
|
for _ in 0..num_of_relearning_steps {
|
||||||
.next_states(Some(s_short_term), 0.9, 0)?
|
s_short_term = current_fsrs
|
||||||
.good
|
.next_states(Some(s_short_term), 0.9, 0)?
|
||||||
.memory;
|
.good
|
||||||
}
|
.memory;
|
||||||
if s_short_term.stability > memory_state.stability {
|
}
|
||||||
let (progress, progress_thread) = create_progress_thread()?;
|
|
||||||
params = fsrs.compute_parameters(items.clone(), Some(progress), false)?;
|
if s_short_term.stability < memory_state.stability {
|
||||||
progress_thread.join().ok();
|
params = current_params.to_vec();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ use anki_proto::scheduler::FuzzDeltaResponse;
|
||||||
use anki_proto::scheduler::GetOptimalRetentionParametersResponse;
|
use anki_proto::scheduler::GetOptimalRetentionParametersResponse;
|
||||||
use anki_proto::scheduler::SimulateFsrsReviewRequest;
|
use anki_proto::scheduler::SimulateFsrsReviewRequest;
|
||||||
use anki_proto::scheduler::SimulateFsrsReviewResponse;
|
use anki_proto::scheduler::SimulateFsrsReviewResponse;
|
||||||
|
use fsrs::ComputeParametersInput;
|
||||||
use fsrs::FSRSItem;
|
use fsrs::FSRSItem;
|
||||||
use fsrs::FSRSReview;
|
use fsrs::FSRSReview;
|
||||||
use fsrs::FSRS;
|
use fsrs::FSRS;
|
||||||
|
@ -352,11 +353,12 @@ impl crate::services::BackendSchedulerService for Backend {
|
||||||
) -> Result<scheduler::ComputeFsrsParamsResponse> {
|
) -> Result<scheduler::ComputeFsrsParamsResponse> {
|
||||||
let fsrs = FSRS::new(None)?;
|
let fsrs = FSRS::new(None)?;
|
||||||
let fsrs_items = req.items.len() as u32;
|
let fsrs_items = req.items.len() as u32;
|
||||||
let params = fsrs.compute_parameters(
|
let params = fsrs.compute_parameters(ComputeParametersInput {
|
||||||
req.items.into_iter().map(fsrs_item_proto_to_fsrs).collect(),
|
train_set: req.items.into_iter().map(fsrs_item_proto_to_fsrs).collect(),
|
||||||
None,
|
progress: None,
|
||||||
true,
|
enable_short_term: true,
|
||||||
)?;
|
num_relearning_steps: None,
|
||||||
|
})?;
|
||||||
Ok(ComputeFsrsParamsResponse { params, fsrs_items })
|
Ok(ComputeFsrsParamsResponse { params, fsrs_items })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -370,7 +372,12 @@ impl crate::services::BackendSchedulerService for Backend {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(fsrs_item_proto_to_fsrs)
|
.map(fsrs_item_proto_to_fsrs)
|
||||||
.collect();
|
.collect();
|
||||||
let params = fsrs.benchmark(train_set, true);
|
let params = fsrs.benchmark(ComputeParametersInput {
|
||||||
|
train_set,
|
||||||
|
progress: None,
|
||||||
|
enable_short_term: true,
|
||||||
|
num_relearning_steps: None,
|
||||||
|
});
|
||||||
Ok(FsrsBenchmarkResponse { params })
|
Ok(FsrsBenchmarkResponse { params })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
use fsrs::FSRS;
|
use fsrs::FSRS;
|
||||||
|
|
||||||
use crate::card::CardType;
|
use crate::card::CardType;
|
||||||
|
use crate::card::FsrsMemoryState;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::revlog::RevlogEntry;
|
use crate::revlog::RevlogEntry;
|
||||||
use crate::scheduler::fsrs::memory_state::fsrs_item_for_memory_state;
|
use crate::scheduler::fsrs::memory_state::fsrs_item_for_memory_state;
|
||||||
|
@ -140,26 +141,31 @@ impl Collection {
|
||||||
let ignore_before = ignore_revlogs_before_ms_from_config(&config)?;
|
let ignore_before = ignore_revlogs_before_ms_from_config(&config)?;
|
||||||
|
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
let mut accumulated_revlog = Vec::new();
|
if let Some(item) = fsrs_item_for_memory_state(
|
||||||
|
&fsrs,
|
||||||
for entry in revlog {
|
revlog.clone(),
|
||||||
accumulated_revlog.push(entry.clone());
|
next_day_at,
|
||||||
let item = fsrs_item_for_memory_state(
|
historical_retention,
|
||||||
&fsrs,
|
ignore_before,
|
||||||
accumulated_revlog.clone(),
|
)? {
|
||||||
next_day_at,
|
let memory_states = fsrs.historical_memory_states(item.item, item.starting_state)?;
|
||||||
historical_retention,
|
let mut revlog_index = 0;
|
||||||
ignore_before,
|
for entry in revlog {
|
||||||
)?;
|
let mut stats_entry = stats_revlog_entry(&entry);
|
||||||
let mut card_clone = card.clone();
|
let memory_state: FsrsMemoryState =
|
||||||
card_clone.set_memory_state(&fsrs, item, historical_retention)?;
|
if entry.id == item.filtered_revlogs[revlog_index].id {
|
||||||
|
revlog_index += 1;
|
||||||
let mut stats_entry = stats_revlog_entry(&entry);
|
memory_states[revlog_index - 1].into()
|
||||||
stats_entry.memory_state = card_clone.memory_state.map(Into::into);
|
} else {
|
||||||
result.push(stats_entry);
|
memory_states[revlog_index].into()
|
||||||
|
};
|
||||||
|
stats_entry.memory_state = Some(memory_state.into());
|
||||||
|
result.push(stats_entry);
|
||||||
|
}
|
||||||
|
Ok(result.into_iter().rev().collect())
|
||||||
|
} else {
|
||||||
|
Ok(revlog.iter().map(stats_revlog_entry).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(result.into_iter().rev().collect())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue