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]]
|
||||
name = "fsrs"
|
||||
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 = [
|
||||
"burn",
|
||||
"itertools 0.12.1",
|
||||
|
|
|
@ -37,7 +37,7 @@ rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca"
|
|||
[workspace.dependencies.fsrs]
|
||||
# version = "=2.0.3"
|
||||
git = "https://github.com/open-spaced-repetition/fsrs-rs.git"
|
||||
rev = "22f8e453c120f5bc5996f86558a559c6b7abfc49"
|
||||
rev = "08d90d1363b0c4722422bf0ef71ed8fd7d053f8a"
|
||||
# path = "../open-spaced-repetition/fsrs-rs"
|
||||
|
||||
[workspace.dependencies]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
[toolchain]
|
||||
channel = "nightly-2023-09-02"
|
||||
channel = "nightly-2025-03-20"
|
||||
profile = "minimal"
|
||||
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-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 }
|
||||
# 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")
|
||||
# also covers *BSD
|
||||
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", "")
|
||||
hmr_mode = os.getenv("HMR", "")
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ from anki._backend import RustBackend
|
|||
from anki.buildinfo import version as _version
|
||||
from anki.collection import Collection
|
||||
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.log import setup_logging
|
||||
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")
|
||||
|
||||
if packaged and wayland_configured:
|
||||
if (packaged or is_gnome) and wayland_configured:
|
||||
if wayland_forced or not x11_available:
|
||||
# Work around broken fractional scaling in Wayland
|
||||
# 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(
|
||||
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_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) {
|
||||
// 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() {
|
||||
"separator" => {
|
||||
if let Some(delimiter) = delimiter_from_value(value) {
|
||||
if let Some(delimiter) = delimiter_from_value(trimmed_value) {
|
||||
metadata.delimiter = delimiter as i32;
|
||||
metadata.force_delimiter = true;
|
||||
}
|
||||
}
|
||||
"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.force_is_html = true;
|
||||
}
|
||||
}
|
||||
// freeform values cannot be trimmed thus without knowing the exact delimiter
|
||||
"tags" => metadata.global_tags = collect_tags(value),
|
||||
"columns" => {
|
||||
if let Ok(columns) = parse_columns(value, metadata.delimiter()) {
|
||||
|
@ -151,22 +166,22 @@ impl Collection {
|
|||
}
|
||||
}
|
||||
"notetype column" => {
|
||||
if let Ok(n) = value.trim().parse() {
|
||||
if let Ok(n) = trimmed_value.trim().parse() {
|
||||
metadata.notetype = Some(CsvNotetype::NotetypeColumn(n));
|
||||
}
|
||||
}
|
||||
"deck column" => {
|
||||
if let Ok(n) = value.trim().parse() {
|
||||
if let Ok(n) = trimmed_value.trim().parse() {
|
||||
metadata.deck = Some(CsvDeck::DeckColumn(n));
|
||||
}
|
||||
}
|
||||
"tags column" => {
|
||||
if let Ok(n) = value.trim().parse() {
|
||||
if let Ok(n) = trimmed_value.trim().parse() {
|
||||
metadata.tags_column = n;
|
||||
}
|
||||
}
|
||||
"guid column" => {
|
||||
if let Ok(n) = value.trim().parse() {
|
||||
if let Ok(n) = trimmed_value.trim().parse() {
|
||||
metadata.guid_column = n;
|
||||
}
|
||||
}
|
||||
|
@ -891,4 +906,32 @@ pub(in crate::import_export) mod test {
|
|||
maybe_set_tags_column(&mut metadata, &meta_columns);
|
||||
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
|
||||
/// review
|
||||
pub starting_state: Option<MemoryState>,
|
||||
pub filtered_revlogs: Vec<RevlogEntry>,
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
item,
|
||||
starting_state: None,
|
||||
filtered_revlogs: output.filtered_revlogs,
|
||||
}))
|
||||
} else if let Some(first_user_grade) = output.filtered_revlogs.first() {
|
||||
// the revlog has been truncated, but not fully
|
||||
|
@ -356,6 +358,7 @@ pub(crate) fn fsrs_item_for_memory_state(
|
|||
Ok(Some(FsrsItemForMemoryState {
|
||||
item,
|
||||
starting_state: Some(starting_state),
|
||||
filtered_revlogs: output.filtered_revlogs,
|
||||
}))
|
||||
} else {
|
||||
// only manual and rescheduled revlogs; treat like empty
|
||||
|
|
|
@ -14,6 +14,7 @@ use anki_proto::stats::DeckEntry;
|
|||
use chrono::NaiveDate;
|
||||
use chrono::NaiveTime;
|
||||
use fsrs::CombinedProgressState;
|
||||
use fsrs::ComputeParametersInput;
|
||||
use fsrs::FSRSItem;
|
||||
use fsrs::FSRSReview;
|
||||
use fsrs::MemoryState;
|
||||
|
@ -107,34 +108,40 @@ impl Collection {
|
|||
|
||||
let (progress, progress_thread) = create_progress_thread()?;
|
||||
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();
|
||||
if let Ok(fsrs) = FSRS::new(Some(current_params)) {
|
||||
let current_rmse = fsrs.evaluate(items.clone(), |_| true)?.rmse_bins;
|
||||
let optimized_fsrs = FSRS::new(Some(¶ms))?;
|
||||
let optimized_rmse = optimized_fsrs.evaluate(items.clone(), |_| true)?.rmse_bins;
|
||||
if current_rmse <= optimized_rmse {
|
||||
params = current_params.to_vec();
|
||||
}
|
||||
if num_of_relearning_steps > 1 {
|
||||
let memory_state = MemoryState {
|
||||
stability: 1.0,
|
||||
difficulty: 1.0,
|
||||
};
|
||||
let s_fail = optimized_fsrs
|
||||
.next_states(Some(memory_state), 0.9, 2)?
|
||||
.again;
|
||||
let mut s_short_term = s_fail.memory;
|
||||
for _ in 0..num_of_relearning_steps {
|
||||
s_short_term = optimized_fsrs
|
||||
.next_states(Some(s_short_term), 0.9, 0)?
|
||||
.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)?;
|
||||
progress_thread.join().ok();
|
||||
if num_of_relearning_steps <= 1 {
|
||||
params = current_params.to_vec();
|
||||
} else {
|
||||
let current_fsrs = FSRS::new(Some(current_params))?;
|
||||
let memory_state = MemoryState {
|
||||
stability: 1.0,
|
||||
difficulty: 1.0,
|
||||
};
|
||||
|
||||
let s_fail = current_fsrs.next_states(Some(memory_state), 0.9, 2)?.again;
|
||||
let mut s_short_term = s_fail.memory;
|
||||
|
||||
for _ in 0..num_of_relearning_steps {
|
||||
s_short_term = current_fsrs
|
||||
.next_states(Some(s_short_term), 0.9, 0)?
|
||||
.good
|
||||
.memory;
|
||||
}
|
||||
|
||||
if s_short_term.stability < memory_state.stability {
|
||||
params = current_params.to_vec();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ use anki_proto::scheduler::FuzzDeltaResponse;
|
|||
use anki_proto::scheduler::GetOptimalRetentionParametersResponse;
|
||||
use anki_proto::scheduler::SimulateFsrsReviewRequest;
|
||||
use anki_proto::scheduler::SimulateFsrsReviewResponse;
|
||||
use fsrs::ComputeParametersInput;
|
||||
use fsrs::FSRSItem;
|
||||
use fsrs::FSRSReview;
|
||||
use fsrs::FSRS;
|
||||
|
@ -352,11 +353,12 @@ impl crate::services::BackendSchedulerService for Backend {
|
|||
) -> Result<scheduler::ComputeFsrsParamsResponse> {
|
||||
let fsrs = FSRS::new(None)?;
|
||||
let fsrs_items = req.items.len() as u32;
|
||||
let params = fsrs.compute_parameters(
|
||||
req.items.into_iter().map(fsrs_item_proto_to_fsrs).collect(),
|
||||
None,
|
||||
true,
|
||||
)?;
|
||||
let params = fsrs.compute_parameters(ComputeParametersInput {
|
||||
train_set: req.items.into_iter().map(fsrs_item_proto_to_fsrs).collect(),
|
||||
progress: None,
|
||||
enable_short_term: true,
|
||||
num_relearning_steps: None,
|
||||
})?;
|
||||
Ok(ComputeFsrsParamsResponse { params, fsrs_items })
|
||||
}
|
||||
|
||||
|
@ -370,7 +372,12 @@ impl crate::services::BackendSchedulerService for Backend {
|
|||
.into_iter()
|
||||
.map(fsrs_item_proto_to_fsrs)
|
||||
.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 })
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
use fsrs::FSRS;
|
||||
|
||||
use crate::card::CardType;
|
||||
use crate::card::FsrsMemoryState;
|
||||
use crate::prelude::*;
|
||||
use crate::revlog::RevlogEntry;
|
||||
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 mut result = Vec::new();
|
||||
let mut accumulated_revlog = Vec::new();
|
||||
|
||||
for entry in revlog {
|
||||
accumulated_revlog.push(entry.clone());
|
||||
let item = fsrs_item_for_memory_state(
|
||||
&fsrs,
|
||||
accumulated_revlog.clone(),
|
||||
next_day_at,
|
||||
historical_retention,
|
||||
ignore_before,
|
||||
)?;
|
||||
let mut card_clone = card.clone();
|
||||
card_clone.set_memory_state(&fsrs, item, historical_retention)?;
|
||||
|
||||
let mut stats_entry = stats_revlog_entry(&entry);
|
||||
stats_entry.memory_state = card_clone.memory_state.map(Into::into);
|
||||
result.push(stats_entry);
|
||||
if let Some(item) = fsrs_item_for_memory_state(
|
||||
&fsrs,
|
||||
revlog.clone(),
|
||||
next_day_at,
|
||||
historical_retention,
|
||||
ignore_before,
|
||||
)? {
|
||||
let memory_states = fsrs.historical_memory_states(item.item, item.starting_state)?;
|
||||
let mut revlog_index = 0;
|
||||
for entry in revlog {
|
||||
let mut stats_entry = stats_revlog_entry(&entry);
|
||||
let memory_state: FsrsMemoryState =
|
||||
if entry.id == item.filtered_revlogs[revlog_index].id {
|
||||
revlog_index += 1;
|
||||
memory_states[revlog_index - 1].into()
|
||||
} else {
|
||||
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