Merge branch 'main' into editor-3830

This commit is contained in:
Abdo 2025-07-21 21:24:50 +03:00
commit c91f943f29
34 changed files with 260 additions and 134 deletions

View file

@ -1 +1 @@
25.08b1 25.07.3

View file

@ -234,6 +234,8 @@ Emmanuel Ferdman <https://github.com/emmanuel-ferdman>
Sunong2008 <https://github.com/Sunrongguo2008> Sunong2008 <https://github.com/Sunrongguo2008>
Marvin Kopf <marvinkopf@outlook.com> Marvin Kopf <marvinkopf@outlook.com>
Kevin Nakamura <grinkers@grinkers.net> Kevin Nakamura <grinkers@grinkers.net>
Bradley Szoke <bradleyszoke@gmail.com>
jcznk <https://github.com/jcznk>
******************** ********************

View file

@ -141,7 +141,7 @@ walkdir = "2.5.0"
which = "8.0.0" which = "8.0.0"
widestring = "1.1.0" widestring = "1.1.0"
winapi = { version = "0.3", features = ["wincon", "winreg"] } winapi = { version = "0.3", features = ["wincon", "winreg"] }
windows = { version = "0.61.3", features = ["Media_SpeechSynthesis", "Media_Core", "Foundation_Collections", "Storage_Streams", "Win32_System_Console", "Win32_System_Registry", "Win32_Foundation", "Win32_UI_Shell"] } windows = { version = "0.61.3", features = ["Media_SpeechSynthesis", "Media_Core", "Foundation_Collections", "Storage_Streams", "Win32_System_Console", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_Foundation", "Win32_UI_Shell", "Wdk_System_SystemServices"] }
wiremock = "0.6.3" wiremock = "0.6.3"
xz2 = "0.1.7" xz2 = "0.1.7"
zip = { version = "4.1.0", default-features = false, features = ["deflate", "time"] } zip = { version = "4.1.0", default-features = false, features = ["deflate", "time"] }

View file

@ -32,10 +32,19 @@ 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_") || key == "VIRTUAL_ENV" {
command.env_remove(key);
}
}
run_command( run_command(
Command::new(args.uv_bin) command
.env("UV_PROJECT_ENVIRONMENT", args.pyenv_folder.clone()) .env("UV_PROJECT_ENVIRONMENT", args.pyenv_folder.clone())
.args(["sync", "--locked"]) .args(["sync", "--locked", "--no-config"])
.args(args.extra_args), .args(args.extra_args),
); );

@ -1 +1 @@
Subproject commit a9216499ba1fb1538cfd740c698adaaa3410fd4b Subproject commit b90ef6f03c251eb336029ac7c5f551200d41273f

View file

@ -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-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-legacy-timezone-handling = Legacy timezone handling (buggy, but required for AnkiDroid <= 2.14)
preferences-default-search-text = Default search text 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 = Theme
preferences-theme-follow-system = Follow System preferences-theme-follow-system = Follow System
preferences-theme-light = Light preferences-theme-light = Light

View file

@ -80,7 +80,7 @@ statistics-reviews =
# This fragment of the tooltip in the FSRS simulation # This fragment of the tooltip in the FSRS simulation
# diagram (Deck options -> FSRS) shows the total number of # diagram (Deck options -> FSRS) shows the total number of
# cards that can be recalled or retrieved on a specific date. # 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-title = Today
statistics-today-again-count = Again count: statistics-today-again-count = Again count:
statistics-today-type-counts = Learn: { $learnCount }, Review: { $reviewCount }, Relearn: { $relearnCount }, Filtered: { $filteredCount } statistics-today-type-counts = Learn: { $learnCount }, Review: { $reviewCount }, Relearn: { $relearnCount }, Filtered: { $filteredCount }
@ -99,9 +99,9 @@ statistics-counts-relearning-cards = Relearning
statistics-counts-title = Card Counts statistics-counts-title = Card Counts
statistics-counts-separate-suspended-buried-cards = Separate suspended/buried cards 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 ## 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. ## 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. ## 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 ## N.B. Stats cards may be very small on mobile devices and when the Stats
## window is certain sizes. ## 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-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-range = Range
statistics-true-retention-pass = Pass statistics-true-retention-pass = Pass
statistics-true-retention-fail = Fail statistics-true-retention-fail = Fail

@ -1 +1 @@
Subproject commit a1134ab59d3d23468af2968741aa1f21d16ff308 Subproject commit 9aa63c335c61b30421d39cf43fd8e3975179059c

View file

