From 1f7f7bc8a3952ad0673b5e6eca473f765c883d52 Mon Sep 17 00:00:00 2001 From: Jarrett Ye Date: Wed, 9 Jul 2025 17:22:59 +0800 Subject: [PATCH 01/92] Fix/FSRS simulator fallback to memory_state_from_sm2 when converting cards (#4189) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix/FSRS simulator fallback to memory_state_from_sm2 for after setting “Ignore cards reviewed before” * add comment to fsrs_item_for_memory_state * Add historical retention field to FSRS review request and update related logic - Added `historical_retention` field to `SimulateFsrsReviewRequest` in `scheduler.proto`. - Updated `simulator.rs` to use `req.historical_retention` instead of the removed `desired_retention`. - Modified `FsrsOptions.svelte` to include `historicalRetention` in the options passed to the component. * Update rslib/src/scheduler/fsrs/memory_state.rs Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com> * Update rslib/src/scheduler/fsrs/simulator.rs Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com> * pass ci * Update rslib/src/scheduler/fsrs/simulator.rs Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com> * format * Update rslib/src/scheduler/fsrs/simulator.rs Co-authored-by: Luc Mcgrady * format * Fix condition in is_included_card function to check CardType instead of CardQueue --------- Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com> Co-authored-by: Luc Mcgrady --- proto/anki/scheduler.proto | 1 + rslib/src/scheduler/fsrs/memory_state.rs | 1 + rslib/src/scheduler/fsrs/simulator.rs | 86 +++++++++++++---------- ts/routes/deck-options/FsrsOptions.svelte | 1 + 4 files changed, 53 insertions(+), 36 deletions(-) diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index 01f092a39..1294b4543 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -404,6 +404,7 @@ message SimulateFsrsReviewRequest { repeated float easy_days_percentages = 10; deck_config.DeckConfig.Config.ReviewCardOrder review_order = 11; optional uint32 suspend_after_lapse_count = 12; + float historical_retention = 13; } message SimulateFsrsReviewResponse { diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index 425d8da69..b2640fa3e 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -377,6 +377,7 @@ pub(crate) fn fsrs_item_for_memory_state( Ok(None) } } else { + // no revlogs (new card or caused by ignore_revlogs_before or deleted revlogs) Ok(None) } } diff --git a/rslib/src/scheduler/fsrs/simulator.rs b/rslib/src/scheduler/fsrs/simulator.rs index 34cc925d6..e032ecaf3 100644 --- a/rslib/src/scheduler/fsrs/simulator.rs +++ b/rslib/src/scheduler/fsrs/simulator.rs @@ -10,11 +10,14 @@ use fsrs::simulate; use fsrs::PostSchedulingFn; use fsrs::ReviewPriorityFn; use fsrs::SimulatorConfig; +use fsrs::FSRS; use itertools::Itertools; use rand::rngs::StdRng; use rand::Rng; use crate::card::CardQueue; +use crate::card::CardType; +use crate::card::FsrsMemoryState; use crate::prelude::*; use crate::scheduler::states::fuzz::constrained_fuzz_bounds; use crate::scheduler::states::load_balancer::calculate_easy_days_modifiers; @@ -129,7 +132,7 @@ impl Collection { fn is_included_card(c: &Card) -> bool { c.queue != CardQueue::Suspended && c.queue != CardQueue::PreviewRepeat - && c.queue != CardQueue::New + && c.ctype != CardType::New } // calculate any missing memory state for c in &mut cards { @@ -143,13 +146,29 @@ impl Collection { let days_elapsed = self.timing_today().unwrap().days_elapsed as i32; let new_cards = cards .iter() - .filter(|c| c.memory_state.is_none() || c.queue == CardQueue::New) + .filter(|c| c.ctype == CardType::New && c.queue != CardQueue::Suspended) .count() + req.deck_size as usize; + let fsrs = FSRS::new(Some(&req.params))?; let mut converted_cards = cards .into_iter() .filter(is_included_card) - .filter_map(|c| Card::convert(c, days_elapsed)) + .filter_map(|c| { + let memory_state = match c.memory_state { + Some(state) => state, + // cards that lack memory states after compute_memory_state have no FSRS items, + // implying a truncated or ignored revlog + None => fsrs + .memory_state_from_sm2( + c.ease_factor(), + c.interval as f32, + req.historical_retention, + ) + .ok()? + .into(), + }; + Card::convert(c, days_elapsed, memory_state) + }) .collect_vec(); let introduced_today_count = self .search_cards(&format!("{} introduced:1", &req.search), SortMode::NoOrder)? @@ -251,39 +270,34 @@ impl Collection { } impl Card { - fn convert(card: Card, days_elapsed: i32) -> Option { - match card.memory_state { - Some(state) => match card.queue { - CardQueue::DayLearn | CardQueue::Review => { - let due = card.original_or_current_due(); - let relative_due = due - days_elapsed; - let last_date = (relative_due - card.interval as i32).min(0) as f32; - Some(fsrs::Card { - id: card.id.0, - difficulty: state.difficulty, - stability: state.stability, - last_date, - due: relative_due as f32, - interval: card.interval as f32, - lapses: card.lapses, - }) - } - CardQueue::New => None, - CardQueue::Learn | CardQueue::SchedBuried | CardQueue::UserBuried => { - Some(fsrs::Card { - id: card.id.0, - difficulty: state.difficulty, - stability: state.stability, - last_date: 0.0, - due: 0.0, - interval: card.interval as f32, - lapses: card.lapses, - }) - } - CardQueue::PreviewRepeat => None, - CardQueue::Suspended => None, - }, - None => None, + fn convert(card: Card, days_elapsed: i32, memory_state: FsrsMemoryState) -> Option { + match card.queue { + CardQueue::DayLearn | CardQueue::Review => { + let due = card.original_or_current_due(); + let relative_due = due - days_elapsed; + let last_date = (relative_due - card.interval as i32).min(0) as f32; + Some(fsrs::Card { + id: card.id.0, + difficulty: memory_state.difficulty, + stability: memory_state.stability, + last_date, + due: relative_due as f32, + interval: card.interval as f32, + lapses: card.lapses, + }) + } + CardQueue::New => None, + CardQueue::Learn | CardQueue::SchedBuried | CardQueue::UserBuried => Some(fsrs::Card { + id: card.id.0, + difficulty: memory_state.difficulty, + stability: memory_state.stability, + last_date: 0.0, + due: 0.0, + interval: card.interval as f32, + lapses: card.lapses, + }), + CardQueue::PreviewRepeat => None, + CardQueue::Suspended => None, } } } diff --git a/ts/routes/deck-options/FsrsOptions.svelte b/ts/routes/deck-options/FsrsOptions.svelte index 706407889..cfdea341c 100644 --- a/ts/routes/deck-options/FsrsOptions.svelte +++ b/ts/routes/deck-options/FsrsOptions.svelte @@ -95,6 +95,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html newCardsIgnoreReviewLimit: $newCardsIgnoreReviewLimit, easyDaysPercentages: $config.easyDaysPercentages, reviewOrder: $config.reviewOrder, + historicalRetention: $config.historicalRetention, }); const DESIRED_RETENTION_LOW_THRESHOLD = 0.8; From dfbb7302e8620ac8b9488f5111fcd65268c449b8 Mon Sep 17 00:00:00 2001 From: Kevin Nakamura Date: Wed, 9 Jul 2025 10:57:37 +0000 Subject: [PATCH 02/92] set UV_PYTHON_DOWNLOADS=auto when doing `uv sync` (#4191) * set UV_PYTHON_DOWNLOADS=auto when doing `uv sync` * Clear env vars prior to invoking uv, and add --no-config --------- Co-authored-by: Damien Elmes --- build/runner/src/pyenv.rs | 3 ++- qt/launcher/src/main.rs | 21 +++------------------ 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/build/runner/src/pyenv.rs b/build/runner/src/pyenv.rs index d64c8fb3f..f514096fb 100644 --- a/build/runner/src/pyenv.rs +++ b/build/runner/src/pyenv.rs @@ -34,8 +34,9 @@ pub fn setup_pyenv(args: PyenvArgs) { run_command( Command::new(args.uv_bin) + .env_clear() .env("UV_PROJECT_ENVIRONMENT", args.pyenv_folder.clone()) - .args(["sync", "--locked"]) + .args(["sync", "--locked", "--no-config"]) .args(args.extra_args), ); diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index 5679f8f71..34a81a622 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -255,29 +255,14 @@ fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Re None }; - // `uv sync` sometimes does not pull in Python automatically - // This might be system/platform specific and/or a uv bug. + // Prepare to sync the venv let mut command = Command::new(&state.uv_path); command .current_dir(&state.uv_install_root) + .env_clear() .env("UV_CACHE_DIR", &state.uv_cache_dir) .env("UV_PYTHON_INSTALL_DIR", &state.uv_python_install_dir) - .args(["python", "install", "--managed-python"]); - - // Add python version if .python-version file exists - if let Some(version) = &python_version_trimmed { - command.args([version]); - } - - command.ensure_success().context("Python install failed")?; - - // Sync the venv - let mut command = Command::new(&state.uv_path); - command - .current_dir(&state.uv_install_root) - .env("UV_CACHE_DIR", &state.uv_cache_dir) - .env("UV_PYTHON_INSTALL_DIR", &state.uv_python_install_dir) - .args(["sync", "--upgrade", "--managed-python"]); + .args(["sync", "--upgrade", "--managed-python", "--no-config"]); // Add python version if .python-version file exists if let Some(version) = &python_version_trimmed { From 51cf09daf326e28ea98d62652ad35ef056fb55e1 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 9 Jul 2025 21:38:45 +0700 Subject: [PATCH 03/92] Strip only UV_* env vars If we don't preserve env vars like TEMP, it results in run failures on Windows: https://forums.ankiweb.net/t/anki-25-08-beta/63645/28 --- build/runner/src/pyenv.rs | 12 ++++++++++-- qt/launcher/src/main.rs | 11 +++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/build/runner/src/pyenv.rs b/build/runner/src/pyenv.rs index f514096fb..9d65626ca 100644 --- a/build/runner/src/pyenv.rs +++ b/build/runner/src/pyenv.rs @@ -32,9 +32,17 @@ pub fn setup_pyenv(args: PyenvArgs) { } } + let mut command = Command::new(args.uv_bin); + + // remove UV_* environment variables to avoid interference + for (key, _) in std::env::vars() { + if key.starts_with("UV_") { + command.env_remove(key); + } + } + run_command( - Command::new(args.uv_bin) - .env_clear() + command .env("UV_PROJECT_ENVIRONMENT", args.pyenv_folder.clone()) .args(["sync", "--locked", "--no-config"]) .args(args.extra_args), diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index 34a81a622..15548d3bc 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -257,9 +257,16 @@ fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Re // Prepare to sync the venv let mut command = Command::new(&state.uv_path); + command.current_dir(&state.uv_install_root); + + // remove UV_* environment variables to avoid interference + for (key, _) in std::env::vars() { + if key.starts_with("UV_") { + command.env_remove(key); + } + } + command - .current_dir(&state.uv_install_root) - .env_clear() .env("UV_CACHE_DIR", &state.uv_cache_dir) .env("UV_PYTHON_INSTALL_DIR", &state.uv_python_install_dir) .args(["sync", "--upgrade", "--managed-python", "--no-config"]); From f4e587256c4a42482f92f3f9c0c486176bff237c Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 12 Jul 2025 13:38:16 +0700 Subject: [PATCH 04/92] Retention rate -> retention https://forums.ankiweb.net/t/rename-true-retention-retention-rate/63446/5 Closes #4190 --- ftl/core/statistics.ftl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ftl/core/statistics.ftl b/ftl/core/statistics.ftl index 8da1aace8..cb1accc88 100644 --- a/ftl/core/statistics.ftl +++ b/ftl/core/statistics.ftl @@ -99,9 +99,9 @@ statistics-counts-relearning-cards = Relearning statistics-counts-title = Card Counts statistics-counts-separate-suspended-buried-cards = Separate suspended/buried cards -## Retention rate represents your actual retention rate from past reviews, in +## Retention represents your actual retention from past reviews, in ## comparison to the "desired retention" setting of FSRS, which forecasts -## future retention. Retention rate is the percentage of all reviewed cards +## future retention. Retention is the percentage of all reviewed cards ## that were marked as "Hard," "Good," or "Easy" within a specific time period. ## ## Most of these strings are used as column / row headings in a table. @@ -112,9 +112,9 @@ statistics-counts-separate-suspended-buried-cards = Separate suspended/buried ca ## N.B. Stats cards may be very small on mobile devices and when the Stats ## window is certain sizes. -statistics-true-retention-title = Retention rate +statistics-true-retention-title = Retention statistics-true-retention-subtitle = Pass rate of cards with an interval ≥ 1 day. -statistics-true-retention-tooltip = If you are using FSRS, your retention rate is expected to be close to your desired retention. Please keep in mind that data for a single day is noisy, so it's better to look at monthly data. +statistics-true-retention-tooltip = If you are using FSRS, your retention is expected to be close to your desired retention. Please keep in mind that data for a single day is noisy, so it's better to look at monthly data. statistics-true-retention-range = Range statistics-true-retention-pass = Pass statistics-true-retention-fail = Fail From c56fd3ee284344c193fa960dabba0d055afbd8cc Mon Sep 17 00:00:00 2001 From: GithubAnon0000 <160563432+GithubAnon0000@users.noreply.github.com> Date: Sat, 12 Jul 2025 06:41:22 +0000 Subject: [PATCH 05/92] FIX Graph Tooltip uses wrong font (#4193) --- ts/routes/graphs/Tooltip.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/ts/routes/graphs/Tooltip.svelte b/ts/routes/graphs/Tooltip.svelte index 12d12fb0f..d6b64df2c 100644 --- a/ts/routes/graphs/Tooltip.svelte +++ b/ts/routes/graphs/Tooltip.svelte @@ -47,6 +47,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html white-space: nowrap; padding: 15px; border-radius: 5px; + font-family: inherit; font-size: 15px; opacity: 0; pointer-events: none; From 3b18097550fd5441473c91243a7c687a55230356 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 12 Jul 2025 23:34:09 +0700 Subject: [PATCH 06/92] Support user pyproject modifications again This changes 'keep existing version' to 'sync project changes' when changes to the pyproject.toml file have been detected that are newer than the installer's version. Also adds a way to temporarily enable the launcher, as we needed some other trigger than the pyproject.toml file anyway, and this approach also solves #4165. And removes the 'quit' option, since it's an uncommon operation, and the user can just close the window instead. Short-term caveat: users with older launchers/addon will trigger the old pyproject.toml mtime bump, leading to a 'sync project changes' message that will not make much sense to a typical user. --- qt/aqt/package.py | 9 ++---- qt/launcher/addon/__init__.py | 9 ++---- qt/launcher/src/main.rs | 53 ++++++++++++++++++++++++----------- 3 files changed, 42 insertions(+), 29 deletions(-) diff --git a/qt/aqt/package.py b/qt/aqt/package.py index 968218741..c8d481312 100644 --- a/qt/aqt/package.py +++ b/qt/aqt/package.py @@ -124,17 +124,14 @@ def launcher_executable() -> str | None: def trigger_launcher_run() -> None: - """Bump the mtime on pyproject.toml in the local data directory to trigger an update on next run.""" + """Create a trigger file to request launcher UI on next run.""" try: root = launcher_root() if not root: return - pyproject_path = Path(root) / "pyproject.toml" - - if pyproject_path.exists(): - # Touch the file to update its mtime - pyproject_path.touch() + trigger_path = Path(root) / ".want-launcher" + trigger_path.touch() except Exception as e: print(e) diff --git a/qt/launcher/addon/__init__.py b/qt/launcher/addon/__init__.py index 63a2cc5a9..799406e86 100644 --- a/qt/launcher/addon/__init__.py +++ b/qt/launcher/addon/__init__.py @@ -69,17 +69,14 @@ def add_python_requirements(reqs: list[str]) -> tuple[bool, str]: def trigger_launcher_run() -> None: - """Bump the mtime on pyproject.toml in the local data directory to trigger an update on next run.""" + """Create a trigger file to request launcher UI on next run.""" try: root = launcher_root() if not root: return - pyproject_path = Path(root) / "pyproject.toml" - - if pyproject_path.exists(): - # Touch the file to update its mtime - pyproject_path.touch() + trigger_path = Path(root) / ".want-launcher" + trigger_path.touch() except Exception as e: print(e) diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index 15548d3bc..d48adaf49 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -46,6 +46,8 @@ struct State { dist_python_version_path: std::path::PathBuf, uv_lock_path: std::path::PathBuf, sync_complete_marker: std::path::PathBuf, + launcher_trigger_file: std::path::PathBuf, + pyproject_modified_by_user: bool, previous_version: Option, resources_dir: std::path::PathBuf, } @@ -70,7 +72,6 @@ pub enum MainMenuChoice { ToggleBetas, ToggleCache, Uninstall, - Quit, } fn main() { @@ -106,6 +107,8 @@ fn run() -> Result<()> { dist_python_version_path: resources_dir.join(".python-version"), uv_lock_path: uv_install_root.join("uv.lock"), sync_complete_marker: uv_install_root.join(".sync_complete"), + launcher_trigger_file: uv_install_root.join(".want-launcher"), + pyproject_modified_by_user: false, // calculated later previous_version: None, resources_dir, }; @@ -125,15 +128,15 @@ fn run() -> Result<()> { &state.user_python_version_path, )?; - let pyproject_has_changed = !state.sync_complete_marker.exists() || { - let pyproject_toml_time = modified_time(&state.user_pyproject_path)?; - let sync_complete_time = modified_time(&state.sync_complete_marker)?; - Ok::(pyproject_toml_time > sync_complete_time) - } - .unwrap_or(true); + let launcher_requested = state.launcher_trigger_file.exists(); - if !pyproject_has_changed { - // If venv is already up to date, launch Anki normally + // Calculate whether user has custom edits that need syncing + let pyproject_time = file_timestamp_secs(&state.user_pyproject_path); + 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 { + // 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)?; launch_anki_normally(cmd)?; @@ -143,6 +146,11 @@ fn run() -> Result<()> { // If we weren't in a terminal, respawn ourselves in one ensure_terminal_shown()?; + if launcher_requested { + // Remove the trigger file to make request ephemeral + let _ = remove_file(&state.launcher_trigger_file); + } + print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top println!("\x1B[1mAnki Launcher\x1B[0m\n"); @@ -313,9 +321,11 @@ fn main_menu_loop(state: &State) -> Result<()> { let menu_choice = get_main_menu_choice(state)?; match menu_choice { - MainMenuChoice::Quit => std::process::exit(0), MainMenuChoice::KeepExisting => { - // Skip sync, just launch existing installation + if state.pyproject_modified_by_user { + // User has custom edits, sync them + handle_version_install_or_update(state, MainMenuChoice::KeepExisting)?; + } break; } MainMenuChoice::ToggleBetas => { @@ -372,14 +382,28 @@ fn write_sync_marker(sync_complete_marker: &std::path::Path) -> Result<()> { Ok(()) } +/// Get mtime of provided file, or 0 if unavailable +fn file_timestamp_secs(path: &std::path::Path) -> i64 { + modified_time(path) + .map(|t| t.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs() as i64) + .unwrap_or_default() +} + fn get_main_menu_choice(state: &State) -> Result { loop { println!("1) Latest Anki (press Enter)"); println!("2) Choose a version"); + if let Some(current_version) = &state.current_version { let normalized_current = normalize_version(current_version); - println!("3) Keep existing version ({normalized_current})"); + + if state.pyproject_modified_by_user { + println!("3) Sync project changes"); + } else { + println!("3) Keep existing version ({normalized_current})"); + } } + if let Some(prev_version) = &state.previous_version { if state.current_version.as_ref() != Some(prev_version) { let normalized_prev = normalize_version(prev_version); @@ -400,7 +424,6 @@ fn get_main_menu_choice(state: &State) -> Result { ); println!(); println!("7) Uninstall"); - println!("8) Quit"); print!("> "); let _ = stdout().flush(); @@ -440,7 +463,6 @@ fn get_main_menu_choice(state: &State) -> Result { "5" => MainMenuChoice::ToggleBetas, "6" => MainMenuChoice::ToggleCache, "7" => MainMenuChoice::Uninstall, - "8" => MainMenuChoice::Quit, _ => { println!("Invalid input. Please try again."); continue; @@ -698,9 +720,6 @@ fn update_pyproject_for_version(menu_choice: MainMenuChoice, state: &State) -> R MainMenuChoice::Version(version_kind) => { apply_version_kind(&version_kind, state)?; } - MainMenuChoice::Quit => { - std::process::exit(0); - } } Ok(()) } From 4604bc75673b68c804dc1afe1ebb83ac1497f85e Mon Sep 17 00:00:00 2001 From: jcznk <60730312+jcznk@users.noreply.github.com> Date: Sun, 13 Jul 2025 21:21:31 +0200 Subject: [PATCH 07/92] Add margin to QPushButton to prevent clipping (#4201) * Update CONTRIBUTORS * Add margin to QPushButton to prevent clipping --- CONTRIBUTORS | 1 + qt/aqt/stylesheets.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index c22bc764a..9645c698f 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -234,6 +234,7 @@ Emmanuel Ferdman Sunong2008 Marvin Kopf Kevin Nakamura +jcznk ******************** diff --git a/qt/aqt/stylesheets.py b/qt/aqt/stylesheets.py index a262e18b9..6b4eff1f5 100644 --- a/qt/aqt/stylesheets.py +++ b/qt/aqt/stylesheets.py @@ -177,9 +177,13 @@ class CustomStyles: QPushButton:default {{ border: 1px solid {tm.var(colors.BORDER_FOCUS)}; }} + QPushButton {{ + margin: 1px; + }} QPushButton:focus {{ border: 2px solid {tm.var(colors.BORDER_FOCUS)}; outline: none; + margin: 0px; }} QPushButton:hover, QTabBar::tab:hover, From 58a8aa7353982a9928d33b5000c536f4e005025b Mon Sep 17 00:00:00 2001 From: Bradley Szoke Date: Sun, 13 Jul 2025 14:29:23 -0500 Subject: [PATCH 08/92] fix: set cursor to pointer when on range (#4197) * set cursor to pointer when on range * chore: white space removal * chore: update contributors file --- CONTRIBUTORS | 1 + ts/routes/deck-options/EasyDaysInput.svelte | 1 + 2 files changed, 2 insertions(+) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 9645c698f..b5dfe1d53 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -234,6 +234,7 @@ Emmanuel Ferdman Sunong2008 Marvin Kopf Kevin Nakamura +Bradley Szoke jcznk ******************** diff --git a/ts/routes/deck-options/EasyDaysInput.svelte b/ts/routes/deck-options/EasyDaysInput.svelte index fb5d9cd2d..c5fb4909a 100644 --- a/ts/routes/deck-options/EasyDaysInput.svelte +++ b/ts/routes/deck-options/EasyDaysInput.svelte @@ -85,6 +85,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } .easy-days-settings input[type="range"] { width: 100%; + cursor: pointer; } .day { From a1934ae9e44ef6916a10647468e61ac2e47fb4fe Mon Sep 17 00:00:00 2001 From: sorata <136738526+brishtibheja@users.noreply.github.com> Date: Mon, 14 Jul 2025 01:05:21 +0530 Subject: [PATCH 09/92] update preferences.ftl (#4196) --- ftl/core/preferences.ftl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ftl/core/preferences.ftl b/ftl/core/preferences.ftl index ce24df434..23b72f267 100644 --- a/ftl/core/preferences.ftl +++ b/ftl/core/preferences.ftl @@ -34,7 +34,7 @@ preferences-when-adding-default-to-current-deck = When adding, default to curren preferences-you-can-restore-backups-via-fileswitch = You can restore backups via File > Switch Profile. preferences-legacy-timezone-handling = Legacy timezone handling (buggy, but required for AnkiDroid <= 2.14) preferences-default-search-text = Default search text -preferences-default-search-text-example = eg. 'deck:current ' +preferences-default-search-text-example = e.g. "deck:current" preferences-theme = Theme preferences-theme-follow-system = Follow System preferences-theme-light = Light From 0375b4aac050c76f971ad071ad1bd3f632b69283 Mon Sep 17 00:00:00 2001 From: llama Date: Mon, 14 Jul 2025 06:22:14 +0800 Subject: [PATCH 10/92] fix default-coloured io masks not following css var (#4202) --- ts/routes/image-occlusion/review.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ts/routes/image-occlusion/review.ts b/ts/routes/image-occlusion/review.ts index ae225449e..4425a8141 100644 --- a/ts/routes/image-occlusion/review.ts +++ b/ts/routes/image-occlusion/review.ts @@ -7,7 +7,7 @@ import { ModuleName, setupI18n } from "@tslib/i18n"; import { optimumPixelSizeForCanvas } from "./canvas-scale"; import { Shape } from "./shapes"; import { Ellipse, extractShapesFromRenderedClozes, Polygon, Rectangle, Text } from "./shapes"; -import { TEXT_BACKGROUND_COLOR, TEXT_FONT_FAMILY, TEXT_PADDING } from "./tools/lib"; +import { SHAPE_MASK_COLOR, TEXT_BACKGROUND_COLOR, TEXT_FONT_FAMILY, TEXT_PADDING } from "./tools/lib"; import type { Size } from "./types"; export type DrawShapesData = { @@ -217,7 +217,7 @@ function drawShapes( context, size, shape, - fill: shape.fill ?? properties.inActiveShapeColor, + fill: shape.fill !== SHAPE_MASK_COLOR ? shape.fill : properties.inActiveShapeColor, stroke: properties.inActiveBorder.color, strokeWidth: properties.inActiveBorder.width, }); @@ -437,7 +437,7 @@ function getShapeProperties(): ShapeProperties { activeShapeColor: activeShapeColor ? activeShapeColor : "#ff8e8e", inActiveShapeColor: inActiveShapeColor ? inActiveShapeColor - : "#ffeba2", + : SHAPE_MASK_COLOR, highlightShapeColor: highlightShapeColor ? highlightShapeColor : "#ff8e8e00", From 5a19027185e4a4d6ca072045545a55451ae1294d Mon Sep 17 00:00:00 2001 From: user1823 <92206575+user1823@users.noreply.github.com> Date: Tue, 15 Jul 2025 15:29:06 +0530 Subject: [PATCH 11/92] Minor tweak in simulator string (#4204) --- ftl/core/statistics.ftl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ftl/core/statistics.ftl b/ftl/core/statistics.ftl index cb1accc88..c5551ef67 100644 --- a/ftl/core/statistics.ftl +++ b/ftl/core/statistics.ftl @@ -80,7 +80,7 @@ statistics-reviews = # This fragment of the tooltip in the FSRS simulation # diagram (Deck options -> FSRS) shows the total number of # cards that can be recalled or retrieved on a specific date. -statistics-memorized = {$memorized} memorized +statistics-memorized = {$memorized} cards memorized statistics-today-title = Today statistics-today-again-count = Again count: statistics-today-type-counts = Learn: { $learnCount }, Review: { $reviewCount }, Relearn: { $relearnCount }, Filtered: { $filteredCount } From 834fb41015cc7cb60518f81df0977bf4ee094f84 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 15 Jul 2025 16:27:21 +0700 Subject: [PATCH 12/92] Exclude VIRTUAL_ENV from environ as well https://forums.ankiweb.net/t/anki-25-08-beta/63645/51 --- build/runner/src/pyenv.rs | 2 +- qt/launcher/src/main.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/runner/src/pyenv.rs b/build/runner/src/pyenv.rs index 9d65626ca..efd58fd91 100644 --- a/build/runner/src/pyenv.rs +++ b/build/runner/src/pyenv.rs @@ -36,7 +36,7 @@ pub fn setup_pyenv(args: PyenvArgs) { // remove UV_* environment variables to avoid interference for (key, _) in std::env::vars() { - if key.starts_with("UV_") { + if key.starts_with("UV_") || key == "VIRTUAL_ENV" { command.env_remove(key); } } diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index d48adaf49..e1529f794 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -269,7 +269,7 @@ fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Re // remove UV_* environment variables to avoid interference for (key, _) in std::env::vars() { - if key.starts_with("UV_") { + if key.starts_with("UV_") || key == "VIRTUAL_ENV" { command.env_remove(key); } } From 12635f4cd29434dedb1240fba111eee54c87bb3c Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 15 Jul 2025 18:25:11 +0700 Subject: [PATCH 13/92] Show Chromium version in About instead of PyQt version --- qt/aqt/about.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qt/aqt/about.py b/qt/aqt/about.py index 228d3cfeb..828506fa6 100644 --- a/qt/aqt/about.py +++ b/qt/aqt/about.py @@ -70,10 +70,10 @@ def show(mw: aqt.AnkiQt) -> QDialog: abouttext += f"

{lede}" abouttext += f"

{tr.about_anki_is_licensed_under_the_agpl3()}" abouttext += f"

{tr.about_version(val=version_with_build())}
" - abouttext += ("Python %s Qt %s PyQt %s
") % ( + abouttext += ("Python %s Qt %s Chromium %s
") % ( platform.python_version(), qVersion(), - PYQT_VERSION_STR, + qWebEngineChromiumVersion().split(".")[0], ) abouttext += ( without_unicode_isolation(tr.about_visit_website(val=aqt.appWebsite)) From 15bbcdd568b08b849656e84f68898b3b1b8a7c85 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 15 Jul 2025 18:26:02 +0700 Subject: [PATCH 14/92] Downgrade Chromium as potential rendering fix https://forums.ankiweb.net/t/anki-25-08-beta/63645/57 --- qt/pyproject.toml | 4 ++-- uv.lock | 46 +++++++++++++++++++++++++++++++++------------- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/qt/pyproject.toml b/qt/pyproject.toml index 6a686dde8..1ff34f59d 100644 --- a/qt/pyproject.toml +++ b/qt/pyproject.toml @@ -40,8 +40,8 @@ qt67 = [ qt = [ "pyqt6==6.9.1", "pyqt6-qt6==6.9.1", - "pyqt6-webengine==6.9.0", - "pyqt6-webengine-qt6==6.9.1", + "pyqt6-webengine==6.8.0", + "pyqt6-webengine-qt6==6.8.2", "pyqt6_sip==13.10.2", ] qt68 = [ diff --git a/uv.lock b/uv.lock index 3f68b25dc..5a1a7ded9 100644 --- a/uv.lock +++ b/uv.lock @@ -170,8 +170,8 @@ dependencies = [ { name = "pyqt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt66' and extra != 'extra-3-aqt-qt67' and extra != 'extra-3-aqt-qt68')" }, { name = "pyqt6-webengine", version = "6.6.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt66' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68')" }, { name = "pyqt6-webengine", version = "6.7.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt67' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68')" }, - { name = "pyqt6-webengine", version = "6.8.0", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra != 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68')" }, - { name = "pyqt6-webengine", version = "6.9.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt66' and extra != 'extra-3-aqt-qt67' and extra != 'extra-3-aqt-qt68')" }, + { name = "pyqt6-webengine", version = "6.8.0", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or extra == 'extra-3-aqt-qt68' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, + { name = "pyqt6-webengine", version = "6.9.0", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt66' and extra != 'extra-3-aqt-qt67' and extra != 'extra-3-aqt-qt68')" }, { name = "pywin32", marker = "sys_platform == 'win32' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68')" }, { name = "requests" }, { name = "send2trash" }, @@ -186,8 +186,8 @@ qt = [ { name = "pyqt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" } }, { name = "pyqt6-qt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" } }, { name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" } }, - { name = "pyqt6-webengine", version = "6.9.0", source = { registry = "https://pypi.org/simple" } }, - { name = "pyqt6-webengine-qt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" } }, + { name = "pyqt6-webengine", version = "6.8.0", source = { registry = "https://pypi.org/simple" } }, + { name = "pyqt6-webengine-qt6", version = "6.8.2", source = { registry = "https://pypi.org/simple" } }, ] qt66 = [ { name = "pyqt6", version = "6.6.1", source = { registry = "https://pypi.org/simple" } }, @@ -234,11 +234,11 @@ requires-dist = [ { name = "pyqt6-sip", marker = "extra == 'qt67'", specifier = "==13.10.2" }, { name = "pyqt6-sip", marker = "extra == 'qt68'", specifier = "==13.10.2" }, { name = "pyqt6-webengine", specifier = ">=6.2" }, - { name = "pyqt6-webengine", marker = "extra == 'qt'", specifier = "==6.9.0" }, + { name = "pyqt6-webengine", marker = "extra == 'qt'", specifier = "==6.8.0" }, { name = "pyqt6-webengine", marker = "extra == 'qt66'", specifier = "==6.6.0" }, { name = "pyqt6-webengine", marker = "extra == 'qt67'", specifier = "==6.7.0" }, { name = "pyqt6-webengine", marker = "extra == 'qt68'", specifier = "==6.8.0" }, - { name = "pyqt6-webengine-qt6", marker = "extra == 'qt'", specifier = "==6.9.1" }, + { name = "pyqt6-webengine-qt6", marker = "extra == 'qt'", specifier = "==6.8.2" }, { name = "pyqt6-webengine-qt6", marker = "extra == 'qt66'", specifier = "==6.6.2" }, { name = "pyqt6-webengine-qt6", marker = "extra == 'qt67'", specifier = "==6.7.3" }, { name = "pyqt6-webengine-qt6", marker = "extra == 'qt68'", specifier = "==6.8.1" }, @@ -535,7 +535,7 @@ name = "importlib-metadata" version = "8.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp" }, + { name = "zipp", marker = "python_full_version < '3.10' or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } wheels = [ @@ -1017,8 +1017,8 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "pyqt6-qt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" } }, - { name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" } }, + { name = "pyqt6-qt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt66' and extra != 'extra-3-aqt-qt67' and extra != 'extra-3-aqt-qt68')" }, + { name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt66' and extra != 'extra-3-aqt-qt67' and extra != 'extra-3-aqt-qt68')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/32/1b/567f46eb43ca961efd38d7a0b73efb70d7342854f075fd919179fdb2a571/pyqt6-6.9.1.tar.gz", hash = "sha256:50642be03fb40f1c2111a09a1f5a0f79813e039c15e78267e6faaf8a96c1c3a6", size = 1067230, upload-time = "2025-06-06T08:49:30.307Z" } wheels = [ @@ -1229,8 +1229,10 @@ resolution-markers = [ ] dependencies = [ { name = "pyqt6", version = "6.8.0", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra != 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68')" }, - { name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra != 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68')" }, + { name = "pyqt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68')" }, + { name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or extra == 'extra-3-aqt-qt68' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67')" }, { name = "pyqt6-webengine-qt6", version = "6.8.1", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra != 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68')" }, + { name = "pyqt6-webengine-qt6", version = "6.8.2", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-3-aqt-qt' or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/c8/cadaa950eaf97f29e48c435e274ea5a81c051e745a3e2f5d9d994b7a6cda/PyQt6_WebEngine-6.8.0.tar.gz", hash = "sha256:64045ea622b6a41882c2b18f55ae9714b8660acff06a54e910eb72822c2f3ff2", size = 34203, upload-time = "2024-12-12T15:34:35.573Z" } wheels = [ @@ -1252,9 +1254,9 @@ resolution-markers = [ "python_full_version < '3.10'", ] dependencies = [ - { name = "pyqt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" } }, - { name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" } }, - { name = "pyqt6-webengine-qt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" } }, + { name = "pyqt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt66' and extra != 'extra-3-aqt-qt67' and extra != 'extra-3-aqt-qt68')" }, + { name = "pyqt6-sip", version = "13.10.2", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt66' and extra != 'extra-3-aqt-qt67' and extra != 'extra-3-aqt-qt68')" }, + { name = "pyqt6-webengine-qt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt68') or (extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt68') or (extra != 'extra-3-aqt-qt' and extra != 'extra-3-aqt-qt66' and extra != 'extra-3-aqt-qt67' and extra != 'extra-3-aqt-qt68')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8f/1a/9971af004a7e859347702f816fb71ecd67c3e32b2f0ae8daf1c1ded99f62/pyqt6_webengine-6.9.0.tar.gz", hash = "sha256:6ae537e3bbda06b8e06535e4852297e0bc3b00543c47929541fcc9b11981aa25", size = 34616, upload-time = "2025-04-08T08:57:35.402Z" } wheels = [ @@ -1321,6 +1323,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/b5/a641ebe3e5113bee23d911c58fdd2e65061a6e3786a26b068468b988e5d2/PyQt6_WebEngine_Qt6-6.8.1-py3-none-win_amd64.whl", hash = "sha256:0ced2a10433da2571cfa29ed882698e0e164184d54068d17ba73799c45af5f0f", size = 95657750, upload-time = "2024-12-06T13:47:43.048Z" }, ] +[[package]] +name = "pyqt6-webengine-qt6" +version = "6.8.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/da/639523b821d68a253f7fb2a8a4f2b277f5a03e9adba5a9cfcc2aa1aa9ed1/PyQt6_WebEngine_Qt6-6.8.2-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:84312705615b5fccedb386531bbd505eb110469444d778f09acd6a214836789e", size = 113127300, upload-time = "2025-02-06T12:05:55.965Z" }, + { url = "https://files.pythonhosted.org/packages/df/bd/33b89cc7cdf54d172be3f98746273b4b6fba73b4802a2e5a6fa757951b47/PyQt6_WebEngine_Qt6-6.8.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:672363b3809973bbe3408048fc49e98f5c54db8629e855d813fd531e05929007", size = 101984083, upload-time = "2025-02-06T12:06:09.736Z" }, + { url = "https://files.pythonhosted.org/packages/91/90/2693e9de1f064ac7cc10ba25548bbab6ce45a163eef07a22db3ff5ce8b81/PyQt6_WebEngine_Qt6-6.8.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:c3be75ef7563b965306de53cae0b357438672d3bf7d9b39edacc307fbeb9965e", size = 105210886, upload-time = "2025-02-06T12:06:23.944Z" }, + { url = "https://files.pythonhosted.org/packages/42/8a/f30075726c8ac391b6fbbc7ab043795ec79f56e452e9d835b883576738b2/PyQt6_WebEngine_Qt6-6.8.2-py3-none-manylinux_2_39_aarch64.whl", hash = "sha256:72c1b4c45a3226f32f6c821ee474c4418727913536a62506d9787e24a46d6f27", size = 101194628, upload-time = "2025-02-06T12:06:37.196Z" }, + { url = "https://files.pythonhosted.org/packages/f3/2a/4fe2bfd3a1ed0e27d1b8f32a5259ebe966432365391c9a541f290f5438de/PyQt6_WebEngine_Qt6-6.8.2-py3-none-win_amd64.whl", hash = "sha256:4421159f3ac4a796499b7f73e98028797a4ae636b04f920b8165308ca0b8c629", size = 95573175, upload-time = "2025-02-06T12:06:49.642Z" }, +] + [[package]] name = "pyqt6-webengine-qt6" version = "6.9.1" From cc4b0a825eff08ae98b44230d157d0ba9f40281b Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 15 Jul 2025 20:45:38 +0700 Subject: [PATCH 15/92] Update translations --- ftl/core-repo | 2 +- ftl/qt-repo | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ftl/core-repo b/ftl/core-repo index 3d04bcbf7..1a0fe850d 160000 --- a/ftl/core-repo +++ b/ftl/core-repo @@ -1 +1 @@ -Subproject commit 3d04bcbf7fefca0007bc9db307409d88210995d8 +Subproject commit 1a0fe850d2e49d4e4a2c604c8a73d2f3b8042e04 diff --git a/ftl/qt-repo b/ftl/qt-repo index c65a9587b..83ca9040e 160000 --- a/ftl/qt-repo +++ b/ftl/qt-repo @@ -1 +1 @@ -Subproject commit c65a9587b1f18931986bdf145872e8e4c44c5c82 +Subproject commit 83ca9040efa906bf383a2e00e31ef1e5760b2099 From 4e29440d6a08b32af15271a99cc2c1e515d09dae Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 15 Jul 2025 22:26:39 +0700 Subject: [PATCH 16/92] Version the launcher --- qt/aqt/about.py | 2 +- qt/launcher/lin/build.sh | 7 ++++--- qt/launcher/mac/Info.plist | 2 +- qt/launcher/mac/build.sh | 3 ++- qt/launcher/mac/dmg/build.sh | 3 ++- qt/launcher/src/bin/build_win.rs | 23 +++++++++++++++++------ qt/launcher/win/anki.template.nsi | 4 ++-- 7 files changed, 29 insertions(+), 15 deletions(-) diff --git a/qt/aqt/about.py b/qt/aqt/about.py index 828506fa6..586225b73 100644 --- a/qt/aqt/about.py +++ b/qt/aqt/about.py @@ -73,7 +73,7 @@ def show(mw: aqt.AnkiQt) -> QDialog: abouttext += ("Python %s Qt %s Chromium %s
") % ( platform.python_version(), qVersion(), - qWebEngineChromiumVersion().split(".")[0], + (qWebEngineChromiumVersion() or "").split(".")[0], ) abouttext += ( without_unicode_isolation(tr.about_visit_website(val=aqt.appWebsite)) diff --git a/qt/launcher/lin/build.sh b/qt/launcher/lin/build.sh index 7bd78c27d..f38f6defe 100755 --- a/qt/launcher/lin/build.sh +++ b/qt/launcher/lin/build.sh @@ -13,7 +13,8 @@ HOST_ARCH=$(uname -m) # Define output paths OUTPUT_DIR="../../../out/launcher" -LAUNCHER_DIR="$OUTPUT_DIR/anki-linux" +ANKI_VERSION=$(cat ../../../.version | tr -d '\n') +LAUNCHER_DIR="$OUTPUT_DIR/anki-launcher-$ANKI_VERSION-linux" # Clean existing output directory rm -rf "$LAUNCHER_DIR" @@ -77,8 +78,8 @@ chmod +x \ chmod -R a+r "$LAUNCHER_DIR" ZSTD="zstd -c --long -T0 -18" -TRANSFORM="s%^.%anki-linux%S" -TARBALL="$OUTPUT_DIR/anki-linux.tar.zst" +TRANSFORM="s%^.%anki-launcher-$ANKI_VERSION-linux%S" +TARBALL="$OUTPUT_DIR/anki-launcher-$ANKI_VERSION-linux.tar.zst" tar -I "$ZSTD" --transform "$TRANSFORM" -cf "$TARBALL" -C "$LAUNCHER_DIR" . diff --git a/qt/launcher/mac/Info.plist b/qt/launcher/mac/Info.plist index ac0ab2f09..a48960208 100644 --- a/qt/launcher/mac/Info.plist +++ b/qt/launcher/mac/Info.plist @@ -5,7 +5,7 @@ CFBundleDisplayName Anki CFBundleShortVersionString - 1.0 + ANKI_VERSION LSMinimumSystemVersion 12 LSApplicationCategoryType diff --git a/qt/launcher/mac/build.sh b/qt/launcher/mac/build.sh index 470b5cd25..d521e155b 100755 --- a/qt/launcher/mac/build.sh +++ b/qt/launcher/mac/build.sh @@ -31,7 +31,8 @@ lipo -create \ cp "$OUTPUT_DIR/uv" "$APP_LAUNCHER/Contents/MacOS/" # Copy support files -cp Info.plist "$APP_LAUNCHER/Contents/" +ANKI_VERSION=$(cat ../../../.version | tr -d '\n') +sed "s/ANKI_VERSION/$ANKI_VERSION/g" Info.plist > "$APP_LAUNCHER/Contents/Info.plist" cp icon/Assets.car "$APP_LAUNCHER/Contents/Resources/" cp ../pyproject.toml "$APP_LAUNCHER/Contents/Resources/" cp ../../../.python-version "$APP_LAUNCHER/Contents/Resources/" diff --git a/qt/launcher/mac/dmg/build.sh b/qt/launcher/mac/dmg/build.sh index 16b48c06a..7eeba9948 100755 --- a/qt/launcher/mac/dmg/build.sh +++ b/qt/launcher/mac/dmg/build.sh @@ -6,7 +6,8 @@ set -e # base folder with Anki.app in it output="$1" dist="$1/tmp" -dmg_path="$output/Anki.dmg" +ANKI_VERSION=$(cat ../../../.version | tr -d '\n') +dmg_path="$output/anki-launcher-$ANKI_VERSION-mac.dmg" if [ -d "/Volumes/Anki" ] then diff --git a/qt/launcher/src/bin/build_win.rs b/qt/launcher/src/bin/build_win.rs index 96688f190..4c2ca4413 100644 --- a/qt/launcher/src/bin/build_win.rs +++ b/qt/launcher/src/bin/build_win.rs @@ -22,6 +22,11 @@ const NSIS_PATH: &str = "C:\\Program Files (x86)\\NSIS\\makensis.exe"; fn main() -> Result<()> { println!("Building Windows launcher..."); + // Read version early so it can be used throughout the build process + let version = std::fs::read_to_string("../../../.version")? + .trim() + .to_string(); + let output_dir = PathBuf::from(OUTPUT_DIR); let launcher_exe_dir = PathBuf::from(LAUNCHER_EXE_DIR); let nsis_dir = PathBuf::from(NSIS_DIR); @@ -31,16 +36,20 @@ fn main() -> Result<()> { extract_nsis_plugins()?; copy_files(&output_dir)?; sign_binaries(&output_dir)?; - copy_nsis_files(&nsis_dir)?; + copy_nsis_files(&nsis_dir, &version)?; build_uninstaller(&output_dir, &nsis_dir)?; sign_file(&output_dir.join("uninstall.exe"))?; generate_install_manifest(&output_dir)?; build_installer(&output_dir, &nsis_dir)?; - sign_file(&PathBuf::from("../../../out/launcher_exe/anki-install.exe"))?; + + let installer_filename = format!("anki-launcher-{version}-windows.exe"); + let installer_path = PathBuf::from("../../../out/launcher_exe").join(&installer_filename); + + sign_file(&installer_path)?; println!("Build completed successfully!"); println!("Output directory: {}", output_dir.display()); - println!("Installer: ../../../out/launcher_exe/anki-install.exe"); + println!("Installer: ../../../out/launcher_exe/{installer_filename}"); Ok(()) } @@ -235,11 +244,13 @@ fn generate_install_manifest(output_dir: &Path) -> Result<()> { Ok(()) } -fn copy_nsis_files(nsis_dir: &Path) -> Result<()> { +fn copy_nsis_files(nsis_dir: &Path, version: &str) -> Result<()> { println!("Copying NSIS support files..."); - // Copy anki.template.nsi as anki.nsi - copy_file("anki.template.nsi", nsis_dir.join("anki.nsi"))?; + // Copy anki.template.nsi as anki.nsi and substitute version placeholders + let template_content = std::fs::read_to_string("anki.template.nsi")?; + let substituted_content = template_content.replace("ANKI_VERSION", version); + write_file(nsis_dir.join("anki.nsi"), substituted_content)?; // Copy fileassoc.nsh copy_file("fileassoc.nsh", nsis_dir.join("fileassoc.nsh"))?; diff --git a/qt/launcher/win/anki.template.nsi b/qt/launcher/win/anki.template.nsi index 84dedf9c8..36b32a893 100644 --- a/qt/launcher/win/anki.template.nsi +++ b/qt/launcher/win/anki.template.nsi @@ -24,7 +24,7 @@ Name "Anki" Unicode true ; The file to write (relative to nsis directory) -OutFile "..\launcher_exe\anki-install.exe" +OutFile "..\launcher_exe\anki-launcher-ANKI_VERSION-windows.exe" ; Non elevated RequestExecutionLevel user @@ -214,7 +214,7 @@ Section "" ; Write the uninstall keys for Windows WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "DisplayName" "Anki Launcher" - WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "DisplayVersion" "1.0.0" + WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "DisplayVersion" "ANKI_VERSION" WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "UninstallString" '"$INSTDIR\uninstall.exe"' WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /S' WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "NoModify" 1 From e77cd791deb8d412d1ed9083f2697e389605823a Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 15 Jul 2025 20:47:12 +0700 Subject: [PATCH 17/92] Bump version --- .version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.version b/.version index ce73bf7c0..469f7be32 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -25.08b1 +25.08b2 From 37fe704326d60c8609c8ea834415064689ea542b Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 16 Jul 2025 14:14:50 +0700 Subject: [PATCH 18/92] Tweak protobuf requirements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivated by https://forums.ankiweb.net/t/python-anki-sync-server-broken/64069 From https://protobuf.dev/support/cross-version-runtime-guarantee/: "Python-specific Guarantees Since the 3.20.0 release, the Protobuf Python generated code became a thin wrapper around an embedded FileDescriptorProto. Because these protos are supported on extremely long timeframes, our usual major version compatibility windows aren’t typically necessary. Python may break generated code compatibility in specific future major version releases, but it will be coupled with poison pill warnings and errors in advance. As of 6.32.0, all generated code since 3.20.0 will be supported until at least 8.x.y." --- pylib/pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pylib/pyproject.toml b/pylib/pyproject.toml index 23e10077f..fb7422694 100644 --- a/pylib/pyproject.toml +++ b/pylib/pyproject.toml @@ -7,7 +7,7 @@ dependencies = [ "decorator", "markdown", "orjson", - "protobuf>=4.21", + "protobuf>=6.0,<8.0", "requests[socks]", # remove after we update to min python 3.11+ "typing_extensions", diff --git a/uv.lock b/uv.lock index 5a1a7ded9..d4277425a 100644 --- a/uv.lock +++ b/uv.lock @@ -66,7 +66,7 @@ requires-dist = [ { name = "distro", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" }, { name = "markdown" }, { name = "orjson" }, - { name = "protobuf", specifier = ">=4.21" }, + { name = "protobuf", specifier = ">=6.0,<8.0" }, { name = "requests", extras = ["socks"] }, { name = "typing-extensions" }, ] From 0b30155c90fb6a8cd4af1abe6c2ea4a577f5d3ab Mon Sep 17 00:00:00 2001 From: Danika-Dakika <115673540+Danika-Dakika@users.noreply.github.com> Date: Thu, 17 Jul 2025 18:05:01 -0700 Subject: [PATCH 19/92] Adding to about.py (#4211) Adding a Hebrew translator. --- qt/aqt/about.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qt/aqt/about.py b/qt/aqt/about.py index 586225b73..03e989f2c 100644 --- a/qt/aqt/about.py +++ b/qt/aqt/about.py @@ -225,6 +225,7 @@ def show(mw: aqt.AnkiQt) -> QDialog: "Adnane Taghi", "Anon_0000", "Bilolbek Normuminov", + "Sagiv Marzini", ) ) From 278a84f8d2ec068fd6e2f398ebb9553f64e80f6c Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 18 Jul 2025 18:12:30 +0700 Subject: [PATCH 20/92] Fix 'same cloze' shortcut on macOS https://forums.ankiweb.net/t/mac-shortcut-for-cloze-deletion-same-card/63785 --- ts/editor/ClozeButtons.svelte | 12 ++++++++---- ts/lib/tslib/platform.ts | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/ts/editor/ClozeButtons.svelte b/ts/editor/ClozeButtons.svelte index e9faae540..dfe4fb7c3 100644 --- a/ts/editor/ClozeButtons.svelte +++ b/ts/editor/ClozeButtons.svelte @@ -4,7 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html --> - - openHelpModal("desiredRetention")}> - {tr.deckConfigDesiredRetention()} - - + + + + + openHelpModal("desiredRetention")}> + {tr.deckConfigDesiredRetention()} + + + + diff --git a/ts/routes/deck-options/SpinBoxFloatRow.svelte b/ts/routes/deck-options/SpinBoxFloatRow.svelte index 5aa93bd30..3b16f32d8 100644 --- a/ts/routes/deck-options/SpinBoxFloatRow.svelte +++ b/ts/routes/deck-options/SpinBoxFloatRow.svelte @@ -23,9 +23,12 @@ - - - - + + + + + + + From 1af3c58d405ded0ec281c48fcc3a703810f70f48 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Mon, 28 Jul 2025 09:55:08 +0100 Subject: [PATCH 48/92] Feat/Desired retention info graphs (#4199) * backend part * split memorised and cost * slapdash frontend * extract some simulator logic * Add zoomed version of graph * ./check * Fix: Tooltip * Fix: Simulator/workload transition * remove "time" * Update ts/routes/graphs/simulator.ts Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com> * Added: Mode toggle * Disable Dr in workload mode * keep button order consistant between modes * dont clear points on mode swap * add review count graph * Revert "dont clear points on mode swap" This reverts commit fc89efb1d954ce331002b4dd0b464109938b8a35. * "Help me pick" button * unrelated title case change * Add translation strings * fix: missing translation string * Fix: Layout shift * Add: Experimental * Fix Time / Memorized * per day values * set review limit to 9999 on open * keep default at currently set value * Do DR calculation in parallel (dae) Approx 5x faster on my machine --------- Co-authored-by: user1823 <92206575+user1823@users.noreply.github.com> Co-authored-by: Damien Elmes --- Cargo.lock | 1 + Cargo.toml | 1 + ftl/core/deck-config.ftl | 8 +- proto/anki/scheduler.proto | 8 + qt/aqt/mediasrv.py | 1 + rslib/Cargo.toml | 1 + rslib/src/scheduler/fsrs/simulator.rs | 36 +++ rslib/src/scheduler/service/mod.rs | 8 + rslib/src/timestamp.rs | 4 + ts/routes/deck-options/FsrsOptions.svelte | 21 ++ ts/routes/deck-options/SimulatorModal.svelte | 292 +++++++++++++------ ts/routes/graphs/simulator.ts | 211 ++++++++++---- 12 files changed, 445 insertions(+), 147 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 86787124a..fce546a9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,6 +131,7 @@ dependencies = [ "prost-reflect", "pulldown-cmark 0.13.0", "rand 0.9.1", + "rayon", "regex", "reqwest 0.12.20", "rusqlite", diff --git a/Cargo.toml b/Cargo.toml index 2ff29cd1a..3ef2df9bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -110,6 +110,7 @@ prost-types = "0.13" pulldown-cmark = "0.13.0" pyo3 = { version = "0.25.1", features = ["extension-module", "abi3", "abi3-py39"] } rand = "0.9.1" +rayon = "1.10.0" regex = "1.11.1" reqwest = { version = "0.12.20", default-features = false, features = ["json", "socks", "stream", "multipart"] } rusqlite = { version = "0.36.0", features = ["trace", "functions", "collation", "bundled"] } diff --git a/ftl/core/deck-config.ftl b/ftl/core/deck-config.ftl index 01eac3369..2ca45fcd8 100644 --- a/ftl/core/deck-config.ftl +++ b/ftl/core/deck-config.ftl @@ -505,7 +505,9 @@ deck-config-desired-retention-below-optimal = Your desired retention is below op # Description of the y axis in the FSRS simulation # diagram (Deck options -> FSRS) showing the total number of # cards that can be recalled or retrieved on a specific date. -deck-config-fsrs-simulator-experimental = FSRS simulator (experimental) +deck-config-fsrs-simulator-experimental = FSRS Simulator (Experimental) +deck-config-fsrs-simulate-desired-retention-experimental = FSRS Desired Retention Simulator (Experimental) +deck-config-fsrs-desired-retention-help-me-decide-experimental = Help Me Decide (Experimental) deck-config-additional-new-cards-to-simulate = Additional new cards to simulate deck-config-simulate = Simulate deck-config-clear-last-simulate = Clear Last Simulation @@ -515,10 +517,14 @@ deck-config-smooth-graph = Smooth graph deck-config-suspend-leeches = Suspend leeches deck-config-save-options-to-preset = Save Changes to Preset deck-config-save-options-to-preset-confirm = Overwrite the options in your current preset with the options that are currently set in the simulator? +deck-config-plotted-on-x-axis = (Plotted on the X-axis) # Radio button in the FSRS simulation diagram (Deck options -> FSRS) selecting # to show the total number of cards that can be recalled or retrieved on a # specific date. deck-config-fsrs-simulator-radio-memorized = Memorized +deck-config-fsrs-simulator-radio-ratio = Time / Memorized Ratio +# $time here is pre-formatted e.g. "10 Seconds" +deck-config-fsrs-simulator-ratio-tooltip = { $time } per memorized card ## Messages related to the FSRS scheduler’s health check. The health check determines whether the correlation between FSRS predictions and your memory is good or bad. It can be optionally triggered as part of the "Optimize" function. diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index 1294b4543..a88cd468c 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -55,6 +55,8 @@ service SchedulerService { returns (ComputeOptimalRetentionResponse); rpc SimulateFsrsReview(SimulateFsrsReviewRequest) returns (SimulateFsrsReviewResponse); + rpc SimulateFsrsWorkload(SimulateFsrsReviewRequest) + returns (SimulateFsrsWorkloadResponse); rpc EvaluateParams(EvaluateParamsRequest) returns (EvaluateParamsResponse); rpc EvaluateParamsLegacy(EvaluateParamsLegacyRequest) returns (EvaluateParamsResponse); @@ -414,6 +416,12 @@ message SimulateFsrsReviewResponse { repeated float daily_time_cost = 4; } +message SimulateFsrsWorkloadResponse { + map cost = 1; + map memorized = 2; + map review_count = 3; +} + message ComputeOptimalRetentionResponse { float optimal_retention = 1; } diff --git a/qt/aqt/mediasrv.py b/qt/aqt/mediasrv.py index f08be4cef..d1d55e232 100644 --- a/qt/aqt/mediasrv.py +++ b/qt/aqt/mediasrv.py @@ -654,6 +654,7 @@ exposed_backend_list = [ "evaluate_params_legacy", "get_optimal_retention_parameters", "simulate_fsrs_review", + "simulate_fsrs_workload", # DeckConfigService "get_ignored_before_count", "get_retention_workload", diff --git a/rslib/Cargo.toml b/rslib/Cargo.toml index a1d24cc87..9be9e8d87 100644 --- a/rslib/Cargo.toml +++ b/rslib/Cargo.toml @@ -81,6 +81,7 @@ pin-project.workspace = true prost.workspace = true pulldown-cmark.workspace = true rand.workspace = true +rayon.workspace = true regex.workspace = true reqwest.workspace = true rusqlite.workspace = true diff --git a/rslib/src/scheduler/fsrs/simulator.rs b/rslib/src/scheduler/fsrs/simulator.rs index e032ecaf3..3b173939b 100644 --- a/rslib/src/scheduler/fsrs/simulator.rs +++ b/rslib/src/scheduler/fsrs/simulator.rs @@ -1,11 +1,13 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +use std::collections::HashMap; use std::sync::Arc; use anki_proto::deck_config::deck_config::config::ReviewCardOrder; use anki_proto::deck_config::deck_config::config::ReviewCardOrder::*; use anki_proto::scheduler::SimulateFsrsReviewRequest; use anki_proto::scheduler::SimulateFsrsReviewResponse; +use anki_proto::scheduler::SimulateFsrsWorkloadResponse; use fsrs::simulate; use fsrs::PostSchedulingFn; use fsrs::ReviewPriorityFn; @@ -14,6 +16,8 @@ use fsrs::FSRS; use itertools::Itertools; use rand::rngs::StdRng; use rand::Rng; +use rayon::iter::IntoParallelIterator; +use rayon::iter::ParallelIterator; use crate::card::CardQueue; use crate::card::CardType; @@ -267,6 +271,38 @@ impl Collection { daily_time_cost: result.cost_per_day, }) } + + pub fn simulate_workload( + &mut self, + req: SimulateFsrsReviewRequest, + ) -> Result { + let (config, cards) = self.simulate_request_to_config(&req)?; + let dr_workload = (70u32..=99u32) + .into_par_iter() + .map(|dr| { + let result = simulate( + &config, + &req.params, + dr as f32 / 100., + None, + Some(cards.clone()), + )?; + Ok(( + dr, + ( + *result.memorized_cnt_per_day.last().unwrap_or(&0.), + result.cost_per_day.iter().sum::(), + result.review_cnt_per_day.iter().sum::() as u32, + ), + )) + }) + .collect::>>()?; + Ok(SimulateFsrsWorkloadResponse { + memorized: dr_workload.iter().map(|(k, v)| (*k, v.0)).collect(), + cost: dr_workload.iter().map(|(k, v)| (*k, v.1)).collect(), + review_count: dr_workload.iter().map(|(k, v)| (*k, v.2)).collect(), + }) + } } impl Card { diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index 43d694e4f..9f42a79f7 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -16,6 +16,7 @@ use anki_proto::scheduler::FuzzDeltaResponse; use anki_proto::scheduler::GetOptimalRetentionParametersResponse; use anki_proto::scheduler::SimulateFsrsReviewRequest; use anki_proto::scheduler::SimulateFsrsReviewResponse; +use anki_proto::scheduler::SimulateFsrsWorkloadResponse; use fsrs::ComputeParametersInput; use fsrs::FSRSItem; use fsrs::FSRSReview; @@ -283,6 +284,13 @@ impl crate::services::SchedulerService for Collection { self.simulate_review(input) } + fn simulate_fsrs_workload( + &mut self, + input: SimulateFsrsReviewRequest, + ) -> Result { + self.simulate_workload(input) + } + fn compute_optimal_retention( &mut self, input: SimulateFsrsReviewRequest, diff --git a/rslib/src/timestamp.rs b/rslib/src/timestamp.rs index a020d706d..8a6ac4eb7 100644 --- a/rslib/src/timestamp.rs +++ b/rslib/src/timestamp.rs @@ -93,6 +93,10 @@ impl TimestampMillis { pub fn adding_secs(self, secs: i64) -> Self { Self(self.0 + secs * 1000) } + + pub fn elapsed_millis(self) -> u64 { + (Self::now().0 - self.0).max(0) as u64 + } } fn elapsed() -> time::Duration { diff --git a/ts/routes/deck-options/FsrsOptions.svelte b/ts/routes/deck-options/FsrsOptions.svelte index 9b7bb218e..526c3aa99 100644 --- a/ts/routes/deck-options/FsrsOptions.svelte +++ b/ts/routes/deck-options/FsrsOptions.svelte @@ -325,6 +325,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } let simulatorModal: Modal; + let workloadModal: Modal; @@ -349,6 +350,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + + @@ -444,6 +455,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {onPresetChange} /> + +