Merge branch 'main' into patch-2

This commit is contained in:
sorata 2025-08-10 04:34:24 +05:30 committed by GitHub
commit 9384938ed3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 142 additions and 62 deletions

View file

@ -1 +1 @@
25.08b4 25.08b5

@ -1 +1 @@
Subproject commit a0d0e232d296ccf5750e39df2442b133267b222b Subproject commit a599715d3c27ff2eb895c749f3534ab73d83dad1

View file

@ -517,7 +517,6 @@ deck-config-smooth-graph = Smooth graph
deck-config-suspend-leeches = Suspend leeches deck-config-suspend-leeches = Suspend leeches
deck-config-save-options-to-preset = Save Changes to Preset 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-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 # 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 # to show the total number of cards that can be recalled or retrieved on a
# specific date. # 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. ## 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 = deck-config-a-100-day-interval =
{ $days -> { $days ->
[one] A 100 day interval will become { $days } day. [one] A 100 day interval will become { $days } day.

@ -1 +1 @@
Subproject commit 9639c96fe5862459aa1ff4e599079cac72a9fd7c Subproject commit bb4207f3b8e9a7c428db282d12c75b850be532f3

View file

@ -46,6 +46,7 @@ struct State {
uv_lock_path: std::path::PathBuf, uv_lock_path: std::path::PathBuf,
sync_complete_marker: std::path::PathBuf, sync_complete_marker: std::path::PathBuf,
launcher_trigger_file: std::path::PathBuf, launcher_trigger_file: std::path::PathBuf,
mirror_path: std::path::PathBuf,
pyproject_modified_by_user: bool, pyproject_modified_by_user: bool,
previous_version: Option<String>, previous_version: Option<String>,
resources_dir: std::path::PathBuf, resources_dir: std::path::PathBuf,
@ -71,6 +72,7 @@ pub enum MainMenuChoice {
Version(VersionKind), Version(VersionKind),
ToggleBetas, ToggleBetas,
ToggleCache, ToggleCache,
DownloadMirror,
Uninstall, Uninstall,
} }
@ -108,6 +110,7 @@ fn run() -> Result<()> {
uv_lock_path: uv_install_root.join("uv.lock"), uv_lock_path: uv_install_root.join("uv.lock"),
sync_complete_marker: uv_install_root.join(".sync_complete"), sync_complete_marker: uv_install_root.join(".sync_complete"),
launcher_trigger_file: uv_install_root.join(".want-launcher"), launcher_trigger_file: uv_install_root.join(".want-launcher"),
mirror_path: uv_install_root.join("mirror"),
pyproject_modified_by_user: false, // calculated later pyproject_modified_by_user: false, // calculated later
previous_version: None, previous_version: None,
resources_dir, resources_dir,
@ -155,12 +158,7 @@ fn run() -> Result<()> {
check_versions(&mut state); 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 marker file to indicate we've completed the sync process
write_sync_marker(&state)?; write_sync_marker(&state)?;
@ -379,6 +377,11 @@ fn main_menu_loop(state: &State) -> Result<()> {
println!(); println!();
continue; continue;
} }
MainMenuChoice::DownloadMirror => {
show_mirror_submenu(state)?;
println!();
continue;
}
MainMenuChoice::Uninstall => { MainMenuChoice::Uninstall => {
if handle_uninstall(state)? { if handle_uninstall(state)? {
std::process::exit(0); std::process::exit(0);
@ -443,8 +446,13 @@ fn get_main_menu_choice(state: &State) -> Result<MainMenuChoice> {
"6) Cache downloads: {}", "6) Cache downloads: {}",
if cache_enabled { "on" } else { "off" } 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!();
println!("7) Uninstall"); println!("8) Uninstall");
print!("> "); print!("> ");
let _ = stdout().flush(); let _ = stdout().flush();
@ -483,7 +491,8 @@ fn get_main_menu_choice(state: &State) -> Result<MainMenuChoice> {
} }
"5" => MainMenuChoice::ToggleBetas, "5" => MainMenuChoice::ToggleBetas,
"6" => MainMenuChoice::ToggleCache, "6" => MainMenuChoice::ToggleCache,
"7" => MainMenuChoice::Uninstall, "7" => MainMenuChoice::DownloadMirror,
"8" => MainMenuChoice::Uninstall,
_ => { _ => {
println!("Invalid input. Please try again."); println!("Invalid input. Please try again.");
continue; continue;
@ -652,7 +661,7 @@ fn fetch_versions(state: &State) -> Result<Vec<String>> {
let mut cmd = Command::new(&state.uv_path); let mut cmd = Command::new(&state.uv_path);
cmd.current_dir(&state.uv_install_root) cmd.current_dir(&state.uv_install_root)
.args(["run", "--no-project", "--no-config", "--managed-python"]) .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 = read_file(&state.dist_python_version_path)?;
let python_version_str = let python_version_str =
@ -716,7 +725,15 @@ fn apply_version_kind(version_kind: &VersionKind, state: &State) -> Result<()> {
&format!("anki-release=={version}\",\n \"anki=={version}\",\n \"aqt=={version}"), &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 // Update .python-version based on version kind
match version_kind { match version_kind {
@ -750,6 +767,9 @@ fn update_pyproject_for_version(menu_choice: MainMenuChoice, state: &State) -> R
MainMenuChoice::ToggleCache => { MainMenuChoice::ToggleCache => {
unreachable!(); unreachable!();
} }
MainMenuChoice::DownloadMirror => {
unreachable!();
}
MainMenuChoice::Uninstall => { MainMenuChoice::Uninstall => {
unreachable!(); unreachable!();
} }
@ -939,6 +959,70 @@ fn build_python_command(state: &State, args: &[String]) -> Result<Command> {
Ok(cmd) Ok(cmd)
} }
fn is_mirror_enabled(state: &State) -> bool {
state.mirror_path.exists()
}
fn get_mirror_urls(state: &State) -> Result<Option<(String, String)>> {
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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -3,9 +3,9 @@
import json import json
import sys import sys
import urllib.request
import pip_system_certs.wrapt_requests import pip_system_certs.wrapt_requests
import requests
pip_system_certs.wrapt_requests.inject_truststore() pip_system_certs.wrapt_requests.inject_truststore()
@ -15,8 +15,9 @@ def main():
url = "https://pypi.org/pypi/aqt/json" url = "https://pypi.org/pypi/aqt/json"
try: try:
with urllib.request.urlopen(url, timeout=30) as response: response = requests.get(url, timeout=30)
data = json.loads(response.read().decode("utf-8")) response.raise_for_status()
data = response.json()
releases = data.get("releases", {}) releases = data.get("releases", {})
# Create list of (version, upload_time) tuples # Create list of (version, upload_time) tuples

View file

@ -105,7 +105,7 @@ impl Card {
/// Returns true if the card has a due date in terms of days. /// Returns true if the card has a due date in terms of days.
fn is_due_in_days(&self) -> bool { 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) || matches!(self.queue, CardQueue::DayLearn | CardQueue::Review)
|| (self.ctype == CardType::Review && self.is_undue_queue()) || (self.ctype == CardType::Review && self.is_undue_queue())
} }
@ -132,15 +132,14 @@ impl Card {
pub(crate) fn seconds_since_last_review(&self, timing: &SchedTimingToday) -> Option<u32> { pub(crate) fn seconds_since_last_review(&self, timing: &SchedTimingToday) -> Option<u32> {
if let Some(last_review_time) = self.last_review_time { if let Some(last_review_time) = self.last_review_time {
Some(timing.now.elapsed_secs_since(last_review_time) as u32) Some(timing.now.elapsed_secs_since(last_review_time) as u32)
} else if !self.is_due_in_days() { } 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 {
self.due_time(timing).map(|due| { self.due_time(timing).map(|due| {
(due.adding_secs(-86_400 * self.interval as i64) (due.adding_secs(-86_400 * self.interval as i64)
.elapsed_secs()) as u32 .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)
} }
} }
} }

View file

@ -44,8 +44,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import Warning from "./Warning.svelte"; import Warning from "./Warning.svelte";
import type { ComputeRetentionProgress } from "@generated/anki/collection_pb"; import type { ComputeRetentionProgress } from "@generated/anki/collection_pb";
import Modal from "bootstrap/js/dist/modal"; 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 state: DeckOptionsState;
export let simulateFsrsRequest: SimulateFsrsReviewRequest; export let simulateFsrsRequest: SimulateFsrsReviewRequest;
@ -373,23 +371,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{tr.deckConfigDesiredRetention()} {tr.deckConfigDesiredRetention()}
</SettingTitle> </SettingTitle>
</SpinBoxFloatRow> </SpinBoxFloatRow>
{:else}
<Row --cols={13}>
<Col --col-size={7} breakpoint="xs">
<SettingTitle
on:click={() => openHelpModal("desiredRetention")}
>
{tr.deckConfigDesiredRetention()}
</SettingTitle>
</Col>
<Col --col-size={6} breakpoint="xs">
<input
type="text"
disabled
value={tr.deckConfigPlottedOnXAxis()}
/>
</Col>
</Row>
{/if} {/if}
<SpinBoxRow <SpinBoxRow

View file

@ -55,7 +55,10 @@
width: 100%; width: 100%;
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
&:has(li:nth-child(3)) {
justify-content: space-between; justify-content: space-between;
}
justify-content: space-around;
padding-inline: 0; padding-inline: 0;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
list-style: none; list-style: none;

View file

@ -74,6 +74,13 @@ export function renderWorkloadChart(
: n.toString(); : n.toString();
}; };
const formatter = new Intl.NumberFormat(undefined, {
style: "percent",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
const xTickFormat = (n: number) => formatter.format(n / 100);
const formatY: (value: number) => string = ({ const formatY: (value: number) => string = ({
[SimulateWorkloadSubgraph.ratio]: (value: number) => [SimulateWorkloadSubgraph.ratio]: (value: number) =>
tr.deckConfigFsrsSimulatorRatioTooltip({ time: timeSpan(value) }), tr.deckConfigFsrsSimulatorRatioTooltip({ time: timeSpan(value) }),
@ -85,7 +92,7 @@ export function renderWorkloadChart(
})[subgraph]; })[subgraph];
function formatX(dr: number) { function formatX(dr: number) {
return `Desired Retention: ${dr}%<br>`; return `${tr.deckConfigDesiredRetention()}: ${xTickFormat(dr)}<br>`;
} }
return _renderSimulationChart( return _renderSimulationChart(
@ -93,10 +100,11 @@ export function renderWorkloadChart(
bounds, bounds,
subgraph_data, subgraph_data,
x, x,
yTickFormat,
formatY, formatY,
formatX, formatX,
(_e: MouseEvent, _d: number) => undefined, (_e: MouseEvent, _d: number) => undefined,
yTickFormat,
xTickFormat,
); );
} }
@ -169,10 +177,11 @@ export function renderSimulationChart(
bounds, bounds,
subgraph_data, subgraph_data,
x, x,
yTickFormat,
formatY, formatY,
formatX, formatX,
legendMouseMove, legendMouseMove,
yTickFormat,
undefined,
); );
} }
@ -181,10 +190,11 @@ function _renderSimulationChart<T extends { x: any; y: any; label: number }>(
bounds: GraphBounds, bounds: GraphBounds,
subgraph_data: T[], subgraph_data: T[],
x: any, x: any,
yTickFormat: (n: number) => string,
formatY: (n: T["y"]) => string, formatY: (n: T["y"]) => string,
formatX: (n: T["x"]) => string, formatX: (n: T["x"]) => string,
legendMouseMove: (e: MouseEvent, d: number) => void, legendMouseMove: (e: MouseEvent, d: number) => void,
yTickFormat?: (n: number) => string,
xTickFormat?: (n: number) => string,
): TableDatum[] { ): TableDatum[] {
const svg = select(svgElem); const svg = select(svgElem);
svg.selectAll(".lines").remove(); svg.selectAll(".lines").remove();
@ -198,7 +208,9 @@ function _renderSimulationChart<T extends { x: any; y: any; label: number }>(
const trans = svg.transition().duration(600) as any; const trans = svg.transition().duration(600) as any;
svg.select<SVGGElement>(".x-ticks") svg.select<SVGGElement>(".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"); .attr("direction", "ltr");
// y scale // y scale