mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
Inject Upgrade/Downgrade menu item and audio support into older versions
This commit is contained in:
parent
a587343f29
commit
f5073b402a
4 changed files with 214 additions and 5 deletions
135
qt/launcher/addon/__init__.py
Normal file
135
qt/launcher/addon/__init__.py
Normal file
|
@ -0,0 +1,135 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import aqt.sound
|
||||
from anki.utils import pointVersion
|
||||
from aqt import mw
|
||||
from aqt.qt import QAction
|
||||
from aqt.utils import askUser, is_mac, is_win, showInfo
|
||||
|
||||
|
||||
def _anki_launcher_path() -> str | None:
|
||||
return os.getenv("ANKI_LAUNCHER")
|
||||
|
||||
|
||||
def have_launcher() -> bool:
|
||||
return _anki_launcher_path() is not None
|
||||
|
||||
|
||||
def update_and_restart() -> None:
|
||||
from aqt import mw
|
||||
|
||||
launcher = _anki_launcher_path()
|
||||
assert launcher
|
||||
|
||||
_trigger_launcher_run()
|
||||
|
||||
with contextlib.suppress(ResourceWarning):
|
||||
env = os.environ.copy()
|
||||
creationflags = 0
|
||||
if sys.platform == "win32":
|
||||
creationflags = (
|
||||
subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
|
||||
)
|
||||
subprocess.Popen(
|
||||
[launcher],
|
||||
start_new_session=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
env=env,
|
||||
creationflags=creationflags,
|
||||
)
|
||||
|
||||
mw.app.quit()
|
||||
|
||||
|
||||
def _trigger_launcher_run() -> None:
|
||||
"""Bump the mtime on pyproject.toml in the local data directory to trigger an update on next run."""
|
||||
try:
|
||||
# Get the local data directory equivalent to Rust's dirs::data_local_dir()
|
||||
if is_win:
|
||||
from aqt.winpaths import get_local_appdata
|
||||
|
||||
data_dir = Path(get_local_appdata())
|
||||
elif is_mac:
|
||||
data_dir = Path.home() / "Library" / "Application Support"
|
||||
else: # Linux
|
||||
data_dir = Path(
|
||||
os.environ.get("XDG_DATA_HOME", Path.home() / ".local" / "share")
|
||||
)
|
||||
|
||||
pyproject_path = data_dir / "AnkiProgramFiles" / "pyproject.toml"
|
||||
|
||||
if pyproject_path.exists():
|
||||
# Touch the file to update its mtime
|
||||
pyproject_path.touch()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
|
||||
def confirm_then_upgrade():
|
||||
if not askUser("Change to a different Anki version?"):
|
||||
return
|
||||
update_and_restart()
|
||||
|
||||
|
||||
# return modified command array that points to bundled command, and return
|
||||
# required environment
|
||||
def _packagedCmd(cmd: list[str]) -> tuple[Any, dict[str, str]]:
|
||||
cmd = cmd[:]
|
||||
env = os.environ.copy()
|
||||
# keep LD_LIBRARY_PATH when in snap environment
|
||||
if "LD_LIBRARY_PATH" in env and "SNAP" not in env:
|
||||
del env["LD_LIBRARY_PATH"]
|
||||
|
||||
# Try to find binary in anki-audio package for Windows/Mac
|
||||
if is_win or is_mac:
|
||||
try:
|
||||
import anki_audio
|
||||
|
||||
audio_pkg_path = Path(anki_audio.__file__).parent
|
||||
if is_win:
|
||||
packaged_path = audio_pkg_path / (cmd[0] + ".exe")
|
||||
else: # is_mac
|
||||
packaged_path = audio_pkg_path / cmd[0]
|
||||
|
||||
if packaged_path.exists():
|
||||
cmd[0] = str(packaged_path)
|
||||
return cmd, env
|
||||
except ImportError:
|
||||
# anki-audio not available, fall back to old behavior
|
||||
pass
|
||||
|
||||
packaged_path = Path(sys.prefix) / cmd[0]
|
||||
if packaged_path.exists():
|
||||
cmd[0] = str(packaged_path)
|
||||
|
||||
return cmd, env
|
||||
|
||||
|
||||
def setup():
|
||||
if pointVersion() >= 250600:
|
||||
return
|
||||
if not have_launcher():
|
||||
return
|
||||
|
||||
# Add action to tools menu
|
||||
action = QAction("Upgrade/Downgrade", mw)
|
||||
action.triggered.connect(confirm_then_upgrade)
|
||||
mw.form.menuTools.addAction(action)
|
||||
|
||||
# Monkey-patch audio tools to use anki-audio
|
||||
if is_win or is_mac:
|
||||
aqt.sound._packagedCmd = _packagedCmd
|
||||
|
||||
|
||||
setup()
|
6
qt/launcher/addon/manifest.json
Normal file
6
qt/launcher/addon/manifest.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "Anki Launcher",
|
||||
"package": "anki-launcher",
|
||||
"min_point_version": 50,
|
||||
"max_point_version": 250600
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
use std::io::stdin;
|
||||
use std::io::stdout;
|
||||
use std::io::Write;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::process::Command;
|
||||
use std::time::SystemTime;
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
@ -17,7 +18,7 @@ use anki_io::read_file;
|
|||
use anki_io::remove_file;
|
||||
use anki_io::write_file;
|
||||
use anki_io::ToUtf8Path;
|
||||
use anki_process::CommandExt;
|
||||
use anki_process::CommandExt as AnkiCommandExt;
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
|
||||
|
@ -133,7 +134,7 @@ fn run() -> Result<()> {
|
|||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let cmd = build_python_command(&state.uv_install_root, &[])?;
|
||||
platform::mac::prepare_for_launch_after_update(cmd)?;
|
||||
platform::mac::prepare_for_launch_after_update(cmd, &uv_install_root)?;
|
||||
}
|
||||
|
||||
if cfg!(unix) && !cfg!(target_os = "macos") {
|
||||
|
@ -179,7 +180,7 @@ fn main_menu_loop(state: &State) -> Result<()> {
|
|||
choice @ (MainMenuChoice::Latest | MainMenuChoice::Version(_)) => {
|
||||
// For other choices, update project files and sync
|
||||
update_pyproject_for_version(
|
||||
choice,
|
||||
choice.clone(),
|
||||
state.dist_pyproject_path.clone(),
|
||||
state.user_pyproject_path.clone(),
|
||||
state.dist_python_version_path.clone(),
|
||||
|
@ -215,7 +216,10 @@ fn main_menu_loop(state: &State) -> Result<()> {
|
|||
|
||||
match command.ensure_success() {
|
||||
Ok(_) => {
|
||||
// Sync succeeded, break out of loop
|
||||
// Sync succeeded
|
||||
if matches!(&choice, MainMenuChoice::Version(VersionKind::PyOxidizer(_))) {
|
||||
inject_helper_addon(&state.uv_install_root)?;
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
|
@ -351,6 +355,7 @@ fn update_pyproject_for_version(
|
|||
&format!(
|
||||
concat!(
|
||||
"aqt[qt6]=={}\",\n",
|
||||
" \"anki-audio==0.1.0; sys.platform == 'win32' or sys.platform == 'darwin'\",\n",
|
||||
" \"pyqt6==6.6.1\",\n",
|
||||
" \"pyqt6-qt6==6.6.2\",\n",
|
||||
" \"pyqt6-webengine==6.6.0\",\n",
|
||||
|
@ -427,6 +432,54 @@ fn parse_version_kind(version: &str) -> Option<VersionKind> {
|
|||
}
|
||||
}
|
||||
|
||||
fn inject_helper_addon(_uv_install_root: &std::path::Path) -> Result<()> {
|
||||
let addons21_path = get_anki_addons21_path()?;
|
||||
|
||||
if !addons21_path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let addon_folder = addons21_path.join("anki-launcher");
|
||||
|
||||
// Remove existing anki-launcher folder if it exists
|
||||
if addon_folder.exists() {
|
||||
anki_io::remove_dir_all(&addon_folder)?;
|
||||
}
|
||||
|
||||
// Create the anki-launcher folder
|
||||
create_dir_all(&addon_folder)?;
|
||||
|
||||
// Write the embedded files
|
||||
let init_py_content = include_str!("../addon/__init__.py");
|
||||
let manifest_json_content = include_str!("../addon/manifest.json");
|
||||
|
||||
write_file(addon_folder.join("__init__.py"), init_py_content)?;
|
||||
write_file(addon_folder.join("manifest.json"), manifest_json_content)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_anki_addons21_path() -> Result<std::path::PathBuf> {
|
||||
let anki_base_path = if cfg!(target_os = "windows") {
|
||||
// Windows: %APPDATA%\Anki2
|
||||
dirs::config_dir()
|
||||
.context("Unable to determine config directory")?
|
||||
.join("Anki2")
|
||||
} else if cfg!(target_os = "macos") {
|
||||
// macOS: ~/Library/Application Support/Anki2
|
||||
dirs::data_dir()
|
||||
.context("Unable to determine data directory")?
|
||||
.join("Anki2")
|
||||
} else {
|
||||
// Linux: ~/.local/share/Anki2
|
||||
dirs::data_dir()
|
||||
.context("Unable to determine data directory")?
|
||||
.join("Anki2")
|
||||
};
|
||||
|
||||
Ok(anki_base_path.join("addons21"))
|
||||
}
|
||||
|
||||
fn build_python_command(uv_install_root: &std::path::Path, args: &[String]) -> Result<Command> {
|
||||
let python_exe = if cfg!(target_os = "windows") {
|
||||
let show_console = std::env::var("ANKI_CONSOLE").is_ok();
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
@ -14,7 +15,7 @@ use anki_process::CommandExt as AnkiCommandExt;
|
|||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
|
||||
pub fn prepare_for_launch_after_update(mut cmd: Command) -> Result<()> {
|
||||
pub fn prepare_for_launch_after_update(mut cmd: Command, root: &Path) -> Result<()> {
|
||||
// Pre-validate by running --version to trigger any Gatekeeper checks
|
||||
print!("\n\x1B[1mThis may take a few minutes. Please wait\x1B[0m");
|
||||
io::stdout().flush().unwrap();
|
||||
|
@ -37,6 +38,20 @@ pub fn prepare_for_launch_after_update(mut cmd: Command) -> Result<()> {
|
|||
.stderr(std::process::Stdio::null())
|
||||
.ensure_success();
|
||||
|
||||
if cfg!(target_os = "macos") {
|
||||
// older Anki versions had a short mpv timeout and didn't support
|
||||
// ANKI_FIRST_RUN, so we need to ensure mpv passes Gatekeeper
|
||||
// validation prior to launch
|
||||
let mpv_path = root.join(".venv/lib/python3.9/site-packages/anki_audio/mpv");
|
||||
if mpv_path.exists() {
|
||||
let _ = Command::new(&mpv_path)
|
||||
.arg("--version")
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.ensure_success();
|
||||
}
|
||||
}
|
||||
|
||||
// Stop progress indicator
|
||||
running.store(false, Ordering::Relaxed);
|
||||
progress_thread.join().unwrap();
|
||||
|
|
Loading…
Reference in a new issue