// Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use anki_proto::generic; use anki_proto::launcher::get_langs_response; use anki_proto::launcher::get_mirrors_response; use anki_proto::launcher::state::Kind as StateProtoKind; use anki_proto::launcher::ChooseVersionRequest; use anki_proto::launcher::ChooseVersionResponse; use anki_proto::launcher::GetLangsResponse; use anki_proto::launcher::GetMirrorsResponse; use anki_proto::launcher::I18nResourcesRequest; use anki_proto::launcher::Mirror; use anki_proto::launcher::NormalState as NormalStateProto; use anki_proto::launcher::Options; use anki_proto::launcher::State as StateProto; use anki_proto::launcher::ZoomWebviewRequest; use anyhow::anyhow; use anyhow::Context; use anyhow::Result; use strum::IntoEnumIterator; use tauri::AppHandle; use tauri::Emitter; use tauri::Manager; use tauri::Runtime; use tauri::WebviewWindow; use tauri_plugin_os::locale; use crate::lang::get_tr; use crate::lang::setup_i18n; use crate::lang::LANGS; use crate::lang::LANGS_DEFAULT_REGION; use crate::lang::LANGS_WITH_REGIONS; use crate::state::ExistingVersions; use crate::state::State; use crate::state::Versions; use crate::uv; pub async fn i18n_resources( app: AppHandle, _window: WebviewWindow, input: I18nResourcesRequest, ) -> Result { let tr = get_tr(&app)?; serde_json::to_vec(&tr.resources_for_js(&input.modules)) .with_context(|| "failed to serialise i18n resources") .map(Into::into) } pub async fn get_langs( _app: AppHandle, _window: WebviewWindow, ) -> Result { let langs = LANGS .into_iter() .map(|(locale, name)| get_langs_response::Pair { name: name.to_string(), locale: locale.to_string(), }) .collect(); let user_locale = locale() .and_then(|l| { if LANGS.contains_key(&l) { Some(l) } else { LANGS_DEFAULT_REGION .get(l.split('-').next().unwrap()) .or_else(|| LANGS_DEFAULT_REGION.get("en")) .map(ToString::to_string) } }) .unwrap(); Ok(GetLangsResponse { user_locale, langs }) } pub async fn set_lang( app: AppHandle, _window: WebviewWindow, input: generic::String, ) -> Result<()> { // python's lang_to_disk_lang let input = input.val; let input = if LANGS_WITH_REGIONS.contains(input.as_str()) { input } else { input.split('-').next().unwrap().to_owned() }; setup_i18n(&app, &[&*input]); Ok(()) } pub async fn get_state( app: AppHandle, _window: WebviewWindow, ) -> Result { let state = app.state::(); let kind = match &*state { State::LaunchAnki(_) => unreachable!(), State::OsUnsupported(e) => StateProtoKind::OsUnsupported(format!("{e:?}")), State::UnknownError(e) => StateProtoKind::UnknownError(format!("{e:?}")), State::Uninstall(_) => StateProtoKind::Uninstall(()), State::Normal(normal) => StateProtoKind::Normal(NormalStateProto { options: Some((&normal.initial_options).into()), }), }; Ok(StateProto { kind: Some(kind) }) } pub async fn get_mirrors( app: AppHandle, _window: WebviewWindow, ) -> Result { let tr = get_tr(&app)?; Ok(GetMirrorsResponse { mirrors: Mirror::iter() .map(|mirror| get_mirrors_response::Pair { mirror: mirror.into(), name: match mirror { Mirror::Disabled => tr.launcher_mirror_no_mirror(), Mirror::China => tr.launcher_mirror_china(), } .into(), }) .collect(), }) } pub async fn get_options( app: AppHandle, _window: WebviewWindow, ) -> Result { let state = app.state::(); let options = (&state.normal()?.initial_options).into(); Ok(options) } pub async fn get_available_versions( app: AppHandle, _window: WebviewWindow, ) -> Result { let state = app.state::(); let state = state.normal()?; let mut rx = state.available_versions.clone().unwrap(); rx.changed().await.unwrap(); let x = rx.borrow(); match x.as_ref().unwrap() { Ok(versions) => Ok(versions.clone()), // TODO: errors are passed as strings to the web Err(e) => Err(anyhow!("{e:?}")), } } pub async fn get_existing_versions( app: AppHandle, _window: WebviewWindow, ) -> Result { let state = app.state::(); let state = state.normal()?; let mut rx = state.current_versions.clone().unwrap(); rx.changed().await.unwrap(); let x = rx.borrow(); match x.as_ref().unwrap() { Ok(versions) => Ok(versions.clone()), Err(e) => Err(anyhow!("{e:?}")), } } pub async fn choose_version( app: AppHandle, _window: WebviewWindow, input: ChooseVersionRequest, ) -> Result { let state = app.state::(); let state = state.normal()?; let paths = state.paths.clone(); tauri::async_runtime::spawn_blocking(move || { if let Some(options) = input.options { uv::set_allow_betas(&paths, options.allow_betas)?; uv::set_cache_enabled(&paths, options.download_caching)?; uv::set_mirror(&paths, options.mirror != Mirror::Disabled as i32)?; } let version = input.version; let on_pty_data = move |data| { let _ = app.emit("pty-data", data); }; if !input.keep_existing || paths.pyproject_modified_by_user { // install or resync uv::handle_version_install_or_update( &paths, &version, input.keep_existing, input.current.as_deref(), on_pty_data, )?; } let warming_up = uv::post_install(&paths)?; Ok(ChooseVersionResponse { version, warming_up, }) }) .await? } pub async fn launch_anki(app: AppHandle, _window: WebviewWindow) -> Result<()> { app.state::().paths().and_then(uv::launch_anki) } pub async fn exit(app: AppHandle, window: WebviewWindow) -> Result<()> { tauri::async_runtime::spawn_blocking(move || { let _ = window.destroy(); // can't be called from the main thread app.exit(0); }); Ok(()) } /// NOTE: [zoomHotkeysEnabled](https://v2.tauri.app/reference/config/#zoomhotkeysenabled) exists /// but the polyfill it uses on linux doesn't allow regular scrolling pub async fn zoom_webview( _app: AppHandle, window: WebviewWindow, input: ZoomWebviewRequest, ) -> Result<()> { let factor = input.scale_factor.into(); // NOTE: not supported on windows let _ = window.set_zoom(factor); Ok(()) } pub async fn window_ready(_app: AppHandle, window: WebviewWindow) -> Result<()> { window .show() .with_context(|| format!("could not show window: {}", window.label())) }