diff --git a/.version b/.version
index ce73bf7c0..8dec45b51 100644
--- a/.version
+++ b/.version
@@ -1 +1 @@
-25.08b1
+25.07.3rc1
diff --git a/Cargo.toml b/Cargo.toml
index db5753893..2ff29cd1a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -141,7 +141,7 @@ walkdir = "2.5.0"
which = "8.0.0"
widestring = "1.1.0"
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"
xz2 = "0.1.7"
zip = { version = "4.1.0", default-features = false, features = ["deflate", "time"] }
diff --git a/build/runner/src/pyenv.rs b/build/runner/src/pyenv.rs
index 9d65626ca..efd58fd91 100644
--- a/build/runner/src/pyenv.rs
+++ b/build/runner/src/pyenv.rs
@@ -36,7 +36,7 @@ pub fn setup_pyenv(args: PyenvArgs) {
// remove UV_* environment variables to avoid interference
for (key, _) in std::env::vars() {
- if key.starts_with("UV_") {
+ if key.starts_with("UV_") || key == "VIRTUAL_ENV" {
command.env_remove(key);
}
}
diff --git a/ftl/core-repo b/ftl/core-repo
index 3d04bcbf7..b90ef6f03 160000
--- a/ftl/core-repo
+++ b/ftl/core-repo
@@ -1 +1 @@
-Subproject commit 3d04bcbf7fefca0007bc9db307409d88210995d8
+Subproject commit b90ef6f03c251eb336029ac7c5f551200d41273f
diff --git a/ftl/qt-repo b/ftl/qt-repo
index c65a9587b..9aa63c335 160000
--- a/ftl/qt-repo
+++ b/ftl/qt-repo
@@ -1 +1 @@
-Subproject commit c65a9587b1f18931986bdf145872e8e4c44c5c82
+Subproject commit 9aa63c335c61b30421d39cf43fd8e3975179059c
diff --git a/pylib/pyproject.toml b/pylib/pyproject.toml
index 23e10077f..fb7422694 100644
--- a/pylib/pyproject.toml
+++ b/pylib/pyproject.toml
@@ -7,7 +7,7 @@ dependencies = [
"decorator",
"markdown",
"orjson",
- "protobuf>=4.21",
+ "protobuf>=6.0,<8.0",
"requests[socks]",
# remove after we update to min python 3.11+
"typing_extensions",
diff --git a/qt/aqt/about.py b/qt/aqt/about.py
index 228d3cfeb..03e989f2c 100644
--- a/qt/aqt/about.py
+++ b/qt/aqt/about.py
@@ -70,10 +70,10 @@ def show(mw: aqt.AnkiQt) -> QDialog:
abouttext += f"
{lede}"
abouttext += f"
{tr.about_anki_is_licensed_under_the_agpl3()}"
abouttext += f"
{tr.about_version(val=version_with_build())}
"
- abouttext += ("Python %s Qt %s PyQt %s
") % (
+ abouttext += ("Python %s Qt %s Chromium %s
") % (
platform.python_version(),
qVersion(),
- PYQT_VERSION_STR,
+ (qWebEngineChromiumVersion() or "").split(".")[0],
)
abouttext += (
without_unicode_isolation(tr.about_visit_website(val=aqt.appWebsite))
@@ -225,6 +225,7 @@ def show(mw: aqt.AnkiQt) -> QDialog:
"Adnane Taghi",
"Anon_0000",
"Bilolbek Normuminov",
+ "Sagiv Marzini",
)
)
diff --git a/qt/aqt/forms/preferences.ui b/qt/aqt/forms/preferences.ui
index 807d4093c..0035e1f42 100644
--- a/qt/aqt/forms/preferences.ui
+++ b/qt/aqt/forms/preferences.ui
@@ -1292,9 +1292,10 @@
daily_backups
weekly_backups
monthly_backups
- tabWidget
syncAnkiHubLogout
syncAnkiHubLogin
+ buttonBox
+ tabWidget
diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py
index bd87ef830..afce6d489 100644
--- a/qt/aqt/preferences.py
+++ b/qt/aqt/preferences.py
@@ -82,11 +82,14 @@ class Preferences(QDialog):
)
group = self.form.preferences_answer_keys
group.setLayout(layout := QFormLayout())
+ tab_widget: QWidget = self.form.url_schemes
for ease, label in ease_labels:
layout.addRow(
label,
line_edit := QLineEdit(self.mw.pm.get_answer_key(ease) or ""),
)
+ QWidget.setTabOrder(tab_widget, line_edit)
+ tab_widget = line_edit
qconnect(
line_edit.textChanged,
functools.partial(self.mw.pm.set_answer_key, ease),
diff --git a/qt/launcher/lin/build.sh b/qt/launcher/lin/build.sh
index 7bd78c27d..f38f6defe 100755
--- a/qt/launcher/lin/build.sh
+++ b/qt/launcher/lin/build.sh
@@ -13,7 +13,8 @@ HOST_ARCH=$(uname -m)
# Define output paths
OUTPUT_DIR="../../../out/launcher"
-LAUNCHER_DIR="$OUTPUT_DIR/anki-linux"
+ANKI_VERSION=$(cat ../../../.version | tr -d '\n')
+LAUNCHER_DIR="$OUTPUT_DIR/anki-launcher-$ANKI_VERSION-linux"
# Clean existing output directory
rm -rf "$LAUNCHER_DIR"
@@ -77,8 +78,8 @@ chmod +x \
chmod -R a+r "$LAUNCHER_DIR"
ZSTD="zstd -c --long -T0 -18"
-TRANSFORM="s%^.%anki-linux%S"
-TARBALL="$OUTPUT_DIR/anki-linux.tar.zst"
+TRANSFORM="s%^.%anki-launcher-$ANKI_VERSION-linux%S"
+TARBALL="$OUTPUT_DIR/anki-launcher-$ANKI_VERSION-linux.tar.zst"
tar -I "$ZSTD" --transform "$TRANSFORM" -cf "$TARBALL" -C "$LAUNCHER_DIR" .
diff --git a/qt/launcher/mac/Info.plist b/qt/launcher/mac/Info.plist
index ac0ab2f09..a48960208 100644
--- a/qt/launcher/mac/Info.plist
+++ b/qt/launcher/mac/Info.plist
@@ -5,7 +5,7 @@
CFBundleDisplayName
Anki
CFBundleShortVersionString
- 1.0
+ ANKI_VERSION
LSMinimumSystemVersion
12
LSApplicationCategoryType
diff --git a/qt/launcher/mac/build.sh b/qt/launcher/mac/build.sh
index 470b5cd25..d521e155b 100755
--- a/qt/launcher/mac/build.sh
+++ b/qt/launcher/mac/build.sh
@@ -31,7 +31,8 @@ lipo -create \
cp "$OUTPUT_DIR/uv" "$APP_LAUNCHER/Contents/MacOS/"
# Copy support files
-cp Info.plist "$APP_LAUNCHER/Contents/"
+ANKI_VERSION=$(cat ../../../.version | tr -d '\n')
+sed "s/ANKI_VERSION/$ANKI_VERSION/g" Info.plist > "$APP_LAUNCHER/Contents/Info.plist"
cp icon/Assets.car "$APP_LAUNCHER/Contents/Resources/"
cp ../pyproject.toml "$APP_LAUNCHER/Contents/Resources/"
cp ../../../.python-version "$APP_LAUNCHER/Contents/Resources/"
diff --git a/qt/launcher/mac/dmg/build.sh b/qt/launcher/mac/dmg/build.sh
index 16b48c06a..7eeba9948 100755
--- a/qt/launcher/mac/dmg/build.sh
+++ b/qt/launcher/mac/dmg/build.sh
@@ -6,7 +6,8 @@ set -e
# base folder with Anki.app in it
output="$1"
dist="$1/tmp"
-dmg_path="$output/Anki.dmg"
+ANKI_VERSION=$(cat ../../../.version | tr -d '\n')
+dmg_path="$output/anki-launcher-$ANKI_VERSION-mac.dmg"
if [ -d "/Volumes/Anki" ]
then
diff --git a/qt/launcher/src/bin/build_win.rs b/qt/launcher/src/bin/build_win.rs
index 96688f190..4c2ca4413 100644
--- a/qt/launcher/src/bin/build_win.rs
+++ b/qt/launcher/src/bin/build_win.rs
@@ -22,6 +22,11 @@ const NSIS_PATH: &str = "C:\\Program Files (x86)\\NSIS\\makensis.exe";
fn main() -> Result<()> {
println!("Building Windows launcher...");
+ // Read version early so it can be used throughout the build process
+ let version = std::fs::read_to_string("../../../.version")?
+ .trim()
+ .to_string();
+
let output_dir = PathBuf::from(OUTPUT_DIR);
let launcher_exe_dir = PathBuf::from(LAUNCHER_EXE_DIR);
let nsis_dir = PathBuf::from(NSIS_DIR);
@@ -31,16 +36,20 @@ fn main() -> Result<()> {
extract_nsis_plugins()?;
copy_files(&output_dir)?;
sign_binaries(&output_dir)?;
- copy_nsis_files(&nsis_dir)?;
+ copy_nsis_files(&nsis_dir, &version)?;
build_uninstaller(&output_dir, &nsis_dir)?;
sign_file(&output_dir.join("uninstall.exe"))?;
generate_install_manifest(&output_dir)?;
build_installer(&output_dir, &nsis_dir)?;
- sign_file(&PathBuf::from("../../../out/launcher_exe/anki-install.exe"))?;
+
+ let installer_filename = format!("anki-launcher-{version}-windows.exe");
+ let installer_path = PathBuf::from("../../../out/launcher_exe").join(&installer_filename);
+
+ sign_file(&installer_path)?;
println!("Build completed successfully!");
println!("Output directory: {}", output_dir.display());
- println!("Installer: ../../../out/launcher_exe/anki-install.exe");
+ println!("Installer: ../../../out/launcher_exe/{installer_filename}");
Ok(())
}
@@ -235,11 +244,13 @@ fn generate_install_manifest(output_dir: &Path) -> Result<()> {
Ok(())
}
-fn copy_nsis_files(nsis_dir: &Path) -> Result<()> {
+fn copy_nsis_files(nsis_dir: &Path, version: &str) -> Result<()> {
println!("Copying NSIS support files...");
- // Copy anki.template.nsi as anki.nsi
- copy_file("anki.template.nsi", nsis_dir.join("anki.nsi"))?;
+ // Copy anki.template.nsi as anki.nsi and substitute version placeholders
+ let template_content = std::fs::read_to_string("anki.template.nsi")?;
+ let substituted_content = template_content.replace("ANKI_VERSION", version);
+ write_file(nsis_dir.join("anki.nsi"), substituted_content)?;
// Copy fileassoc.nsh
copy_file("fileassoc.nsh", nsis_dir.join("fileassoc.nsh"))?;
diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs
index d48adaf49..903cc2b60 100644
--- a/qt/launcher/src/main.rs
+++ b/qt/launcher/src/main.rs
@@ -198,6 +198,7 @@ fn extract_aqt_version(
) -> Option {
let output = Command::new(uv_path)
.current_dir(uv_install_root)
+ .env("VIRTUAL_ENV", uv_install_root.join(".venv"))
.args(["pip", "show", "aqt"])
.output()
.ok()?;
@@ -269,7 +270,7 @@ fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Re
// remove UV_* environment variables to avoid interference
for (key, _) in std::env::vars() {
- if key.starts_with("UV_") {
+ if key.starts_with("UV_") || key == "VIRTUAL_ENV" {
command.env_remove(key);
}
}
diff --git a/qt/launcher/src/platform/windows.rs b/qt/launcher/src/platform/windows.rs
index 3c060a9de..ebdff6261 100644
--- a/qt/launcher/src/platform/windows.rs
+++ b/qt/launcher/src/platform/windows.rs
@@ -8,6 +8,7 @@ use anyhow::Context;
use anyhow::Result;
use widestring::u16cstr;
use windows::core::PCWSTR;
+use windows::Wdk::System::SystemServices::RtlGetVersion;
use windows::Win32::System::Console::AttachConsole;
use windows::Win32::System::Console::GetConsoleWindow;
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::KEY_READ;
use windows::Win32::System::Registry::REG_SZ;
+use windows::Win32::System::SystemInformation::OSVERSIONINFOW;
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::() 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<()> {
unsafe {
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() {
+ // 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
reconnect_stdio_to_console();
return Ok(());
diff --git a/qt/launcher/win/anki.template.nsi b/qt/launcher/win/anki.template.nsi
index 84dedf9c8..36b32a893 100644
--- a/qt/launcher/win/anki.template.nsi
+++ b/qt/launcher/win/anki.template.nsi
@@ -24,7 +24,7 @@ Name "Anki"
Unicode true
; The file to write (relative to nsis directory)
-OutFile "..\launcher_exe\anki-install.exe"
+OutFile "..\launcher_exe\anki-launcher-ANKI_VERSION-windows.exe"
; Non elevated
RequestExecutionLevel user
@@ -214,7 +214,7 @@ Section ""
; Write the uninstall keys for Windows
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "DisplayName" "Anki Launcher"
- WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "DisplayVersion" "1.0.0"
+ WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "DisplayVersion" "ANKI_VERSION"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "UninstallString" '"$INSTDIR\uninstall.exe"'
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /S'
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\Anki" "NoModify" 1
diff --git a/qt/pyproject.toml b/qt/pyproject.toml
index 6a686dde8..1ff34f59d 100644
--- a/qt/pyproject.toml
+++ b/qt/pyproject.toml
@@ -40,8 +40,8 @@ qt67 = [
qt = [
"pyqt6==6.9.1",
"pyqt6-qt6==6.9.1",
- "pyqt6-webengine==6.9.0",
- "pyqt6-webengine-qt6==6.9.1",
+ "pyqt6-webengine==6.8.0",
+ "pyqt6-webengine-qt6==6.8.2",
"pyqt6_sip==13.10.2",
]
qt68 = [
diff --git a/ts/editor/ClozeButtons.svelte b/ts/editor/ClozeButtons.svelte
index e9faae540..dfe4fb7c3 100644
--- a/ts/editor/ClozeButtons.svelte
+++ b/ts/editor/ClozeButtons.svelte
@@ -4,7 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->