From 2d60471f3697a577aafe17ab392e13f20a90f007 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Thu, 7 Aug 2025 04:36:53 +0100 Subject: [PATCH 01/10] Use space-around for tabbed values (#4252) * space-around * have your cake and eat it --- ts/routes/deck-options/TabbedValue.svelte | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ts/routes/deck-options/TabbedValue.svelte b/ts/routes/deck-options/TabbedValue.svelte index daad409c3..87dc4ed2c 100644 --- a/ts/routes/deck-options/TabbedValue.svelte +++ b/ts/routes/deck-options/TabbedValue.svelte @@ -55,7 +55,10 @@ width: 100%; display: flex; flex-wrap: nowrap; - justify-content: space-between; + &:has(li:nth-child(3)) { + justify-content: space-between; + } + justify-content: space-around; padding-inline: 0; margin-bottom: 0.5rem; list-style: none; From 5462d99255c41ed47b7953ca6487594ba7cabd5d Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 8 Aug 2025 08:56:50 +0100 Subject: [PATCH 02/10] Fix/Retention help button bounds (#4253) * Move onTitleClick * rename variable * Fix: Tabbing issues --- ts/lib/components/TitledContainer.svelte | 28 ++++++++++-------------- ts/routes/graphs/Graph.svelte | 4 ++-- ts/routes/graphs/TrueRetention.svelte | 4 ++-- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/ts/lib/components/TitledContainer.svelte b/ts/lib/components/TitledContainer.svelte index 70e4a078c..98983940a 100644 --- a/ts/lib/components/TitledContainer.svelte +++ b/ts/lib/components/TitledContainer.svelte @@ -12,7 +12,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html export { className as class }; export let title: string; - export let onTitleClick: ((_e: MouseEvent | KeyboardEvent) => void) | null = null; + export let onHelpClick: ((_e: MouseEvent | KeyboardEvent) => void) | null = null;
- {#if onTitleClick} - + {title} + + {#if onHelpClick} +
-

- {title} -

- - {:else} -

- {title} -

+ +
{/if} -
- -
diff --git a/ts/routes/graphs/Graph.svelte b/ts/routes/graphs/Graph.svelte index 2c509639e..1dafed4bb 100644 --- a/ts/routes/graphs/Graph.svelte +++ b/ts/routes/graphs/Graph.svelte @@ -8,7 +8,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // When title is null (default), the graph is inlined, not having TitledContainer wrapper. export let title: string | null = null; export let subtitle: string | null = null; - export let onTitleClick: ((_e: MouseEvent | KeyboardEvent) => void) | null = null; + export let onHelpClick: ((_e: MouseEvent | KeyboardEvent) => void) | null = null; {#if title == null} @@ -19,7 +19,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {:else} - +
{#if subtitle} diff --git a/ts/routes/graphs/TrueRetention.svelte b/ts/routes/graphs/TrueRetention.svelte index 6b89ab9b8..9af2b4d85 100644 --- a/ts/routes/graphs/TrueRetention.svelte +++ b/ts/routes/graphs/TrueRetention.svelte @@ -57,12 +57,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html const title = tr.statisticsTrueRetentionTitle(); const subtitle = tr.statisticsTrueRetentionSubtitle(); - const onTitleClick = () => { + const onHelpClick = () => { openHelpModal(Object.keys(retentionHelp).indexOf("trueRetention")); }; - + Date: Fri, 8 Aug 2025 15:58:13 +0530 Subject: [PATCH 03/10] Fix/Exclude new cards from is_due_in_days (#4249) https://github.com/ankitects/anki/pull/4231/files#r2238901958 --- rslib/src/browser_table.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rslib/src/browser_table.rs b/rslib/src/browser_table.rs index c13d9c294..ef7453955 100644 --- a/rslib/src/browser_table.rs +++ b/rslib/src/browser_table.rs @@ -105,7 +105,7 @@ impl Card { /// Returns true if the card has a due date in terms of days. fn is_due_in_days(&self) -> bool { - self.original_or_current_due() <= 365_000 // keep consistent with SQL + self.ctype != CardType::New && self.original_or_current_due() <= 365_000 // keep consistent with SQL || matches!(self.queue, CardQueue::DayLearn | CardQueue::Review) || (self.ctype == CardType::Review && self.is_undue_queue()) } From f4266f014247eefdebb6c577a54738ab7a957198 Mon Sep 17 00:00:00 2001 From: Luc Mcgrady Date: Fri, 8 Aug 2025 11:30:10 +0100 Subject: [PATCH 04/10] Feat/Neaten dr graph x-axis (#4251) * Remove "Plotted on x axis" * Add: X tick format * fix formatx * Fix: Regular simualtor x axis --- ftl/core/deck-config.ftl | 2 +- ts/routes/deck-options/SimulatorModal.svelte | 19 ----------------- ts/routes/graphs/simulator.ts | 22 +++++++++++++++----- 3 files changed, 18 insertions(+), 25 deletions(-) diff --git a/ftl/core/deck-config.ftl b/ftl/core/deck-config.ftl index 2ca45fcd8..5154f44c1 100644 --- a/ftl/core/deck-config.ftl +++ b/ftl/core/deck-config.ftl @@ -517,7 +517,6 @@ 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. @@ -545,6 +544,7 @@ deck-config-fsrs-good-fit = Health Check: ## NO NEED TO TRANSLATE. This text is no longer used by Anki, and will be removed in the future. +deck-config-plotted-on-x-axis = (Plotted on the X-axis) deck-config-a-100-day-interval = { $days -> [one] A 100 day interval will become { $days } day. diff --git a/ts/routes/deck-options/SimulatorModal.svelte b/ts/routes/deck-options/SimulatorModal.svelte index 5493b0093..c60f90455 100644 --- a/ts/routes/deck-options/SimulatorModal.svelte +++ b/ts/routes/deck-options/SimulatorModal.svelte @@ -44,8 +44,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import Warning from "./Warning.svelte"; import type { ComputeRetentionProgress } from "@generated/anki/collection_pb"; import Modal from "bootstrap/js/dist/modal"; - import Row from "$lib/components/Row.svelte"; - import Col from "$lib/components/Col.svelte"; export let state: DeckOptionsState; export let simulateFsrsRequest: SimulateFsrsReviewRequest; @@ -373,23 +371,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {tr.deckConfigDesiredRetention()} - {:else} - - - openHelpModal("desiredRetention")} - > - {tr.deckConfigDesiredRetention()} - - - - - - {/if} formatter.format(n / 100); + const formatY: (value: number) => string = ({ [SimulateWorkloadSubgraph.ratio]: (value: number) => tr.deckConfigFsrsSimulatorRatioTooltip({ time: timeSpan(value) }), @@ -85,7 +92,7 @@ export function renderWorkloadChart( })[subgraph]; function formatX(dr: number) { - return `Desired Retention: ${dr}%
`; + return `${tr.deckConfigDesiredRetention()}: ${xTickFormat(dr)}
`; } return _renderSimulationChart( @@ -93,10 +100,11 @@ export function renderWorkloadChart( bounds, subgraph_data, x, - yTickFormat, formatY, formatX, (_e: MouseEvent, _d: number) => undefined, + yTickFormat, + xTickFormat, ); } @@ -169,10 +177,11 @@ export function renderSimulationChart( bounds, subgraph_data, x, - yTickFormat, formatY, formatX, legendMouseMove, + yTickFormat, + undefined, ); } @@ -181,10 +190,11 @@ function _renderSimulationChart( bounds: GraphBounds, subgraph_data: T[], x: any, - yTickFormat: (n: number) => string, formatY: (n: T["y"]) => string, formatX: (n: T["x"]) => string, legendMouseMove: (e: MouseEvent, d: number) => void, + yTickFormat?: (n: number) => string, + xTickFormat?: (n: number) => string, ): TableDatum[] { const svg = select(svgElem); svg.selectAll(".lines").remove(); @@ -198,7 +208,9 @@ function _renderSimulationChart( const trans = svg.transition().duration(600) as any; svg.select(".x-ticks") - .call((selection) => selection.transition(trans).call(axisBottom(x).ticks(7).tickSizeOuter(0))) + .call((selection) => + selection.transition(trans).call(axisBottom(x).ticks(7).tickSizeOuter(0).tickFormat(xTickFormat as any)) + ) .attr("direction", "ltr"); // y scale From 68bc4c02cfa87356e74c04cf025a28d590f55539 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 8 Aug 2025 20:13:24 +1000 Subject: [PATCH 05/10] Add mirror option to launcher; stop downloading automatically To give users a chance to choose a mirror first, we have to give up the automatic downloading on first run. Closes #4226 --- qt/launcher/src/main.rs | 102 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 93 insertions(+), 9 deletions(-) diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index 2d9f0aaf3..02fc08e76 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -46,6 +46,7 @@ struct State { uv_lock_path: std::path::PathBuf, sync_complete_marker: std::path::PathBuf, launcher_trigger_file: std::path::PathBuf, + mirror_path: std::path::PathBuf, pyproject_modified_by_user: bool, previous_version: Option, resources_dir: std::path::PathBuf, @@ -71,6 +72,7 @@ pub enum MainMenuChoice { Version(VersionKind), ToggleBetas, ToggleCache, + DownloadMirror, Uninstall, } @@ -108,6 +110,7 @@ fn run() -> Result<()> { 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"), + mirror_path: uv_install_root.join("mirror"), pyproject_modified_by_user: false, // calculated later previous_version: None, resources_dir, @@ -155,12 +158,7 @@ fn run() -> Result<()> { check_versions(&mut state); - let first_run = !state.venv_folder.exists(); - if first_run { - handle_version_install_or_update(&state, MainMenuChoice::Latest)?; - } else { - main_menu_loop(&state)?; - } + main_menu_loop(&state)?; // Write marker file to indicate we've completed the sync process write_sync_marker(&state)?; @@ -379,6 +377,11 @@ fn main_menu_loop(state: &State) -> Result<()> { println!(); continue; } + MainMenuChoice::DownloadMirror => { + show_mirror_submenu(state)?; + println!(); + continue; + } MainMenuChoice::Uninstall => { if handle_uninstall(state)? { std::process::exit(0); @@ -443,8 +446,13 @@ fn get_main_menu_choice(state: &State) -> Result { "6) Cache downloads: {}", if cache_enabled { "on" } else { "off" } ); + let mirror_enabled = is_mirror_enabled(state); + println!( + "7) Download mirror: {}", + if mirror_enabled { "on" } else { "off" } + ); println!(); - println!("7) Uninstall"); + println!("8) Uninstall"); print!("> "); let _ = stdout().flush(); @@ -483,7 +491,8 @@ fn get_main_menu_choice(state: &State) -> Result { } "5" => MainMenuChoice::ToggleBetas, "6" => MainMenuChoice::ToggleCache, - "7" => MainMenuChoice::Uninstall, + "7" => MainMenuChoice::DownloadMirror, + "8" => MainMenuChoice::Uninstall, _ => { println!("Invalid input. Please try again."); continue; @@ -716,7 +725,15 @@ fn apply_version_kind(version_kind: &VersionKind, state: &State) -> Result<()> { &format!("anki-release=={version}\",\n \"anki=={version}\",\n \"aqt=={version}"), ), }; - write_file(&state.user_pyproject_path, &updated_content)?; + + // Add mirror configuration if enabled + let final_content = if let Some((python_mirror, pypi_mirror)) = get_mirror_urls(state)? { + format!("{updated_content}\n\n[[tool.uv.index]]\nname = \"mirror\"\nurl = \"{pypi_mirror}\"\ndefault = true\n\n[tool.uv]\npython-install-mirror = \"{python_mirror}\"\n") + } else { + updated_content + }; + + write_file(&state.user_pyproject_path, &final_content)?; // Update .python-version based on version kind match version_kind { @@ -750,6 +767,9 @@ fn update_pyproject_for_version(menu_choice: MainMenuChoice, state: &State) -> R MainMenuChoice::ToggleCache => { unreachable!(); } + MainMenuChoice::DownloadMirror => { + unreachable!(); + } MainMenuChoice::Uninstall => { unreachable!(); } @@ -939,6 +959,70 @@ fn build_python_command(state: &State, args: &[String]) -> Result { Ok(cmd) } +fn is_mirror_enabled(state: &State) -> bool { + state.mirror_path.exists() +} + +fn get_mirror_urls(state: &State) -> Result> { + if !state.mirror_path.exists() { + return Ok(None); + } + + let content = read_file(&state.mirror_path)?; + let content_str = String::from_utf8(content).context("Invalid UTF-8 in mirror file")?; + + let lines: Vec<&str> = content_str.lines().collect(); + if lines.len() >= 2 { + Ok(Some(( + lines[0].trim().to_string(), + lines[1].trim().to_string(), + ))) + } else { + Ok(None) + } +} + +fn show_mirror_submenu(state: &State) -> Result<()> { + loop { + println!("Download mirror options:"); + println!("1) No mirror"); + println!("2) China"); + print!("> "); + let _ = stdout().flush(); + + let mut input = String::new(); + let _ = stdin().read_line(&mut input); + let input = input.trim(); + + match input { + "1" => { + // Remove mirror file + if state.mirror_path.exists() { + let _ = remove_file(&state.mirror_path); + } + println!("Mirror disabled."); + break; + } + "2" => { + // Write China mirror URLs + let china_mirrors = "https://registry.npmmirror.com/-/binary/python-build-standalone/\nhttps://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/"; + write_file(&state.mirror_path, china_mirrors)?; + println!("China mirror enabled."); + break; + } + "" => { + // Empty input - return to main menu + break; + } + _ => { + println!("Invalid input. Please try again."); + continue; + } + } + } + Ok(()) +} + #[cfg(test)] mod tests { use super::*; From 8c7cd802455f52d53b85e7e7098248d9cb236735 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 8 Aug 2025 20:21:48 +1000 Subject: [PATCH 06/10] Support socks proxies when fetching versions --- qt/launcher/src/main.rs | 2 +- qt/launcher/versions.py | 35 ++++++++++++++++++----------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index 02fc08e76..297df5b8b 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -661,7 +661,7 @@ fn fetch_versions(state: &State) -> Result> { let mut cmd = Command::new(&state.uv_path); cmd.current_dir(&state.uv_install_root) .args(["run", "--no-project", "--no-config", "--managed-python"]) - .args(["--with", "pip-system-certs"]); + .args(["--with", "pip-system-certs,requests[socks]"]); let python_version = read_file(&state.dist_python_version_path)?; let python_version_str = diff --git a/qt/launcher/versions.py b/qt/launcher/versions.py index 5d314d84f..0fdf69c84 100644 --- a/qt/launcher/versions.py +++ b/qt/launcher/versions.py @@ -3,9 +3,9 @@ import json import sys -import urllib.request import pip_system_certs.wrapt_requests +import requests pip_system_certs.wrapt_requests.inject_truststore() @@ -15,25 +15,26 @@ def main(): url = "https://pypi.org/pypi/aqt/json" try: - with urllib.request.urlopen(url, timeout=30) as response: - data = json.loads(response.read().decode("utf-8")) - releases = data.get("releases", {}) + response = requests.get(url, timeout=30) + response.raise_for_status() + data = response.json() + releases = data.get("releases", {}) - # Create list of (version, upload_time) tuples - version_times = [] - for version, files in releases.items(): - if files: # Only include versions that have files - # Use the upload time of the first file for each version - upload_time = files[0].get("upload_time_iso_8601") - if upload_time: - version_times.append((version, upload_time)) + # Create list of (version, upload_time) tuples + version_times = [] + for version, files in releases.items(): + if files: # Only include versions that have files + # Use the upload time of the first file for each version + upload_time = files[0].get("upload_time_iso_8601") + if upload_time: + version_times.append((version, upload_time)) - # Sort by upload time - version_times.sort(key=lambda x: x[1]) + # Sort by upload time + version_times.sort(key=lambda x: x[1]) - # Extract just the version names - versions = [version for version, _ in version_times] - print(json.dumps(versions)) + # Extract just the version names + versions = [version for version, _ in version_times] + print(json.dumps(versions)) except Exception as e: print(f"Error fetching versions: {e}", file=sys.stderr) sys.exit(1) From 34ed6748694de32d2c5d89af037990b778fae212 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 8 Aug 2025 20:31:05 +1000 Subject: [PATCH 07/10] 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 a0d0e232d..a599715d3 160000 --- a/ftl/core-repo +++ b/ftl/core-repo @@ -1 +1 @@ -Subproject commit a0d0e232d296ccf5750e39df2442b133267b222b +Subproject commit a599715d3c27ff2eb895c749f3534ab73d83dad1 diff --git a/ftl/qt-repo b/ftl/qt-repo index 9639c96fe..bb4207f3b 160000 --- a/ftl/qt-repo +++ b/ftl/qt-repo @@ -1 +1 @@ -Subproject commit 9639c96fe5862459aa1ff4e599079cac72a9fd7c +Subproject commit bb4207f3b8e9a7c428db282d12c75b850be532f3 From d4862e99da011016c9a2e7b7ee2abdd5c98c9b31 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 8 Aug 2025 20:37:53 +1000 Subject: [PATCH 08/10] Bump version --- .version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.version b/.version index c64758c2d..6b856e54b 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -25.08b4 +25.08b5 From a0c1a398f4bb914d368a41fb9781f25917beebea Mon Sep 17 00:00:00 2001 From: user1823 <92206575+user1823@users.noreply.github.com> Date: Sat, 9 Aug 2025 11:46:36 +0530 Subject: [PATCH 09/10] Improve elapsed seconds calculation for learning cards in browser table (#4255) * Improve calculation of elapsed seconds for learning cards in browser_table.rs https://github.com/ankitects/anki/pull/4231/files#r2257105522 * Format --- rslib/src/browser_table.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/rslib/src/browser_table.rs b/rslib/src/browser_table.rs index ef7453955..4d943e408 100644 --- a/rslib/src/browser_table.rs +++ b/rslib/src/browser_table.rs @@ -132,15 +132,14 @@ impl Card { pub(crate) fn seconds_since_last_review(&self, timing: &SchedTimingToday) -> Option { if let Some(last_review_time) = self.last_review_time { Some(timing.now.elapsed_secs_since(last_review_time) as u32) - } else if !self.is_due_in_days() { - let last_review_time = - TimestampSecs(self.original_or_current_due() as i64 - self.interval as i64); - Some(timing.now.elapsed_secs_since(last_review_time) as u32) - } else { + } else if self.is_due_in_days() { self.due_time(timing).map(|due| { (due.adding_secs(-86_400 * self.interval as i64) .elapsed_secs()) as u32 }) + } else { + let last_review_time = TimestampSecs(self.original_or_current_due() as i64); + Some(timing.now.elapsed_secs_since(last_review_time) as u32) } } } From fb2e2bd37adbcfab2d4e2e6f7103c6835c80a7f3 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 9 Aug 2025 16:46:25 +1000 Subject: [PATCH 10/10] Revert "Fix/Retention help button bounds (#4253)" (#4258) This reverts commit 5462d99255c41ed47b7953ca6487594ba7cabd5d. --- ts/lib/components/TitledContainer.svelte | 28 ++++++++++++++---------- ts/routes/graphs/Graph.svelte | 4 ++-- ts/routes/graphs/TrueRetention.svelte | 4 ++-- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/ts/lib/components/TitledContainer.svelte b/ts/lib/components/TitledContainer.svelte index 98983940a..70e4a078c 100644 --- a/ts/lib/components/TitledContainer.svelte +++ b/ts/lib/components/TitledContainer.svelte @@ -12,7 +12,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html export { className as class }; export let title: string; - export let onHelpClick: ((_e: MouseEvent | KeyboardEvent) => void) | null = null; + export let onTitleClick: ((_e: MouseEvent | KeyboardEvent) => void) | null = null;
-

- {title} -

- {#if onHelpClick} -
- -
+

+ {title} +

+ + {:else} +

+ {title} +

{/if} +
+ +
diff --git a/ts/routes/graphs/Graph.svelte b/ts/routes/graphs/Graph.svelte index 1dafed4bb..2c509639e 100644 --- a/ts/routes/graphs/Graph.svelte +++ b/ts/routes/graphs/Graph.svelte @@ -8,7 +8,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // When title is null (default), the graph is inlined, not having TitledContainer wrapper. export let title: string | null = null; export let subtitle: string | null = null; - export let onHelpClick: ((_e: MouseEvent | KeyboardEvent) => void) | null = null; + export let onTitleClick: ((_e: MouseEvent | KeyboardEvent) => void) | null = null; {#if title == null} @@ -19,7 +19,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{:else} - +
{#if subtitle} diff --git a/ts/routes/graphs/TrueRetention.svelte b/ts/routes/graphs/TrueRetention.svelte index 9af2b4d85..6b89ab9b8 100644 --- a/ts/routes/graphs/TrueRetention.svelte +++ b/ts/routes/graphs/TrueRetention.svelte @@ -57,12 +57,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html const title = tr.statisticsTrueRetentionTitle(); const subtitle = tr.statisticsTrueRetentionSubtitle(); - const onHelpClick = () => { + const onTitleClick = () => { openHelpModal(Object.keys(retentionHelp).indexOf("trueRetention")); }; - +