@ -404,6 +404,7 @@ message SimulateFsrsReviewRequest {
repeated float easy_days_percentages = 10; repeated float easy_days_percentages = 10;
deck_config.DeckConfig.Config.ReviewCardOrder review_order = 11; deck_config.DeckConfig.Config.ReviewCardOrder review_order = 11;
optional uint32 suspend_after_lapse_count = 12; optional uint32 suspend_after_lapse_count = 12;
float historical_retention = 13;
} }
message SimulateFsrsReviewResponse { message SimulateFsrsReviewResponse {

View file

@ -7,7 +7,7 @@ dependencies = [
"decorator", "decorator",
"markdown", "markdown",
"orjson", "orjson",
"protobuf>=4.21", "protobuf>=6.0,<8.0",
"requests[socks]", "requests[socks]",
# remove after we update to min python 3.11+ # remove after we update to min python 3.11+
"typing_extensions", "typing_extensions",

View file

@ -70,10 +70,10 @@ def show(mw: aqt.AnkiQt) -> QDialog:
abouttext += f"<p>{lede}" abouttext += f"<p>{lede}"
abouttext += f"<p>{tr.about_anki_is_licensed_under_the_agpl3()}" abouttext += f"<p>{tr.about_anki_is_licensed_under_the_agpl3()}"
abouttext += f"<p>{tr.about_version(val=version_with_build())}<br>" abouttext += f"<p>{tr.about_version(val=version_with_build())}<br>"
abouttext += ("Python %s Qt %s PyQt %s<br>") % ( abouttext += ("Python %s Qt %s Chromium %s<br>") % (
platform.python_version(), platform.python_version(),
qVersion(), qVersion(),
PYQT_VERSION_STR, (qWebEngineChromiumVersion() or "").split(".")[0],
) )
abouttext += ( abouttext += (
without_unicode_isolation(tr.about_visit_website(val=aqt.appWebsite)) without_unicode_isolation(tr.about_visit_website(val=aqt.appWebsite))
@ -225,6 +225,7 @@ def show(mw: aqt.AnkiQt) -> QDialog:
"Adnane Taghi", "Adnane Taghi",
"Anon_0000", "Anon_0000",
"Bilolbek Normuminov", "Bilolbek Normuminov",
"Sagiv Marzini",
) )
) )

View file

@ -1292,9 +1292,10 @@
<tabstop>daily_backups</tabstop> <tabstop>daily_backups</tabstop>
<tabstop>weekly_backups</tabstop> <tabstop>weekly_backups</tabstop>
<tabstop>monthly_backups</tabstop> <tabstop>monthly_backups</tabstop>
<tabstop>tabWidget</tabstop>
<tabstop>syncAnkiHubLogout</tabstop> <tabstop>syncAnkiHubLogout</tabstop>
<tabstop>syncAnkiHubLogin</tabstop> <tabstop>syncAnkiHubLogin</tabstop>
<tabstop>buttonBox</tabstop>
<tabstop>tabWidget</tabstop>
</tabstops> </tabstops>
<resources/> <resources/>
<connections> <connections>

View file

@ -124,17 +124,14 @@ def launcher_executable() -> str | None:
def trigger_launcher_run() -> 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: try:
root = launcher_root() root = launcher_root()
if not root: if not root:
return return
pyproject_path = Path(root) / "pyproject.toml" trigger_path = Path(root) / ".want-launcher"
trigger_path.touch()
if pyproject_path.exists():
# Touch the file to update its mtime
pyproject_path.touch()
except Exception as e: except Exception as e:
print(e) print(e)

View file

@ -82,11 +82,14 @@ class Preferences(QDialog):
) )
group = self.form.preferences_answer_keys group = self.form.preferences_answer_keys
group.setLayout(layout := QFormLayout()) group.setLayout(layout := QFormLayout())
tab_widget: QWidget = self.form.url_schemes
for ease, label in ease_labels: for ease, label in ease_labels:
layout.addRow( layout.addRow(
label, label,
line_edit := QLineEdit(self.mw.pm.get_answer_key(ease) or ""), line_edit := QLineEdit(self.mw.pm.get_answer_key(ease) or ""),
) )
QWidget.setTabOrder(tab_widget, line_edit)
tab_widget = line_edit
qconnect( qconnect(
line_edit.textChanged, line_edit.textChanged,
functools.partial(self.mw.pm.set_answer_key, ease), functools.partial(self.mw.pm.set_answer_key, ease),

View file

@ -177,9 +177,13 @@ class CustomStyles:
QPushButton:default {{ QPushButton:default {{
border: 1px solid {tm.var(colors.BORDER_FOCUS)}; border: 1px solid {tm.var(colors.BORDER_FOCUS)};
}} }}
QPushButton {{
margin: 1px;
}}
QPushButton:focus {{ QPushButton:focus {{
border: 2px solid {tm.var(colors.BORDER_FOCUS)}; border: 2px solid {tm.var(colors.BORDER_FOCUS)};
outline: none; outline: none;
margin: 0px;
}} }}
QPushButton:hover, QPushButton:hover,
QTabBar::tab:hover, QTabBar::tab:hover,

View file

@ -69,17 +69,14 @@ def add_python_requirements(reqs: list[str]) -> tuple[bool, str]:
def trigger_launcher_run() -> 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: try:
root = launcher_root() root = launcher_root()
if not root: if not root:
return return
pyproject_path = Path(root) / "pyproject.toml" trigger_path = Path(root) / ".want-launcher"
trigger_path.touch()
if pyproject_path.exists():
# Touch the file to update its mtime
pyproject_path.touch()
except Exception as e: except Exception as e:
print(e) print(e)

View file

@ -13,7 +13,8 @@ HOST_ARCH=$(uname -m)
# Define output paths # Define output paths
OUTPUT_DIR="../../../out/launcher" 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 # Clean existing output directory
rm -rf "$LAUNCHER_DIR" rm -rf "$LAUNCHER_DIR"
@ -77,8 +78,8 @@ chmod +x \
chmod -R a+r "$LAUNCHER_DIR" chmod -R a+r "$LAUNCHER_DIR"
ZSTD="zstd -c --long -T0 -18" ZSTD="zstd -c --long -T0 -18"
TRANSFORM="s%^.%anki-linux%S" TRANSFORM="s%^.%anki-launcher-$ANKI_VERSION-linux%S"
TARBALL="$OUTPUT_DIR/anki-linux.tar.zst" TARBALL="$OUTPUT_DIR/anki-launcher-$ANKI_VERSION-linux.tar.zst"
tar -I "$ZSTD" --transform "$TRANSFORM" -cf "$TARBALL" -C "$LAUNCHER_DIR" . tar -I "$ZSTD" --transform "$TRANSFORM" -cf "$TARBALL" -C "$LAUNCHER_DIR" .

View file

@ -5,7 +5,7 @@
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Anki</string> <string>Anki</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0</string> <string>ANKI_VERSION</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>12</string> <string>12</string>
<key>LSApplicationCategoryType</key> <key>LSApplicationCategoryType</key>

View file

@ -31,7 +31,8 @@ lipo -create \
cp "$OUTPUT_DIR/uv" "$APP_LAUNCHER/Contents/MacOS/" cp "$OUTPUT_DIR/uv" "$APP_LAUNCHER/Contents/MacOS/"
# Copy support files # 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 icon/Assets.car "$APP_LAUNCHER/Contents/Resources/"
cp ../pyproject.toml "$APP_LAUNCHER/Contents/Resources/" cp ../pyproject.toml "$APP_LAUNCHER/Contents/Resources/"
cp ../../../.python-version "$APP_LAUNCHER/Contents/Resources/" cp ../../../.python-version "$APP_LAUNCHER/Contents/Resources/"

View file

@ -6,7 +6,8 @@ set -e
# base folder with Anki.app in it # base folder with Anki.app in it
output="$1" output="$1"
dist="$1/tmp" 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" ] if [ -d "/Volumes/Anki" ]
then then

View file

@ -22,6 +22,11 @@ const NSIS_PATH: &str = "C:\\Program Files (x86)\\NSIS\\makensis.exe";
fn main() -> Result<()> { fn main() -> Result<()> {
println!("Building Windows launcher..."); 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 output_dir = PathBuf::from(OUTPUT_DIR);
let launcher_exe_dir = PathBuf::from(LAUNCHER_EXE_DIR); let launcher_exe_dir = PathBuf::from(LAUNCHER_EXE_DIR);
let nsis_dir = PathBuf::from(NSIS_DIR); let nsis_dir = PathBuf::from(NSIS_DIR);
@ -31,16 +36,20 @@ fn main() -> Result<()> {
extract_nsis_plugins()?; extract_nsis_plugins()?;
copy_files(&output_dir)?; copy_files(&output_dir)?;
sign_binaries(&output_dir)?; sign_binaries(&output_dir)?;
copy_nsis_files(&nsis_dir)?; copy_nsis_files(&nsis_dir, &version)?;
build_uninstaller(&output_dir, &nsis_dir)?; build_uninstaller(&output_dir, &nsis_dir)?;
sign_file(&output_dir.join("uninstall.exe"))?; sign_file(&output_dir.join("uninstall.exe"))?;
generate_install_manifest(&output_dir)?; generate_install_manifest(&output_dir)?;
build_installer(&output_dir, &nsis_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!("Build completed successfully!");
println!("Output directory: {}", output_dir.display()); println!("Output directory: {}", output_dir.display());
println!("Installer: ../../../out/launcher_exe/anki-install.exe"); println!("Installer: ../../../out/launcher_exe/{installer_filename}");
Ok(()) Ok(())
} }
@ -235,11 +244,13 @@ fn generate_install_manifest(output_dir: &Path) -> Result<()> {
Ok(()) Ok(())
} }
fn copy_nsis_files(nsis_dir: &Path) -> Result<()> { fn copy_nsis_files(nsis_dir: &Path, version: &str) -> Result<()> {
println!("Copying NSIS support files..."); println!("Copying NSIS support files...");
// Copy anki.template.nsi as anki.nsi // Copy anki.template.nsi as anki.nsi and substitute version placeholders
copy_file("anki.template.nsi", nsis_dir.join("anki.nsi"))?; 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 fileassoc.nsh
copy_file("fileassoc.nsh", nsis_dir.join("fileassoc.nsh"))?; copy_file("fileassoc.nsh", nsis_dir.join("fileassoc.nsh"))?;

View file

@ -46,6 +46,8 @@ struct State {
dist_python_version_path: std::path::PathBuf, dist_python_version_path: std::path::PathBuf,
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,
pyproject_modified_by_user: bool,
previous_version: Option<String>, previous_version: Option<String>,
resources_dir: std::path::PathBuf, resources_dir: std::path::PathBuf,
} }
@ -70,7 +72,6 @@ pub enum MainMenuChoice {
ToggleBetas, ToggleBetas,
ToggleCache, ToggleCache,
Uninstall, Uninstall,
Quit,
} }
fn main() { fn main() {
@ -106,6 +107,8 @@ fn run() -> Result<()> {
dist_python_version_path: resources_dir.join(".python-version"), dist_python_version_path: resources_dir.join(".python-version"),
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"),
pyproject_modified_by_user: false, // calculated later
previous_version: None, previous_version: None,
resources_dir, resources_dir,
}; };
@ -125,15 +128,15 @@ fn run() -> Result<()> {
&state.user_python_version_path, &state.user_python_version_path,
)?; )?;
let pyproject_has_changed = !state.sync_complete_marker.exists() || { let launcher_requested = state.launcher_trigger_file.exists();
let pyproject_toml_time = modified_time(&state.user_pyproject_path)?;
let sync_complete_time = modified_time(&state.sync_complete_marker)?;
Ok::<bool, anyhow::Error>(pyproject_toml_time > sync_complete_time)
}
.unwrap_or(true);
if !pyproject_has_changed { // Calculate whether user has custom edits that need syncing
// If venv is already up to date, launch Anki normally 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<String> = std::env::args().skip(1).collect(); let args: Vec<String> = std::env::args().skip(1).collect();
let cmd = build_python_command(&state, &args)?; let cmd = build_python_command(&state, &args)?;
launch_anki_normally(cmd)?; launch_anki_normally(cmd)?;
@ -143,6 +146,11 @@ fn run() -> Result<()> {
// If we weren't in a terminal, respawn ourselves in one // If we weren't in a terminal, respawn ourselves in one
ensure_terminal_shown()?; 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 print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top
println!("\x1B[1mAnki Launcher\x1B[0m\n"); println!("\x1B[1mAnki Launcher\x1B[0m\n");
@ -190,6 +198,7 @@ fn extract_aqt_version(
) -> Option<String> { ) -> Option<String> {
let output = Command::new(uv_path) let output = Command::new(uv_path)
.current_dir(uv_install_root) .current_dir(uv_install_root)
.env("VIRTUAL_ENV", uv_install_root.join(".venv"))
.args(["pip", "show", "aqt"]) .args(["pip", "show", "aqt"])
.output() .output()
.ok()?; .ok()?;
@ -255,29 +264,21 @@ fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Re
None None
}; };
// `uv sync` sometimes does not pull in Python automatically // Prepare to sync the venv
// This might be system/platform specific and/or a uv bug.
let mut command = Command::new(&state.uv_path); let mut command = Command::new(&state.uv_path);
command command.current_dir(&state.uv_install_root);
.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(["python", "install", "--managed-python"]);
// Add python version if .python-version file exists // remove UV_* environment variables to avoid interference
if let Some(version) = &python_version_trimmed { for (key, _) in std::env::vars() {
command.args([version]); if key.starts_with("UV_") || key == "VIRTUAL_ENV" {
command.env_remove(key);
}
} }
command.ensure_success().context("Python install failed")?;
// Sync the venv
let mut command = Command::new(&state.uv_path);
command command
.current_dir(&state.uv_install_root)
.env("UV_CACHE_DIR", &state.uv_cache_dir) .env("UV_CACHE_DIR", &state.uv_cache_dir)
.env("UV_PYTHON_INSTALL_DIR", &state.uv_python_install_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 // Add python version if .python-version file exists
if let Some(version) = &python_version_trimmed { if let Some(version) = &python_version_trimmed {
@ -321,9 +322,11 @@ fn main_menu_loop(state: &State) -> Result<()> {
let menu_choice = get_main_menu_choice(state)?; let menu_choice = get_main_menu_choice(state)?;
match menu_choice { match menu_choice {
MainMenuChoice::Quit => std::process::exit(0),
MainMenuChoice::KeepExisting => { 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; break;
} }
MainMenuChoice::ToggleBetas => { MainMenuChoice::ToggleBetas => {
@ -380,14 +383,28 @@ fn write_sync_marker(sync_complete_marker: &std::path::Path) -> Result<()> {
Ok(()) 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<MainMenuChoice> { fn get_main_menu_choice(state: &State) -> Result<MainMenuChoice> {
loop { loop {
println!("1) Latest Anki (press Enter)"); println!("1) Latest Anki (press Enter)");
println!("2) Choose a version"); println!("2) Choose a version");
if let Some(current_version) = &state.current_version { if let Some(current_version) = &state.current_version {
let normalized_current = normalize_version(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 let Some(prev_version) = &state.previous_version {
if state.current_version.as_ref() != Some(prev_version) { if state.current_version.as_ref() != Some(prev_version) {
let normalized_prev = normalize_version(prev_version); let normalized_prev = normalize_version(prev_version);
@ -408,7 +425,6 @@ fn get_main_menu_choice(state: &State) -> Result<MainMenuChoice> {
); );
println!(); println!();
println!("7) Uninstall"); println!("7) Uninstall");
println!("8) Quit");
print!("> "); print!("> ");
let _ = stdout().flush(); let _ = stdout().flush();
@ -448,7 +464,6 @@ 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::Uninstall,
"8" => MainMenuChoice::Quit,
_ => { _ => {
println!("Invalid input. Please try again."); println!("Invalid input. Please try again.");
continue; continue;
@ -706,9 +721,6 @@ fn update_pyproject_for_version(menu_choice: MainMenuChoice, state: &State) -> R
MainMenuChoice::Version(version_kind) => { MainMenuChoice::Version(version_kind) => {
apply_version_kind(&version_kind, state)?; apply_version_kind(&version_kind, state)?;
} }
MainMenuChoice::Quit => {
std::process::exit(0);
}
} }
Ok(()) Ok(())
} }

View file

@ -8,6 +8,7 @@ use anyhow::Context;
use anyhow::Result; use anyhow::Result;
use widestring::u16cstr; use widestring::u16cstr;
use windows::core::PCWSTR; use windows::core::PCWSTR;
use windows::Wdk::System::SystemServices::RtlGetVersion;
use windows::Win32::System::Console::AttachConsole; use windows::Win32::System::Console::AttachConsole;
use windows::Win32::System::Console::GetConsoleWindow; use windows::Win32::System::Console::GetConsoleWindow;
use windows::Win32::System::Console::ATTACH_PARENT_PROCESS; use windows::Win32::System::Console::ATTACH_PARENT_PROCESS;
@ -18,8 +19,25 @@ use windows::Win32::System::Registry::HKEY;
use windows::Win32::System::Registry::HKEY_CURRENT_USER; use windows::Win32::System::Registry::HKEY_CURRENT_USER;
use windows::Win32::System::Registry::KEY_READ; use windows::Win32::System::Registry::KEY_READ;
use windows::Win32::System::Registry::REG_SZ; use windows::Win32::System::Registry::REG_SZ;
use windows::Win32::System::SystemInformation::OSVERSIONINFOW;
use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID; use windows::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID;
/// Returns true if running on Windows 10 (not Windows 11)
fn is_windows_10() -> bool {
unsafe {
let mut info = OSVERSIONINFOW {
dwOSVersionInfoSize: std::mem::size_of::<OSVERSIONINFOW>() as u32,
..Default::default()
};
if RtlGetVersion(&mut info).is_ok() {
// Windows 10 has build numbers < 22000, Windows 11 >= 22000
info.dwBuildNumber < 22000 && info.dwMajorVersion == 10
} else {
false
}
}
}
pub fn ensure_terminal_shown() -> Result<()> { pub fn ensure_terminal_shown() -> Result<()> {
unsafe { unsafe {
if !GetConsoleWindow().is_invalid() { if !GetConsoleWindow().is_invalid() {
@ -29,6 +47,14 @@ pub fn ensure_terminal_shown() -> Result<()> {
} }
if std::env::var("ANKI_IMPLICIT_CONSOLE").is_ok() && attach_to_parent_console() { if std::env::var("ANKI_IMPLICIT_CONSOLE").is_ok() && attach_to_parent_console() {
// This black magic triggers Windows to switch to the new
// ANSI-supporting console host, which is usually only available
// when the app is built with the console subsystem.
// Only needed on Windows 10, not Windows 11.
if is_windows_10() {
let _ = Command::new("cmd").args(["/C", ""]).status();
}
// Successfully attached to parent console // Successfully attached to parent console
reconnect_stdio_to_console(); reconnect_stdio_to_console();
return Ok(()); return Ok(());

View file

@ -24,7 +24,7 @@ Name "Anki"
Unicode true Unicode true
; The file to write (relative to nsis directory) ; 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 ; Non elevated
RequestExecutionLevel user RequestExecutionLevel user
@ -214,7 +214,7 @@ Section ""
; Write the uninstall keys for Windows ; 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" "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" "UninstallString" '"$INSTDIR\uninstall.exe"'
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /S' WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /S'
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "NoModify" 1 WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "NoModify" 1

View file

@ -40,8 +40,8 @@ qt67 = [
qt = [ qt = [
"pyqt6==6.9.1", "pyqt6==6.9.1",
"pyqt6-qt6==6.9.1", "pyqt6-qt6==6.9.1",
"pyqt6-webengine==6.9.0", "pyqt6-webengine==6.8.0",
"pyqt6-webengine-qt6==6.9.1", "pyqt6-webengine-qt6==6.8.2",
"pyqt6_sip==13.10.2", "pyqt6_sip==13.10.2",
] ]
qt68 = [ qt68 = [

View file

@ -377,6 +377,7 @@ pub(crate) fn fsrs_item_for_memory_state(
Ok(None) Ok(None)
} }
} else { } else {
// no revlogs (new card or caused by ignore_revlogs_before or deleted revlogs)
Ok(None) Ok(None)
} }
} }

View file

@ -10,11 +10,14 @@ use fsrs::simulate;
use fsrs::PostSchedulingFn; use fsrs::PostSchedulingFn;
use fsrs::ReviewPriorityFn; use fsrs::ReviewPriorityFn;
use fsrs::SimulatorConfig; use fsrs::SimulatorConfig;
use fsrs::FSRS;
use itertools::Itertools; use itertools::Itertools;
use rand::rngs::StdRng; use rand::rngs::StdRng;
use rand::Rng; use rand::Rng;
use crate::card::CardQueue; use crate::card::CardQueue;
use crate::card::CardType;
use crate::card::FsrsMemoryState;
use crate::prelude::*; use crate::prelude::*;
use crate::scheduler::states::fuzz::constrained_fuzz_bounds; use crate::scheduler::states::fuzz::constrained_fuzz_bounds;
use crate::scheduler::states::load_balancer::calculate_easy_days_modifiers; use crate::scheduler::states::load_balancer::calculate_easy_days_modifiers;
@ -129,7 +132,7 @@ impl Collection {
fn is_included_card(c: &Card) -> bool { fn is_included_card(c: &Card) -> bool {
c.queue != CardQueue::Suspended c.queue != CardQueue::Suspended
&& c.queue != CardQueue::PreviewRepeat && c.queue != CardQueue::PreviewRepeat
&& c.queue != CardQueue::New && c.ctype != CardType::New
} }
// calculate any missing memory state // calculate any missing memory state
for c in &mut cards { for c in &mut cards {
@ -143,13 +146,29 @@ impl Collection {
let days_elapsed = self.timing_today().unwrap().days_elapsed as i32; let days_elapsed = self.timing_today().unwrap().days_elapsed as i32;
let new_cards = cards let new_cards = cards
.iter() .iter()
.filter(|c| c.memory_state.is_none() || c.queue == CardQueue::New) .filter(|c| c.ctype == CardType::New && c.queue != CardQueue::Suspended)
.count() .count()
+ req.deck_size as usize; + req.deck_size as usize;
let fsrs = FSRS::new(Some(&req.params))?;
let mut converted_cards = cards let mut converted_cards = cards
.into_iter() .into_iter()
.filter(is_included_card) .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(); .collect_vec();
let introduced_today_count = self let introduced_today_count = self
.search_cards(&format!("{} introduced:1", &req.search), SortMode::NoOrder)? .search_cards(&format!("{} introduced:1", &req.search), SortMode::NoOrder)?
@ -251,39 +270,34 @@ impl Collection {
} }
impl Card { impl Card {
fn convert(card: Card, days_elapsed: i32) -> Option<fsrs::Card> { fn convert(card: Card, days_elapsed: i32, memory_state: FsrsMemoryState) -> Option<fsrs::Card> {
match card.memory_state { match card.queue {
Some(state) => match card.queue { CardQueue::DayLearn | CardQueue::Review => {
CardQueue::DayLearn | CardQueue::Review => { let due = card.original_or_current_due();
let due = card.original_or_current_due(); let relative_due = due - days_elapsed;
let relative_due = due - days_elapsed; let last_date = (relative_due - card.interval as i32).min(0) as f32;
let last_date = (relative_due - card.interval as i32).min(0) as f32; Some(fsrs::Card {
Some(fsrs::Card { id: card.id.0,
id: card.id.0, difficulty: memory_state.difficulty,
difficulty: state.difficulty, stability: memory_state.stability,
stability: state.stability, last_date,
last_date, due: relative_due as f32,
due: relative_due as f32, interval: card.interval as f32,
interval: card.interval as f32, lapses: card.lapses,
lapses: card.lapses, })
}) }
} CardQueue::New => None,
CardQueue::New => None, CardQueue::Learn | CardQueue::SchedBuried | CardQueue::UserBuried => Some(fsrs::Card {
CardQueue::Learn | CardQueue::SchedBuried | CardQueue::UserBuried => { id: card.id.0,
Some(fsrs::Card { difficulty: memory_state.difficulty,
id: card.id.0, stability: memory_state.stability,
difficulty: state.difficulty, last_date: 0.0,
stability: state.stability, due: 0.0,
last_date: 0.0, interval: card.interval as f32,
due: 0.0, lapses: card.lapses,
interval: card.interval as f32, }),
lapses: card.lapses, CardQueue::PreviewRepeat => None,
}) CardQueue::Suspended => None,
}
CardQueue::PreviewRepeat => None,
CardQueue::Suspended => None,
},
None => None,
} }
} }
} }

View file

@ -13,3 +13,20 @@ export function isApplePlatform(): boolean {
export function isDesktop(): boolean { export function isDesktop(): boolean {
return !(/iphone|ipad|ipod|android/i.test(window.navigator.userAgent)); return !(/iphone|ipad|ipod|android/i.test(window.navigator.userAgent));
} }
export function chromiumVersion(): number | null {
const userAgent = window.navigator.userAgent;
// Check if it's a Chromium-based browser (Chrome, Edge, Opera, etc.)
// but exclude Safari which also contains "Chrome" in its user agent
if (userAgent.includes("Safari") && !userAgent.includes("Chrome")) {
return null; // Safari
}
const chromeMatch = userAgent.match(/Chrome\/(\d+)/);
if (chromeMatch) {
return parseInt(chromeMatch[1], 10);
}
return null; // Not a Chromium-based browser
}

View file

@ -85,6 +85,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
} }
.easy-days-settings input[type="range"] { .easy-days-settings input[type="range"] {
width: 100%; width: 100%;
cursor: pointer;
} }
.day { .day {

View file

@ -95,6 +95,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
newCardsIgnoreReviewLimit: $newCardsIgnoreReviewLimit, newCardsIgnoreReviewLimit: $newCardsIgnoreReviewLimit,
easyDaysPercentages: $config.easyDaysPercentages, easyDaysPercentages: $config.easyDaysPercentages,
reviewOrder: $config.reviewOrder, reviewOrder: $config.reviewOrder,
historicalRetention: $config.historicalRetention,
}); });
const DESIRED_RETENTION_LOW_THRESHOLD = 0.8; const DESIRED_RETENTION_LOW_THRESHOLD = 0.8;

View file

@ -4,7 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import * as tr from "@generated/ftl"; import * as tr from "@generated/ftl";
import { isApplePlatform } from "@tslib/platform"; import { chromiumVersion, isApplePlatform } from "@tslib/platform";
import { getPlatformString } from "@tslib/shortcuts"; import { getPlatformString } from "@tslib/shortcuts";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import { get } from "svelte/store"; import { get } from "svelte/store";
@ -22,9 +22,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
const { focusedInput, fields } = noteEditorContext.get(); const { focusedInput, fields } = noteEditorContext.get();
// Workaround for Cmd+Option+Shift+C not working on macOS. The keyup approach works // Workaround for Cmd+Option+Shift+C not working on macOS on older Chromium
// on Linux as well, but fails on Windows. // versions.
const event = isApplePlatform() ? "keyup" : "keydown"; const chromiumVer = chromiumVersion();
const event =
isApplePlatform() && chromiumVer != null && chromiumVer <= 112
? "keyup"
: "keydown";
const clozePattern = /\{\{c(\d+)::/gu; const clozePattern = /\{\{c(\d+)::/gu;
function getCurrentHighestCloze(increment: boolean): number { function getCurrentHighestCloze(increment: boolean): number {

View file

@ -47,6 +47,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
white-space: nowrap; white-space: nowrap;
padding: 15px; padding: 15px;
border-radius: 5px; border-radius: 5px;
font-family: inherit;
font-size: 15px; font-size: 15px;
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;

View file

@ -7,7 +7,7 @@ import { ModuleName, setupI18n } from "@tslib/i18n";
import { optimumPixelSizeForCanvas } from "./canvas-scale"; import { optimumPixelSizeForCanvas } from "./canvas-scale";
import { Shape } from "./shapes"; import { Shape } from "./shapes";
import { Ellipse, extractShapesFromRenderedClozes, Polygon, Rectangle, Text } 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"; import type { Size } from "./types";
export type DrawShapesData = { export type DrawShapesData = {
@ -217,7 +217,7 @@ function drawShapes(
context, context,
size, size,
shape, shape,
fill: shape.fill ?? properties.inActiveShapeColor, fill: shape.fill !== SHAPE_MASK_COLOR ? shape.fill : properties.inActiveShapeColor,
stroke: properties.inActiveBorder.color, stroke: properties.inActiveBorder.color,
strokeWidth: properties.inActiveBorder.width, strokeWidth: properties.inActiveBorder.width,
}); });
@ -437,7 +437,7 @@ function getShapeProperties(): ShapeProperties {
activeShapeColor: activeShapeColor ? activeShapeColor : "#ff8e8e", activeShapeColor: activeShapeColor ? activeShapeColor : "#ff8e8e",
inActiveShapeColor: inActiveShapeColor inActiveShapeColor: inActiveShapeColor
? inActiveShapeColor ? inActiveShapeColor
: "#ffeba2", : SHAPE_MASK_COLOR,
highlightShapeColor: highlightShapeColor highlightShapeColor: highlightShapeColor
? highlightShapeColor ? highlightShapeColor
: "#ff8e8e00", : "#ff8e8e00",

48
uv.lock
View file

@ -66,7 +66,7 @@ requires-dist = [
{ name = "distro", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" }, { name = "distro", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" },
{ name = "markdown" }, { name = "markdown" },
{ name = "orjson" }, { name = "orjson" },
{ name = "protobuf", specifier = ">=4.21" }, { name = "protobuf", specifier = ">=6.0,<8.0" },
{ name = "requests", extras = ["socks"] }, { name = "requests", extras = ["socks"] },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
@ -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", 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.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.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.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' 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.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 = "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 = "requests" },
{ name = "send2trash" }, { name = "send2trash" },
@ -186,8 +186,8 @@ qt = [
{ name = "pyqt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" } }, { 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-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-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", version = "6.8.0", source = { registry = "https://pypi.org/simple" } },
{ name = "pyqt6-webengine-qt6", version = "6.9.1", source = { registry = "https://pypi.org/simple" } }, { name = "pyqt6-webengine-qt6", version = "6.8.2", source = { registry = "https://pypi.org/simple" } },
] ]
qt66 = [ qt66 = [
{ name = "pyqt6", version = "6.6.1", source = { registry = "https://pypi.org/simple" } }, { 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 == 'qt67'", specifier = "==13.10.2" },
{ name = "pyqt6-sip", marker = "extra == 'qt68'", specifier = "==13.10.2" }, { name = "pyqt6-sip", marker = "extra == 'qt68'", specifier = "==13.10.2" },
{ name = "pyqt6-webengine", specifier = ">=6.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 == 'qt66'", specifier = "==6.6.0" },
{ name = "pyqt6-webengine", marker = "extra == 'qt67'", specifier = "==6.7.0" }, { name = "pyqt6-webengine", marker = "extra == 'qt67'", specifier = "==6.7.0" },
{ name = "pyqt6-webengine", marker = "extra == 'qt68'", specifier = "==6.8.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 == 'qt66'", specifier = "==6.6.2" },
{ name = "pyqt6-webengine-qt6", marker = "extra == 'qt67'", specifier = "==6.7.3" }, { name = "pyqt6-webengine-qt6", marker = "extra == 'qt67'", specifier = "==6.7.3" },
{ name = "pyqt6-webengine-qt6", marker = "extra == 'qt68'", specifier = "==6.8.1" }, { name = "pyqt6-webengine-qt6", marker = "extra == 'qt68'", specifier = "==6.8.1" },
@ -552,7 +552,7 @@ name = "importlib-metadata"
version = "8.7.0" version = "8.7.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ 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" } 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 = [ wheels = [
@ -1034,8 +1034,8 @@ resolution-markers = [
"python_full_version < '3.10'", "python_full_version < '3.10'",
] ]
dependencies = [ dependencies = [
{ name = "pyqt6-qt6", version = "6.9.1", 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" } }, { 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" } 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 = [ wheels = [
@ -1246,8 +1246,10 @@ resolution-markers = [
] ]
dependencies = [ 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", 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.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" } 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 = [ wheels = [
@ -1269,9 +1271,9 @@ resolution-markers = [
"python_full_version < '3.10'", "python_full_version < '3.10'",
] ]
dependencies = [ dependencies = [
{ name = "pyqt6", 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" } }, { 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" } }, { 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" } 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 = [ wheels = [
@ -1338,6 +1340,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" }, { 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]] [[package]]
name = "pyqt6-webengine-qt6" name = "pyqt6-webengine-qt6"
version = "6.9.1" version = "6.9.1"