use crate::api::Result; use crate::api::instance::InstanceLink; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use theseus::data::ModLoader; use theseus::install::{ ImportPlanRequest, InstallJobSnapshot, InstallModpackPreview, InstallPostInstallEdit, }; use theseus::pack::import::ImportLauncherType; use theseus::pack::install_from::CreatePackLocation; use uuid::Uuid; pub fn init() -> tauri::plugin::TauriPlugin { tauri::plugin::Builder::new("install") .invoke_handler(tauri::generate_handler![ install_get_modpack_preview, install_create_instance, install_create_modpack_instance, install_import_instance, install_start_import_plan, install_cancel_import_plan, install_duplicate_instance, install_existing_instance, install_pack_to_existing_instance, install_job_list, install_job_get, install_job_retry, install_job_repair_cache_and_retry, install_job_resume, install_job_skip_missing_content, install_job_missing_files, install_job_scan_missing_files, install_job_retry_missing_file, install_job_import_missing_file, install_job_cancel, install_job_dismiss, install_job_support_details, download_job_list, download_job_get, download_job_retry, download_job_resume, download_job_cancel, download_job_delete, download_history_clear, download_job_support_details, ]) .build() } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct InstallCreateInstanceRequest { pub name: String, pub game_version: String, pub loader: ModLoader, pub loader_version: Option, #[serde(default)] pub adjuncts: Vec, pub icon_path: Option, pub link: Option, #[serde(default)] pub game_dir_override: Option, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct InstallPostInstallEditRequest { pub name: Option, #[serde( default, skip_serializing_if = "Option::is_none", with = "serde_with::rust::double_option" )] pub icon_path: Option>, pub link: Option, } impl InstallPostInstallEditRequest { fn into_core(self) -> Result { Ok(InstallPostInstallEdit { name: self.name, icon_path: self.icon_path, link: self.link.map(|link| link.into_core()).transpose()?, }) } } #[tauri::command] pub async fn install_get_modpack_preview( location: CreatePackLocation, ) -> Result { Ok(theseus::pack::install_from::get_instance_from_pack(location).await?) } #[tauri::command] pub async fn install_create_instance( request: InstallCreateInstanceRequest, ) -> Result { Ok(theseus::install::create_instance_with_adjuncts( request.name.trim().to_string(), request.game_version, request.loader, request.loader_version, request.adjuncts, request.icon_path, match request.link { Some(link) => link.into_core()?, None => theseus::data::InstanceLink::Unmanaged, }, request.game_dir_override, ) .await?) } #[tauri::command] pub async fn install_create_modpack_instance( location: CreatePackLocation, post_install_edit: Option, ) -> Result { Ok(theseus::install::create_modpack_instance( location, post_install_edit.map(|edit| edit.into_core()).transpose()?, ) .await?) } #[tauri::command] pub async fn install_import_instance( launcher_type: ImportLauncherType, base_path: PathBuf, instance_folder: String, instance_path: Option, symlink: bool, game_version: Option, loader: Option, loader_version: Option, game_dir_override: Option, ) -> Result { tracing::debug!( "install_import_instance called: launcher_type={launcher_type:?} base_path={} instance_folder={} instance_path={:?} symlink={symlink} game_version={game_version:?} loader={loader:?} loader_version={loader_version:?} game_dir_override={game_dir_override:?}", base_path.display(), instance_folder, instance_path, ); // Extracted launcher archives live in a temporary folder that is removed // after the import; a symlink into it would dangle immediately. if symlink && base_path .starts_with(std::env::temp_dir().join("axolotl-launcher-import")) { return Err(theseus::Error::from(theseus::ErrorKind::InputError( "Symbolic-link import is unavailable for extracted archives: \ the temporary folder is deleted after the import completes. \ Choose copy instead." .to_string(), )) .into()); } Ok(theseus::install::import_instance_with_plan( launcher_type, base_path, instance_folder, instance_path, symlink, game_version, loader, loader_version, game_dir_override, ) .await?) } #[tauri::command] pub async fn install_start_import_plan( request: ImportPlanRequest, ) -> Result { Ok(theseus::install::start_import_plan(request)?) } #[tauri::command] pub async fn install_cancel_import_plan(request_id: String) -> Result<()> { Ok(theseus::install::cancel_import_plan(&request_id).await?) } #[tauri::command] pub async fn install_duplicate_instance( source_instance_id: String, ) -> Result { Ok(theseus::install::duplicate_instance(source_instance_id).await?) } #[tauri::command] pub async fn install_existing_instance( instance_id: String, force: bool, ) -> Result { Ok(theseus::install::install_existing_instance(instance_id, force).await?) } #[tauri::command] pub async fn install_pack_to_existing_instance( instance_id: String, location: CreatePackLocation, post_install_edit: Option, ) -> Result { Ok(theseus::install::install_pack_to_existing_instance( instance_id, location, post_install_edit.map(|edit| edit.into_core()).transpose()?, ) .await?) } #[tauri::command] pub async fn install_job_list( include_finished: bool, ) -> Result> { Ok(theseus::install::list_jobs(include_finished).await?) } #[tauri::command] pub async fn install_job_get(job_id: Uuid) -> Result { Ok(theseus::install::get_job(job_id).await?) } #[tauri::command] pub async fn install_job_retry(job_id: Uuid) -> Result { Ok(theseus::install::retry_job(job_id).await?) } #[tauri::command] pub async fn install_job_repair_cache_and_retry( job_id: Uuid, ) -> Result { Ok(theseus::install::repair_cache_and_retry_job(job_id).await?) } #[tauri::command] pub async fn install_job_resume(job_id: Uuid) -> Result { Ok(theseus::install::resume_job(job_id).await?) } #[tauri::command] pub async fn install_job_skip_missing_content( job_id: Uuid, ) -> Result { Ok(theseus::install::skip_missing_content_and_resume_job(job_id).await?) } #[tauri::command] pub async fn install_job_missing_files( job_id: Uuid, ) -> Result { Ok(theseus::install::list_missing_modpack_files(job_id).await?) } #[tauri::command] pub async fn install_job_scan_missing_files( job_id: Uuid, scan_directory: Option, ) -> Result { Ok( theseus::install::scan_missing_modpack_files(job_id, scan_directory) .await?, ) } #[tauri::command] pub async fn install_job_retry_missing_file( job_id: Uuid, item_id: String, ) -> Result { Ok(theseus::install::retry_missing_modpack_file(job_id, item_id).await?) } #[tauri::command] pub async fn install_job_import_missing_file( job_id: Uuid, item_id: String, selected_file_path: PathBuf, ) -> Result { Ok(theseus::install::import_missing_modpack_file( job_id, item_id, selected_file_path, ) .await?) } #[tauri::command] pub async fn install_job_cancel(job_id: Uuid) -> Result { Ok(theseus::install::cancel_job(job_id).await?) } #[tauri::command] pub async fn install_job_dismiss(job_id: Uuid) -> Result<()> { Ok(theseus::install::dismiss_job(job_id).await?) } #[tauri::command] pub async fn install_job_support_details(job_id: String) -> Result { let Ok(uuid) = Uuid::parse_str(&job_id) else { // Synthetic, frontend-tracked jobs (e.g. server downloads) use non-UUID // identifiers and do not expose backend diagnostic details. return Ok(String::new()); }; Ok(theseus::install::job_support_details(uuid).await?) } #[derive(Deserialize, Default)] #[serde(rename_all = "camelCase")] pub struct DownloadJobListRequest { pub status: Option, pub provider: Option, pub query: Option, pub cursor: Option, pub limit: Option, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct DownloadJobPage { pub jobs: Vec, pub next_cursor: Option, } #[tauri::command] pub async fn download_job_list( request: Option, ) -> Result { let request = request.unwrap_or_default(); let mut jobs = theseus::install::list_jobs(true).await?; if let Some(status) = request.status.as_deref() { jobs.retain(|job| job.status.as_str() == status); } if let Some(provider) = request.provider.as_deref() { jobs.retain(|job| { format!("{:?}", job.provider) .to_ascii_lowercase() .replace('_', "") == provider.to_ascii_lowercase().replace('_', "") }); } if let Some(query) = request.query.as_deref() { let query = query.trim().to_ascii_lowercase(); if !query.is_empty() { jobs.retain(|job| { job.display.as_ref().is_some_and(|display| { display.title.to_ascii_lowercase().contains(&query) }) || job.job_id.to_string().contains(&query) }); } } jobs.sort_by(|a, b| b.created.cmp(&a.created)); let offset = request .cursor .as_deref() .and_then(|cursor| cursor.parse::().ok()) .unwrap_or(0); let limit = request.limit.unwrap_or(100).clamp(1, 250); let jobs = jobs .into_iter() .skip(offset) .take(limit) .collect::>(); let next_cursor = (jobs.len() == limit).then(|| (offset + limit).to_string()); Ok(DownloadJobPage { jobs, next_cursor }) } #[tauri::command] pub async fn download_job_get(job_id: Uuid) -> Result { install_job_get(job_id).await } #[tauri::command] pub async fn download_job_retry(job_id: Uuid) -> Result { Ok(theseus::install::retry_job_as_new(job_id).await?) } #[tauri::command] pub async fn download_job_resume(job_id: Uuid) -> Result { install_job_resume(job_id).await } #[tauri::command] pub async fn download_job_cancel(job_id: Uuid) -> Result { install_job_cancel(job_id).await } #[tauri::command] pub async fn download_job_delete(job_id: Uuid) -> Result<()> { install_job_dismiss(job_id).await } #[tauri::command] pub async fn download_history_clear() -> Result { Ok(theseus::install::clear_job_history().await?) } #[tauri::command] pub async fn download_job_support_details(job_id: String) -> Result { install_job_support_details(job_id).await }