From d24d2e33943af2361b5a9880572b30887efcf3ee Mon Sep 17 00:00:00 2001 From: Ranjit Odedra <78635944+ranjitodedra@users.noreply.github.com> Date: Fri, 17 Oct 2025 18:11:50 -0400 Subject: [PATCH 01/25] Fix sync login dialog not using newly-set custom sync server #4395 (#4396) * Fix sync login with custom server URL Call update_network() before showing login dialog to ensure the custom sync server URL is saved before attempting login. Previously, the custom URL was only saved when closing the preferences dialog, causing login to fail on first attempt. Fixes #4395 * Add contributor info --- CONTRIBUTORS | 1 + qt/aqt/preferences.py | 1 + 2 files changed, 2 insertions(+) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 4e01aa0b2..43f7a7e46 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -251,6 +251,7 @@ Matbe766 Amanda Sternberg arold0 nav1s +Ranjit Odedra ******************** diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py index afce6d489..939dd8c2c 100644 --- a/qt/aqt/preferences.py +++ b/qt/aqt/preferences.py @@ -260,6 +260,7 @@ class Preferences(QDialog): self.update_login_status() self.confirm_sync_after_login() + self.update_network() sync_login(self.mw, on_success) def sync_logout(self) -> None: From 9f9bafa66ca8cd51823d74a912c8b6bdb1d3adce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 18:30:23 +0700 Subject: [PATCH 02/25] Bump vite from 6.3.6 to 6.4.1 (#4405) Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.6 to 6.4.1. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/create-vite@6.4.1/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 6.4.1 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 761f20972..bc1640152 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6939,8 +6939,8 @@ __metadata: linkType: hard "vite@npm:6": - version: 6.3.6 - resolution: "vite@npm:6.3.6" + version: 6.4.1 + resolution: "vite@npm:6.4.1" dependencies: esbuild: "npm:^0.25.0" fdir: "npm:^6.4.4" @@ -6989,7 +6989,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/add701f1e72596c002275782e38d0389ab400c1be330c93a3009804d62db68097a936ca1c53c3301df3aaacfe5e328eab547060f31ef9c49a277ae50df6ad4fb + checksum: 10c0/77bb4c5b10f2a185e7859cc9a81c789021bc18009b02900347d1583b453b58e4b19ff07a5e5a5b522b68fc88728460bb45a63b104d969e8c6a6152aea3b849f7 languageName: node linkType: hard From 23263caea25fbe6e804c11b82b9765f952e9c515 Mon Sep 17 00:00:00 2001 From: llama Date: Mon, 27 Oct 2025 20:08:40 +0800 Subject: [PATCH 03/25] feat: show launcher if different version was installed (#4381) --- qt/launcher/build.rs | 3 +++ qt/launcher/src/main.rs | 18 +++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/qt/launcher/build.rs b/qt/launcher/build.rs index 3ba75b0e1..bc30f8dff 100644 --- a/qt/launcher/build.rs +++ b/qt/launcher/build.rs @@ -7,4 +7,7 @@ fn main() { .manifest_required() .unwrap(); } + println!("cargo:rerun-if-changed=../../out/buildhash"); + let buildhash = std::fs::read_to_string("../../out/buildhash").unwrap_or_default(); + println!("cargo:rustc-env=BUILDHASH={buildhash}"); } diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index 26fbe86a7..c350bcc14 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -152,7 +152,9 @@ fn run() -> Result<()> { let sync_time = file_timestamp_secs(&state.sync_complete_marker); state.pyproject_modified_by_user = pyproject_time > sync_time; let pyproject_has_changed = state.pyproject_modified_by_user; - if !launcher_requested && !pyproject_has_changed { + let different_launcher = diff_launcher_was_installed(&state)?; + + if !launcher_requested && !pyproject_has_changed && !different_launcher { // If no launcher request and venv is already up to date, launch Anki normally let args: Vec = std::env::args().skip(1).collect(); let cmd = build_python_command(&state, &args)?; @@ -1107,6 +1109,20 @@ fn show_mirror_submenu(state: &State) -> Result<()> { Ok(()) } +fn diff_launcher_was_installed(state: &State) -> Result { + let launcher_version = option_env!("BUILDHASH").unwrap_or("dev").trim(); + let launcher_version_path = state.uv_install_root.join("launcher-version"); + if let Ok(content) = read_file(&launcher_version_path) { + if let Ok(version_str) = String::from_utf8(content) { + if version_str.trim() == launcher_version { + return Ok(false); + } + } + } + write_file(launcher_version_path, launcher_version)?; + Ok(true) +} + #[cfg(test)] mod tests { use super::*; From a05c90cbce98b38a95d9813887d4c61a7146f146 Mon Sep 17 00:00:00 2001 From: llama Date: Mon, 27 Oct 2025 20:19:28 +0800 Subject: [PATCH 04/25] fix: make uv use system certstore (#4386) --- qt/launcher/src/main.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index c350bcc14..117051fc7 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -1015,6 +1015,9 @@ fn uv_command(state: &State) -> Result { .env("UV_DEFAULT_INDEX", &pypi_mirror); } + // have uv use the system certstore instead of webpki-roots' + command.env("UV_NATIVE_TLS", "1"); + Ok(command) } From 62c950120cfd483f16e0263bb31ef8e2d95af5dc Mon Sep 17 00:00:00 2001 From: user1823 <92206575+user1823@users.noreply.github.com> Date: Mon, 27 Oct 2025 17:56:09 +0530 Subject: [PATCH 05/25] Fix(launcher)/Exclude pre-release if a newer major_minor exists (#4388) The current main produces a list like "25.09.2, 25.08b5, 25.07.5, 25.06b7, 25.02.7" Here, 25.08b5 and 25.06b7 should be filtered out for the same reason this code filters out the older patch releases. --- qt/launcher/src/main.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index 117051fc7..a399a3460 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -605,18 +605,27 @@ fn get_version_kind(state: &State) -> Result> { } fn with_only_latest_patch(versions: &[String]) -> Vec { - // Only show the latest patch release for a given (major, minor) + // Assumes versions are sorted in descending order (newest first) + // Only show the latest patch release for a given (major, minor), + // and exclude pre-releases if a newer major_minor exists let mut seen_major_minor = std::collections::HashSet::new(); versions .iter() .filter(|v| { - let (major, minor, _, _) = parse_version_for_filtering(v); + let (major, minor, _, is_prerelease) = parse_version_for_filtering(v); if major == 2 { return true; } let major_minor = (major, minor); if seen_major_minor.contains(&major_minor) { false + } else if is_prerelease + && seen_major_minor + .iter() + .any(|&(seen_major, seen_minor)| (seen_major, seen_minor) > (major, minor)) + { + // Exclude pre-release if a newer major_minor exists + false } else { seen_major_minor.insert(major_minor); true From d3ca0cd0b342a7178221493d2a0fb18a69ca7efd Mon Sep 17 00:00:00 2001 From: Abdo Date: Mon, 27 Oct 2025 15:27:50 +0300 Subject: [PATCH 06/25] Update PyCharm docs (#4389) * Add .idea.dist * Update PyCharm docs --- .idea.dist/repo.iml | 13 +++++++++++++ docs/editing.md | 14 +++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 .idea.dist/repo.iml diff --git a/.idea.dist/repo.iml b/.idea.dist/repo.iml new file mode 100644 index 000000000..a9ec5ee1a --- /dev/null +++ b/.idea.dist/repo.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/docs/editing.md b/docs/editing.md index ba3fd6fce..42a92c5a8 100644 --- a/docs/editing.md +++ b/docs/editing.md @@ -46,10 +46,14 @@ see and install a number of recommended extensions. ## PyCharm/IntelliJ -If you decide to use PyCharm instead of VS Code, there are somethings to be -aware of. +### Setting up Python environment -### Pylib References +To make PyCharm recognize `anki` and `aqt` imports, you need to add source paths to _Settings > Project Structure_. +You can copy the provided .idea.dist directory to set up the paths automatically: -You'll need to use File>Project Structure to tell IntelliJ that pylib/ is a -sources root, so it knows references to 'anki' in aqt are valid. +``` +mkdir .idea && cd .idea +ln -sf ../.idea.dist/* . +``` + +You also need to add a new Python interpreter under _Settings > Python > Interpreter_ pointing to the Python executable under `out/pyenv` (available after building Anki). From 510a3b3533d239a56112d6ca62245931bd555073 Mon Sep 17 00:00:00 2001 From: Abdo Date: Mon, 27 Oct 2025 15:31:59 +0300 Subject: [PATCH 07/25] Update VS Code config (#4397) --- .vscode.dist/tasks.json | 3 +-- tools/run.py | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.vscode.dist/tasks.json b/.vscode.dist/tasks.json index 72eab9604..b89704d2e 100644 --- a/.vscode.dist/tasks.json +++ b/.vscode.dist/tasks.json @@ -12,8 +12,7 @@ "command": "tools/ninja.bat", "args": [ "pylib", - "qt", - "extract:win_amd64_audio" + "qt" ] } } diff --git a/tools/run.py b/tools/run.py index da0baa2c4..e17e22a97 100644 --- a/tools/run.py +++ b/tools/run.py @@ -5,8 +5,6 @@ import os import sys sys.path.extend(["pylib", "qt", "out/pylib", "out/qt"]) -if sys.platform == "win32": - os.environ["PATH"] += ";out\\extracted\\win_amd64_audio" import aqt From 23d0657a01de1257091f97b0e18ecc88af55f7fe Mon Sep 17 00:00:00 2001 From: jcznk <60730312+jcznk@users.noreply.github.com> Date: Mon, 27 Oct 2025 13:35:07 +0100 Subject: [PATCH 08/25] (UI polish) Filtered decks: replace maximumSize with sizePolicy in "Limit to" QSpinBox (#4398) * Fix (UI) / Increase width of "Limit to" QSpinBox (Filtered Decks) Increased the width from 60 px to 75 px, so that there is always enough space, even in the case of large numbers (e.g., 99999). * Update filtered_deck.ui (replace maximumSize with sizePolicy) --- qt/aqt/forms/filtered_deck.ui | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/qt/aqt/forms/filtered_deck.ui b/qt/aqt/forms/filtered_deck.ui index 0a90c40e5..a64a3968a 100644 --- a/qt/aqt/forms/filtered_deck.ui +++ b/qt/aqt/forms/filtered_deck.ui @@ -85,11 +85,11 @@ - - - 60 - 16777215 - + + + 0 + 0 + 1 @@ -168,11 +168,11 @@ - - - 60 - 16777215 - + + + 0 + 0 + 1 From 9eb6ec4db8b0f91938498a71ccf2935efc299b33 Mon Sep 17 00:00:00 2001 From: Eltaurus Date: Mon, 27 Oct 2025 18:41:40 +0600 Subject: [PATCH 09/25] Fix unescaped HTML in correct type-in answers (#4407) * Fix unescaped HTML in correct type-in answers * unit test for correct answer escaping * fix string conversion --- CONTRIBUTORS | 1 + rslib/src/typeanswer.rs | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 43f7a7e46..01573b419 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -252,6 +252,7 @@ Amanda Sternberg arold0 nav1s Ranjit Odedra +Eltaurus ******************** diff --git a/rslib/src/typeanswer.rs b/rslib/src/typeanswer.rs index 08c638e12..9bf3dc47c 100644 --- a/rslib/src/typeanswer.rs +++ b/rslib/src/typeanswer.rs @@ -58,7 +58,7 @@ trait DiffTrait { if self.get_typed() == self.get_expected() { format_typeans!(format!( "{}", - self.get_expected_original() + htmlescape::encode_minimal(&self.get_expected_original()) )) } else { let output = self.to_tokens(); @@ -391,6 +391,15 @@ mod test { assert_eq!(ctx, "123"); } + #[test] + fn correct_input_is_escaped() { + let ctx = Diff::new("source /bin/activate", "source /bin/activate"); + assert_eq!( + ctx.to_html(), + "source <dir>/bin/activate" + ); + } + #[test] fn correct_input_is_collapsed() { let ctx = Diff::new("123", "123"); From 739e41ce16cceffad9ce25e1bbcfe34b70ebbe91 Mon Sep 17 00:00:00 2001 From: llama Date: Mon, 27 Oct 2025 20:42:45 +0800 Subject: [PATCH 10/25] fix(launcher): apply cache settings to all uv invocations (#4404) --- qt/launcher/src/main.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index a399a3460..dab9435ea 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -327,7 +327,6 @@ fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Re } command - .env("UV_CACHE_DIR", &state.uv_cache_dir) .env("UV_PYTHON_INSTALL_DIR", &state.uv_python_install_dir) .env( "UV_HTTP_TIMEOUT", @@ -346,10 +345,6 @@ fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Re } } - if state.no_cache_marker.exists() { - command.env("UV_NO_CACHE", "1"); - } - match command.ensure_success() { Ok(_) => { // Sync succeeded @@ -1024,6 +1019,12 @@ fn uv_command(state: &State) -> Result { .env("UV_DEFAULT_INDEX", &pypi_mirror); } + if state.no_cache_marker.exists() { + command.env("UV_NO_CACHE", "1"); + } else { + command.env("UV_CACHE_DIR", &state.uv_cache_dir); + } + // have uv use the system certstore instead of webpki-roots' command.env("UV_NATIVE_TLS", "1"); From da5b8cb5b41b22bcf87094f55d5b6bd8aed6c708 Mon Sep 17 00:00:00 2001 From: jariji <96840304+jariji@users.noreply.github.com> Date: Tue, 28 Oct 2025 22:51:55 -0700 Subject: [PATCH 11/25] Show text on occlusion cards regardless of occludeInactive (#4387) * Show text on occlusion cards regardless of `occludeInactive`. Before this change, on an image occlusion card, a text box was visible during editing but not visible during review. This change makes text visible even if other shapes would be hidden. * Move fix to render_image_occlusion() --------- Co-authored-by: jariji Co-authored-by: Abdo --- CONTRIBUTORS | 3 ++- rslib/src/cloze.rs | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 01573b419..d90b7dbcc 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -189,7 +189,7 @@ Christian Donat Asuka Minato Dillon Baldwin Voczi -Ben Nguyen <105088397+bpnguyen107@users.noreply.github.com> +Ben Nguyen <105088397+bpnguyen107@users.noreply.github.com> Themis Demetriades Luke Bartholomew Gregory Abrasaldo @@ -253,6 +253,7 @@ arold0 nav1s Ranjit Odedra Eltaurus +jariji ******************** diff --git a/rslib/src/cloze.rs b/rslib/src/cloze.rs index 9df53286d..70a5d1703 100644 --- a/rslib/src/cloze.rs +++ b/rslib/src/cloze.rs @@ -260,7 +260,6 @@ fn reveal_cloze( image_occlusion_text, question, active, - cloze_ord, &cloze.ordinals, )); return; @@ -332,10 +331,9 @@ fn render_image_occlusion( text: &str, question_side: bool, active: bool, - ordinal: u16, ordinals: &[u16], ) -> String { - if (question_side && active) || ordinal == 0 { + if (question_side && active) || ordinals.contains(&0) { format!( r#"
"#, ordinals_str(ordinals), From dac26ce67147b261d79a56092320cc2f5af0d990 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Wed, 29 Oct 2025 17:15:56 +0000 Subject: [PATCH 12/25] Fix/Exclude BackendFrontendService from write_python_interface (#4410) * Fix/Missing python import in write_header * Revert "Fix/Missing python import in write_header" This reverts commit 7c736d984d474377c4326f0cdf473f0e039857c7. * exclude BackendFrontendService --------- Co-authored-by: Abdo --- rslib/proto/python.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rslib/proto/python.rs b/rslib/proto/python.rs index a5adb4179..5c245de1d 100644 --- a/rslib/proto/python.rs +++ b/rslib/proto/python.rs @@ -22,7 +22,7 @@ pub(crate) fn write_python_interface(services: &[BackendService]) -> Result<()> write_header(&mut out)?; for service in services { - if service.name == "BackendAnkidroidService" { + if ["BackendAnkidroidService", "BackendFrontendService"].contains(&service.name.as_str()) { continue; } for method in service.all_methods() { From 5d4b00a11d72da53db24045ee94243d88ce7ff4a Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 10 Nov 2025 13:03:53 +0000 Subject: [PATCH 13/25] Fix/Per deck retention not used when card is moved to filtered deck (#4413) * Fix/Per deck retention not used for filtered decks * improve error * Perf: prevent double "home_deck" read * Perf: prevent duplicate database read when home deck --- rslib/src/scheduler/answering/mod.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/rslib/src/scheduler/answering/mod.rs b/rslib/src/scheduler/answering/mod.rs index 6ff8c6e2d..a71c6330f 100644 --- a/rslib/src/scheduler/answering/mod.rs +++ b/rslib/src/scheduler/answering/mod.rs @@ -443,9 +443,20 @@ impl Collection { .storage .get_deck(card.deck_id)? .or_not_found(card.deck_id)?; - let config = self.home_deck_config(deck.config_id(), card.original_deck_id)?; + let home_deck = if card.original_deck_id.0 == 0 { + &deck + } else { + &self + .storage + .get_deck(card.original_deck_id)? + .or_not_found(card.original_deck_id)? + }; + let config = self + .storage + .get_deck_config(home_deck.config_id().or_invalid("home deck is filtered")?)? + .unwrap_or_default(); - let desired_retention = deck.effective_desired_retention(&config); + let desired_retention = home_deck.effective_desired_retention(&config); let fsrs_enabled = self.get_config_bool(BoolKey::Fsrs); let fsrs_next_states = if fsrs_enabled { let params = config.fsrs_params(); From c2fddcaf42716682c46880b0d1bf7cb477674e27 Mon Sep 17 00:00:00 2001 From: Nechaiter <164061016+Nechaiter@users.noreply.github.com> Date: Mon, 10 Nov 2025 10:18:57 -0300 Subject: [PATCH 14/25] Fix FindDuplicates dialog size exceeding screen bounds (#4426) --- CONTRIBUTORS | 1 + qt/aqt/forms/finddupes.ui | 3 +++ qt/aqt/utils.py | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index d90b7dbcc..2ec25da2d 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -254,6 +254,7 @@ nav1s Ranjit Odedra Eltaurus jariji +Francisco Esteva ******************** diff --git a/qt/aqt/forms/finddupes.ui b/qt/aqt/forms/finddupes.ui index 9a7c44c06..9bc8be87b 100644 --- a/qt/aqt/forms/finddupes.ui +++ b/qt/aqt/forms/finddupes.ui @@ -47,6 +47,9 @@ QComboBox::NoInsert + + QComboBox::SizeAdjustPolicy::AdjustToMinimumContentsLengthWithIcon + diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py index 43efc513f..ae88dadcb 100644 --- a/qt/aqt/utils.py +++ b/qt/aqt/utils.py @@ -809,7 +809,7 @@ def ensureWidgetInScreenBoundaries(widget: QWidget) -> None: wsize = widget.size() cappedWidth = min(geom.width(), wsize.width()) cappedHeight = min(geom.height(), wsize.height()) - if cappedWidth > wsize.width() or cappedHeight > wsize.height(): + if cappedWidth < wsize.width() or cappedHeight < wsize.height(): widget.resize(QSize(cappedWidth, cappedHeight)) # ensure widget is inside top left From 9facea0a6616995458421abace3f54981a53909c Mon Sep 17 00:00:00 2001 From: Lee Doughty <32392044+leedoughty@users.noreply.github.com> Date: Wed, 12 Nov 2025 19:55:47 +0000 Subject: [PATCH 15/25] feat(tts): fallback to Apple Samantha voice for TTS on macOS (#4420) * Fallback to Apple Samantha voice for en_US TTS on macOS * Use prefix match for Apple Samantha voice fallback --- qt/aqt/tts.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/qt/aqt/tts.py b/qt/aqt/tts.py index d559fb41f..f77e5c975 100644 --- a/qt/aqt/tts.py +++ b/qt/aqt/tts.py @@ -94,8 +94,15 @@ class TTSPlayer: rank -= 1 - # if no preferred voices match, we fall back on language - # with a rank of -100 + # if no requested voices match, use a preferred fallback voice + # (for example, Apple Samantha) with rank of -50 + for avail in avail_voices: + if avail.lang == tag.lang: + if avail.lang == "en_US" and avail.name.startswith("Apple_Samantha"): + return TTSVoiceMatch(voice=avail, rank=-50) + + # if no requested or preferred voices match, we fall back on + # the first available voice for the language, with a rank of -100 for avail in avail_voices: if avail.lang == tag.lang: return TTSVoiceMatch(voice=avail, rank=-100) From dda192f24c201fc6baf276cb243adfe2a5a95825 Mon Sep 17 00:00:00 2001 From: llama Date: Thu, 20 Nov 2025 03:48:14 +0800 Subject: [PATCH 16/25] fix(import): support variable field count with notetype column (#4421) --- rslib/src/import_export/text/csv/import.rs | 22 +++++++++++++++----- rslib/src/import_export/text/csv/metadata.rs | 7 ++----- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/rslib/src/import_export/text/csv/import.rs b/rslib/src/import_export/text/csv/import.rs index e45bbca1b..31dee84e4 100644 --- a/rslib/src/import_export/text/csv/import.rs +++ b/rslib/src/import_export/text/csv/import.rs @@ -1,6 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use std::collections::HashSet; use std::io::BufRead; use std::io::BufReader; use std::io::Read; @@ -106,6 +107,8 @@ struct ColumnContext { notetype_column: Option, /// Source column indices for the fields of a notetype field_source_columns: FieldSourceColumns, + /// Metadata column indices (1-based) + meta_columns: HashSet, /// How fields are converted to strings. Used for escaping HTML if /// appropriate. stringify: fn(&str) -> String, @@ -119,6 +122,7 @@ impl ColumnContext { deck_column: metadata.deck()?.column(), notetype_column: metadata.notetype()?.column(), field_source_columns: metadata.field_source_columns()?, + meta_columns: metadata.meta_columns(), stringify: stringify_fn(metadata.is_html), }) } @@ -166,11 +170,19 @@ impl ColumnContext { } fn gather_note_fields(&self, record: &csv::StringRecord) -> Vec> { - let stringify = self.stringify; - self.field_source_columns - .iter() - .map(|opt| opt.and_then(|idx| record.get(idx - 1)).map(stringify)) - .collect() + let op = |i| record.get(i - 1).map(self.stringify); + if !self.field_source_columns.is_empty() { + self.field_source_columns + .iter() + .map(|opt| opt.and_then(op)) + .collect() + } else { + // notetype column provided, assume all non-metadata columns are notetype fields + (1..=record.len()) + .filter(|i| !self.meta_columns.contains(i)) + .map(op) + .collect() + } } } diff --git a/rslib/src/import_export/text/csv/metadata.rs b/rslib/src/import_export/text/csv/metadata.rs index d505c60d2..cd4150813 100644 --- a/rslib/src/import_export/text/csv/metadata.rs +++ b/rslib/src/import_export/text/csv/metadata.rs @@ -291,11 +291,8 @@ impl CsvMetadataHelpers for CsvMetadata { .map(|&i| (i > 0).then_some(i as usize)) .collect(), CsvNotetype::NotetypeColumn(_) => { - let meta_columns = self.meta_columns(); - (1..self.column_labels.len() + 1) - .filter(|idx| !meta_columns.contains(idx)) - .map(Some) - .collect() + // each row's notetype could have varying number of fields + vec![] } }) } From 5614d20bedcc4dd268136d389ad796b404a69d2c Mon Sep 17 00:00:00 2001 From: llama Date: Thu, 20 Nov 2025 23:43:14 +0800 Subject: [PATCH 17/25] fix(Import): case-fold media filenames when checking uniqueness (#4435) * add wrapper struct with case-folding get impl * use wrapper struct * restrict case-folding to windows * Revert "restrict case-folding to windows" This reverts commit aad01d904f07b28466190d849141883e8a76e1c2. * case-fold filenames for newly added media * add test * fix incorrect comment --- .../package/apkg/import/media.rs | 3 ++- rslib/src/media/files.rs | 12 ++++++++++-- rslib/src/media/mod.rs | 6 +++--- rslib/src/sync/media/database/client/mod.rs | 19 +++++++++++++++++-- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/rslib/src/import_export/package/apkg/import/media.rs b/rslib/src/import_export/package/apkg/import/media.rs index 32bf7c807..20543e074 100644 --- a/rslib/src/import_export/package/apkg/import/media.rs +++ b/rslib/src/import_export/package/apkg/import/media.rs @@ -17,6 +17,7 @@ use crate::import_export::package::media::SafeMediaEntry; use crate::import_export::ImportProgress; use crate::media::files::add_hash_suffix_to_file_stem; use crate::media::files::sha1_of_reader; +use crate::media::Checksums; use crate::prelude::*; use crate::progress::ThrottlingProgressHandler; @@ -75,7 +76,7 @@ impl Context<'_> { fn prepare_media( media_entries: Vec, archive: &mut ZipArchive, - existing_sha1s: &HashMap, + existing_sha1s: &Checksums, progress: &mut ThrottlingProgressHandler, ) -> Result { let mut media_map = MediaUseMap::default(); diff --git a/rslib/src/media/files.rs b/rslib/src/media/files.rs index ce17b40bb..b098eb19e 100644 --- a/rslib/src/media/files.rs +++ b/rslib/src/media/files.rs @@ -173,7 +173,9 @@ pub fn add_data_to_folder_uniquely<'a, P>( where P: AsRef, { - let normalized_name = normalize_filename(desired_name); + // force lowercase to account for case-insensitive filesystems + // but not within normalize_filename, for existing media refs + let normalized_name: Cow<_> = normalize_filename(desired_name).to_lowercase().into(); let mut target_path = folder.as_ref().join(normalized_name.as_ref()); @@ -496,8 +498,14 @@ mod test { "test.mp3" ); - // different contents + // different contents, filenames differ only by case let h2 = sha1_of_data(b"hello1"); + assert_eq!( + add_data_to_folder_uniquely(dpath, "Test.mp3", b"hello1", h2).unwrap(), + "test-88fdd585121a4ccb3d1540527aee53a77c77abb8.mp3" + ); + + // same contents, filenames differ only by case assert_eq!( add_data_to_folder_uniquely(dpath, "test.mp3", b"hello1", h2).unwrap(), "test-88fdd585121a4ccb3d1540527aee53a77c77abb8.mp3" diff --git a/rslib/src/media/mod.rs b/rslib/src/media/mod.rs index 259dd52f8..8a599fbec 100644 --- a/rslib/src/media/mod.rs +++ b/rslib/src/media/mod.rs @@ -6,7 +6,6 @@ pub mod files; mod service; use std::borrow::Cow; -use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; @@ -22,6 +21,7 @@ use crate::progress::ThrottlingProgressHandler; use crate::sync::http_client::HttpSyncClient; use crate::sync::login::SyncAuth; use crate::sync::media::database::client::changetracker::ChangeTracker; +pub use crate::sync::media::database::client::Checksums; use crate::sync::media::database::client::MediaDatabase; use crate::sync::media::database::client::MediaEntry; use crate::sync::media::progress::MediaSyncProgress; @@ -157,7 +157,7 @@ impl MediaManager { pub fn all_checksums_after_checking( &self, progress: impl FnMut(usize) -> bool, - ) -> Result> { + ) -> Result { ChangeTracker::new(&self.media_folder, progress).register_changes(&self.db)?; self.db.all_registered_checksums() } @@ -176,7 +176,7 @@ impl MediaManager { /// All checksums without registering changes first. #[cfg(test)] - pub(crate) fn all_checksums_as_is(&self) -> HashMap { + pub(crate) fn all_checksums_as_is(&self) -> Checksums { self.db.all_registered_checksums().unwrap() } } diff --git a/rslib/src/sync/media/database/client/mod.rs b/rslib/src/sync/media/database/client/mod.rs index f9c6e5ed1..fe3e7c840 100644 --- a/rslib/src/sync/media/database/client/mod.rs +++ b/rslib/src/sync/media/database/client/mod.rs @@ -18,6 +18,20 @@ use crate::prelude::*; pub mod changetracker; +pub struct Checksums(HashMap); + +impl Checksums { + // case-fold filenames when checking files to be imported + // to account for case-insensitive filesystems + pub fn get(&self, key: impl AsRef) -> Option<&Sha1Hash> { + self.0.get(key.as_ref().to_lowercase().as_str()) + } + + pub fn contains_key(&self, key: impl AsRef) -> bool { + self.get(key).is_some() + } +} + #[derive(Debug, PartialEq, Eq)] pub struct MediaEntry { pub fname: String, @@ -175,11 +189,12 @@ delete from media where fname=?", } /// Returns all filenames and checksums, where the checksum is not null. - pub(crate) fn all_registered_checksums(&self) -> error::Result> { + pub(crate) fn all_registered_checksums(&self) -> error::Result { self.db .prepare("SELECT fname, csum FROM media WHERE csum IS NOT NULL")? .query_and_then([], row_to_name_and_checksum)? - .collect() + .collect::>() + .map(Checksums) } pub(crate) fn force_resync(&self) -> error::Result<()> { From 2d4de33cf3160342c4c704c294e643c3e11071b1 Mon Sep 17 00:00:00 2001 From: Lee Doughty <32392044+leedoughty@users.noreply.github.com> Date: Mon, 1 Dec 2025 18:54:46 +0000 Subject: [PATCH 18/25] Ensure trailing spaces are placed outside cloze deletions (#4446) --- ts/lib/tslib/wrap.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ts/lib/tslib/wrap.ts b/ts/lib/tslib/wrap.ts index 39b10e9d1..e22c3e6d8 100644 --- a/ts/lib/tslib/wrap.ts +++ b/ts/lib/tslib/wrap.ts @@ -4,7 +4,12 @@ import { getRange, getSelection } from "./cross-browser"; function wrappedExceptForWhitespace(text: string, front: string, back: string): string { - const match = text.match(/^(\s*)([^]*?)(\s*)$/)!; + const normalizedText = text + .replace(/ /g, " ") + .replace(/ /g, " ") + .replace(/\u00A0/g, " "); + + const match = normalizedText.match(/^(\s*)([^]*?)(\s*)$/)!; return match[1] + front + match[2] + back + match[3]; } From 26751f220762bac08f5b06f015a03be7f55cc108 Mon Sep 17 00:00:00 2001 From: llama Date: Tue, 16 Dec 2025 00:09:51 +0800 Subject: [PATCH 19/25] fix(io): remove incorrect error toast shown when saving twice (#4458) --- .../add-or-update-note.svelte.ts | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/ts/routes/image-occlusion/add-or-update-note.svelte.ts b/ts/routes/image-occlusion/add-or-update-note.svelte.ts index ce31eaaaf..8494563b4 100644 --- a/ts/routes/image-occlusion/add-or-update-note.svelte.ts +++ b/ts/routes/image-occlusion/add-or-update-note.svelte.ts @@ -37,7 +37,9 @@ export const addOrUpdateNote = async function( backExtra, tags, }); - showResult(mode.noteId, result, noteCount); + if (result.note) { + showResult(mode.noteId, result, noteCount); + } } else { const result = await addImageOcclusionNote({ // IOCloningMode is not used on mobile @@ -55,23 +57,12 @@ export const addOrUpdateNote = async function( // show toast message const showResult = (noteId: number | null, result: OpChanges, count: number) => { const props = $state({ - message: "", - type: "error" as "error" | "success", + message: noteId ? tr.browsingCardsUpdated({ count: count }) : tr.importingCardsAdded({ count: count }), + type: "success" as "error" | "success", showToast: true, }); mount(Toast, { target: document.body, props, }); - - if (result.note) { - const msg = noteId ? tr.browsingCardsUpdated({ count: count }) : tr.importingCardsAdded({ count: count }); - props.message = msg; - props.type = "success"; - props.showToast = true; - } else { - const msg = tr.notetypesErrorGeneratingCloze(); - props.message = msg; - props.showToast = true; - } }; From cb7a8dbc1022167f04be7d1669bfcfbee86d5019 Mon Sep 17 00:00:00 2001 From: llama Date: Wed, 17 Dec 2025 00:22:18 +0800 Subject: [PATCH 20/25] fix(ci): bump cargo-deny to 0.18.6 (#4447) --- tools/minilints/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/minilints/src/main.rs b/tools/minilints/src/main.rs index 6d38278b5..ca645efe6 100644 --- a/tools/minilints/src/main.rs +++ b/tools/minilints/src/main.rs @@ -202,7 +202,7 @@ fn sveltekit_temp_file(path: &str) -> bool { } fn check_cargo_deny() -> Result<()> { - Command::run("cargo install cargo-deny@0.18.3")?; + Command::run("cargo install cargo-deny@0.18.6")?; Command::run("cargo deny check")?; Ok(()) } From 4e8c992be1b7d628ba4168a54355fedb0b0e6804 Mon Sep 17 00:00:00 2001 From: Lee Doughty <32392044+leedoughty@users.noreply.github.com> Date: Tue, 16 Dec 2025 16:23:40 +0000 Subject: [PATCH 21/25] fix: prevent
from appearing as text in error message (#4451) * Update error text format to use RichText rather than PlainText * Set CardTypeError messages as rich text to allow HTML formatting * Use CardTypeError from anki.errors module --- qt/aqt/errors.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/qt/aqt/errors.py b/qt/aqt/errors.py index a6d9251e2..89e15246e 100644 --- a/qt/aqt/errors.py +++ b/qt/aqt/errors.py @@ -14,7 +14,7 @@ from markdown import markdown import aqt from anki.collection import HelpPage -from anki.errors import BackendError, Interrupted +from anki.errors import BackendError, CardTypeError, Interrupted from anki.utils import is_win from aqt.addons import AddonManager, AddonMeta from aqt.qt import * @@ -36,6 +36,14 @@ def show_exception(*, parent: QWidget, exception: Exception) -> None: global _mbox error_lines = [] help_page = HelpPage.TROUBLESHOOTING + + # default to PlainText + text_format = Qt.TextFormat.PlainText + + # set CardTypeError messages as rich text to allow HTML formatting + if isinstance(exception, CardTypeError): + text_format = Qt.TextFormat.RichText + if isinstance(exception, BackendError): if exception.context: error_lines.append(exception.context) @@ -51,7 +59,7 @@ def show_exception(*, parent: QWidget, exception: Exception) -> None: ) error_text = "\n".join(error_lines) print(error_lines) - _mbox = _init_message_box(str(exception), error_text, help_page) + _mbox = _init_message_box(str(exception), error_text, help_page, text_format) _mbox.show() @@ -171,7 +179,10 @@ if not os.environ.get("DEBUG"): def _init_message_box( - user_text: str, debug_text: str, help_page=HelpPage.TROUBLESHOOTING + user_text: str, + debug_text: str, + help_page=HelpPage.TROUBLESHOOTING, + text_format=Qt.TextFormat.PlainText, ): global _mbox @@ -179,7 +190,7 @@ def _init_message_box( _mbox.setWindowTitle("Anki") _mbox.setText(user_text) _mbox.setIcon(QMessageBox.Icon.Warning) - _mbox.setTextFormat(Qt.TextFormat.PlainText) + _mbox.setTextFormat(text_format) def show_help(): openHelp(help_page) From 4c7b343231bb2730ae72d6be54cb6be0a6aec3d7 Mon Sep 17 00:00:00 2001 From: GithubAnon0000 <160563432+GithubAnon0000@users.noreply.github.com> Date: Tue, 16 Dec 2025 16:26:43 +0000 Subject: [PATCH 22/25] CHANGE: use minus sign instead of hyphen (#4437) --- qt/aqt/deckbrowser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index 5dc688155..ca754e783 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -234,7 +234,7 @@ class DeckBrowser: if node.collapsed: prefix = "+" else: - prefix = "-" + prefix = "−" def indent() -> str: return " " * 6 * (node.level - 1) From a245f8ce61740646230c123b5005cfc636bfa344 Mon Sep 17 00:00:00 2001 From: llama Date: Wed, 17 Dec 2025 00:38:18 +0800 Subject: [PATCH 23/25] fix(build): treat proto/i18n's implicit outputs as inputs (#4439) * fix(build): make proto/i18n's implicit outputs explicit * use option_env! instead of rerun-if-env-changed > As of 1.46, using env! and option_env! in source code will automatically detect changes and trigger rebuilds. https://doc.rust-lang.org/cargo/reference/build-scripts.html#rerun-if-env-changed * ditto for STRINGS_PY and STRINGS_TS * fix comment * remove space --- rslib/i18n/build.rs | 4 +--- rslib/i18n/python.rs | 3 +-- rslib/i18n/typescript.rs | 3 +-- rslib/io/src/lib.rs | 9 +++++++++ 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/rslib/i18n/build.rs b/rslib/i18n/build.rs index f604c9167..75bc38787 100644 --- a/rslib/i18n/build.rs +++ b/rslib/i18n/build.rs @@ -8,7 +8,6 @@ mod python; mod typescript; mod write_strings; -use std::env; use std::path::PathBuf; use anki_io::create_dir_all; @@ -32,8 +31,7 @@ fn main() -> Result<()> { python::write_py_interface(&modules)?; // write strings.json file to requested path - println!("cargo:rerun-if-env-changed=STRINGS_JSON"); - if let Ok(path) = env::var("STRINGS_JSON") { + if let Some(path) = option_env!("STRINGS_JSON") { if !path.is_empty() { let path = PathBuf::from(path); let meta_json = serde_json::to_string_pretty(&modules).unwrap(); diff --git a/rslib/i18n/python.rs b/rslib/i18n/python.rs index ca780c041..a564de48d 100644 --- a/rslib/i18n/python.rs +++ b/rslib/i18n/python.rs @@ -1,7 +1,6 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use std::env; use std::fmt::Write; use std::path::PathBuf; @@ -21,7 +20,7 @@ pub fn write_py_interface(modules: &[Module]) -> Result<()> { render_methods(modules, &mut out); render_legacy_enum(modules, &mut out); - if let Ok(path) = env::var("STRINGS_PY") { + if let Some(path) = option_env!("STRINGS_PY") { let path = PathBuf::from(path); create_dir_all(path.parent().unwrap())?; write_file_if_changed(path, out)?; diff --git a/rslib/i18n/typescript.rs b/rslib/i18n/typescript.rs index ce30048e2..0f483cb58 100644 --- a/rslib/i18n/typescript.rs +++ b/rslib/i18n/typescript.rs @@ -1,7 +1,6 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use std::env; use std::fmt::Write; use std::path::PathBuf; @@ -22,7 +21,7 @@ pub fn write_ts_interface(modules: &[Module]) -> Result<()> { render_module_map(modules, &mut ts_out); render_methods(modules, &mut ts_out); - if let Ok(path) = env::var("STRINGS_TS") { + if let Some(path) = option_env!("STRINGS_TS") { let path = PathBuf::from(path); create_dir_all(path.parent().unwrap())?; write_file_if_changed(path, ts_out)?; diff --git a/rslib/io/src/lib.rs b/rslib/io/src/lib.rs index cb44467e6..0fd85e490 100644 --- a/rslib/io/src/lib.rs +++ b/rslib/io/src/lib.rs @@ -335,6 +335,15 @@ pub fn write_file_if_changed(path: impl AsRef, contents: impl AsRef<[u8]>) .map(|existing| existing != contents) .unwrap_or(true) }; + + match std::env::var("CARGO_PKG_NAME") { + Ok(pkg) if pkg == "anki_proto" || pkg == "anki_i18n" => { + // at comptime for the proto/i18n crates, register implicit output as input + println!("cargo:rerun-if-changed={}", path.to_str().unwrap()); + } + _ => {} + } + if changed { write_file(path, contents)?; Ok(true) From 62252f7216d0001b73f374617b7acb6c37cbeb74 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Tue, 16 Dec 2025 16:51:13 +0000 Subject: [PATCH 24/25] Fix/Retrievability SQL (#4424) * Fix/SQL Retrievability Underflow * Added: Reminder * ./check * Apply code diff changes Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com> * Slight cleanup * fix bug again * extra label comment * Update rslib/src/storage/sqlite.rs Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com> * Fix: Ignore new cards * use const --------- Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com> --- rslib/src/search/sqlwriter.rs | 3 +- rslib/src/storage/card/filtered.rs | 2 +- rslib/src/storage/card/mod.rs | 2 +- rslib/src/storage/sqlite.rs | 65 ++++++++++++++++-------------- 4 files changed, 38 insertions(+), 34 deletions(-) diff --git a/rslib/src/search/sqlwriter.rs b/rslib/src/search/sqlwriter.rs index f6237d6fd..2479b87ae 100644 --- a/rslib/src/search/sqlwriter.rs +++ b/rslib/src/search/sqlwriter.rs @@ -434,9 +434,10 @@ impl SqlWriter<'_> { let timing = self.col.timing_today()?; (timing.days_elapsed, timing.next_day_at, timing.now) }; + const NEW_TYPE: i8 = CardType::New as i8; write!( self.sql, - "extract_fsrs_retrievability(c.data, case when c.odue !=0 then c.odue else c.due end, c.ivl, {elap}, {next_day_at}, {now}) {op} {r}" + "case when c.type = {NEW_TYPE} then false else (extract_fsrs_retrievability(c.data, case when c.odue !=0 then c.odue else c.due end, c.ivl, {elap}, {next_day_at}, {now}) {op} {r}) end" ) .unwrap() } diff --git a/rslib/src/storage/card/filtered.rs b/rslib/src/storage/card/filtered.rs index ef436f6e8..03f845f4e 100644 --- a/rslib/src/storage/card/filtered.rs +++ b/rslib/src/storage/card/filtered.rs @@ -54,7 +54,7 @@ fn build_retrievability_query( ) -> String { if fsrs { format!( - "extract_fsrs_relative_retrievability(c.data, case when c.odue !=0 then c.odue else c.due end, {today}, ivl, {next_day_at}, {now}) {order}" + "extract_fsrs_relative_retrievability(c.data, case when c.odue !=0 then c.odue else c.due end, ivl, {today}, {next_day_at}, {now}) {order}" ) } else { format!( diff --git a/rslib/src/storage/card/mod.rs b/rslib/src/storage/card/mod.rs index 3a5066ff4..9e06edf07 100644 --- a/rslib/src/storage/card/mod.rs +++ b/rslib/src/storage/card/mod.rs @@ -837,7 +837,7 @@ impl fmt::Display for ReviewOrderSubclause { let next_day_at = timing.next_day_at.0; let now = timing.now.0; temp_string = - format!("extract_fsrs_relative_retrievability(data, case when odue !=0 then odue else due end, {today}, ivl, {next_day_at}, {now}) {order}"); + format!("extract_fsrs_relative_retrievability(data, case when odue !=0 then odue else due end, ivl, {today}, {next_day_at}, {now}) {order}"); &temp_string } ReviewOrderSubclause::Added => "nid asc, ord asc", diff --git a/rslib/src/storage/sqlite.rs b/rslib/src/storage/sqlite.rs index 3ce1baff0..95853afc9 100644 --- a/rslib/src/storage/sqlite.rs +++ b/rslib/src/storage/sqlite.rs @@ -332,23 +332,30 @@ fn add_extract_fsrs_retrievability(db: &Connection) -> rusqlite::Result<()> { return Ok(None); }; let seconds_elapsed = if let Some(last_review_time) = card_data.last_review_time { - now.saturating_sub(last_review_time.0) as u32 + // This and any following + // (x as u32).saturating_sub(y as u32) + // must not be changed to + // x.saturating_sub(y) as u32 + // as x and y are i64's and saturating_sub will therfore allow negative numbers + // before converting to u32 in the latter example. + (now as u32).saturating_sub(last_review_time.0 as u32) } else if due > 365_000 { // (re)learning card in seconds let Ok(ivl) = ctx.get_raw(2).as_i64() else { return Ok(None); }; - let last_review_time = due.saturating_sub(ivl); - now.saturating_sub(last_review_time) as u32 + let last_review_time = (due as u32).saturating_sub(ivl as u32); + (now as u32).saturating_sub(last_review_time) } else { let Ok(ivl) = ctx.get_raw(2).as_i64() else { return Ok(None); }; - let Ok(days_elapsed) = ctx.get_raw(3).as_i64() else { + // timing.days_elapsed + let Ok(today) = ctx.get_raw(3).as_i64() else { return Ok(None); }; - let review_day = due.saturating_sub(ivl); - days_elapsed.saturating_sub(review_day) as u32 * 86_400 + let review_day = (due as u32).saturating_sub(ivl as u32); + (today as u32).saturating_sub(review_day) * 86_400 }; let decay = card_data.decay.unwrap_or(FSRS5_DEFAULT_DECAY); let retrievability = card_data.memory_state().map(|state| { @@ -364,7 +371,7 @@ fn add_extract_fsrs_retrievability(db: &Connection) -> rusqlite::Result<()> { } /// eg. extract_fsrs_relative_retrievability(card.data, card.due, -/// timing.days_elapsed, card.ivl, timing.next_day_at, timing.now) -> float | +/// card.ivl, timing.days_elapsed, timing.next_day_at, timing.now) -> float | /// null. The higher the number, the higher the card's retrievability relative /// to the configured desired retention. fn add_extract_fsrs_relative_retrievability(db: &Connection) -> rusqlite::Result<()> { @@ -378,25 +385,32 @@ fn add_extract_fsrs_relative_retrievability(db: &Connection) -> rusqlite::Result let Ok(due) = ctx.get_raw(1).as_i64() else { return Ok(None); }; - let Ok(interval) = ctx.get_raw(3).as_i64() else { + let Ok(interval) = ctx.get_raw(2).as_i64() else { return Ok(None); }; + /* + // Unused let Ok(next_day_at) = ctx.get_raw(4).as_i64() else { return Ok(None); }; + */ let Ok(now) = ctx.get_raw(5).as_i64() else { return Ok(None); }; - let days_elapsed = if due > 365_000 { - // (re)learning - (next_day_at as u32).saturating_sub(due as u32) / 86_400 + let secs_elapsed = if due > 365_000 { + // (re)learning card with due in seconds + + // Don't change this to now.subtracting_sub(due) as u32 + // for the same reasons listed in the comment + // in add_extract_fsrs_retrievability + (now as u32).saturating_sub(due as u32) } else { - let Ok(days_elapsed) = ctx.get_raw(2).as_i64() else { + // timing.days_elapsed + let Ok(today) = ctx.get_raw(3).as_i64() else { return Ok(None); }; let review_day = due.saturating_sub(interval); - - (days_elapsed as u32).saturating_sub(review_day as u32) + (today as u32).saturating_sub(review_day as u32) * 86_400 }; if let Ok(card_data) = ctx.get_raw(0).as_str() { if !card_data.is_empty() { @@ -410,23 +424,12 @@ fn add_extract_fsrs_relative_retrievability(db: &Connection) -> rusqlite::Result let seconds_elapsed = if let Some(last_review_time) = card_data.last_review_time { - now.saturating_sub(last_review_time.0) as u32 - } else if due > 365_000 { - // (re)learning card in seconds - let Ok(ivl) = ctx.get_raw(2).as_i64() else { - return Ok(None); - }; - let last_review_time = due.saturating_sub(ivl); - now.saturating_sub(last_review_time) as u32 + // Don't change this to now.subtracting_sub(due) as u32 + // for the same reasons listed in the comment + // in add_extract_fsrs_retrievability + (now as u32).saturating_sub(last_review_time.0 as u32) } else { - let Ok(ivl) = ctx.get_raw(2).as_i64() else { - return Ok(None); - }; - let Ok(days_elapsed) = ctx.get_raw(3).as_i64() else { - return Ok(None); - }; - let review_day = due.saturating_sub(ivl); - days_elapsed.saturating_sub(review_day) as u32 * 86_400 + secs_elapsed }; let current_retrievability = FSRS::new(None) @@ -441,7 +444,7 @@ fn add_extract_fsrs_relative_retrievability(db: &Connection) -> rusqlite::Result } } } - + let days_elapsed = secs_elapsed / 86_400; // FSRS data missing; fall back to SM2 ordering Ok(Some( -((days_elapsed as f32) + 0.001) / (interval as f32).max(1.0), From 8f2144534bff6efedb22b7f052fba13ffe28cbc2 Mon Sep 17 00:00:00 2001 From: SelfishPig <62257049+SelfishPig@users.noreply.github.com> Date: Sat, 20 Dec 2025 11:27:13 -0600 Subject: [PATCH 25/25] Start decoding images immediately (#4471) * Start decoding images immediately * Start decoding images immediately (revised) * New contributor --- CONTRIBUTORS | 1 + ts/reviewer/images.ts | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 2ec25da2d..a874a313d 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -255,6 +255,7 @@ Ranjit Odedra Eltaurus jariji Francisco Esteva +SelfishPig ******************** diff --git a/ts/reviewer/images.ts b/ts/reviewer/images.ts index 28c54bebb..35a2e269f 100644 --- a/ts/reviewer/images.ts +++ b/ts/reviewer/images.ts @@ -10,9 +10,6 @@ export function allImagesLoaded(): Promise { } function imageLoaded(img: HTMLImageElement): Promise { - if (!img.getAttribute("decoding")) { - img.decoding = "async"; - } return img.complete ? Promise.resolve() : new Promise((resolve) => { @@ -31,6 +28,8 @@ function extractImageSrcs(fragment: DocumentFragment): string[] { function createImage(src: string): HTMLImageElement { const img = new Image(); img.src = src; + img.decoding = "async"; + img.decode(); return img; }