diff options
| author | 2026-02-25 02:06:07 +0800 | |
|---|---|---|
| committer | 2026-02-25 02:06:07 +0800 | |
| commit | 78ac61904d78d558d092eff08c9f261cbdb187e8 (patch) | |
| tree | 96f68d1f1554ee3a0532793afaaa52b0c73dcbec /src-tauri/src/main.rs | |
| parent | 8ff3af6cb908fd824b512379dd21ed4f595ab6bb (diff) | |
| parent | 329734b23957b84cde2af459fa61c7385fb5b5f1 (diff) | |
| download | DropOut-78ac61904d78d558d092eff08c9f261cbdb187e8.tar.gz DropOut-78ac61904d78d558d092eff08c9f261cbdb187e8.zip | |
feat(ui): partial react rewrite (#77)
## Summary by Sourcery
Export backend data structures to TypeScript for the new React-based UI
and update CI to build additional targets.
New Features:
- Generate TypeScript definitions for core backend structs and enums
used by the UI.
- Now use our own Azure app(_DropOut_) to finish the authorize process.
Enhancements:
- Annotate existing Rust models with ts-rs metadata to control exported
TypeScript shapes, including tagged enums and opaque JSON fields.
Build:
- Add ts-rs as a dependency for generating TypeScript bindings from Rust
types.
CI:
- Extend the Semifold CI workflow to run on the dev branch and build
additional Linux musl and Windows GNU targets using cross where needed.
Diffstat (limited to 'src-tauri/src/main.rs')
| -rw-r--r-- | src-tauri/src/main.rs | 255 |
1 files changed, 157 insertions, 98 deletions
diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 0f1d7a1..33c94fe 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -6,7 +6,8 @@ use std::process::Stdio; use std::sync::Mutex; use tauri::{Emitter, Manager, State, Window}; // Added Emitter use tokio::io::{AsyncBufReadExt, BufReader}; -use tokio::process::Command; // Added Serialize +use tokio::process::Command; +use ts_rs::TS; // Added Serialize #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; @@ -63,6 +64,7 @@ fn has_unresolved_placeholder(s: &str) -> bool { } #[tauri::command] +#[dropout_macros::api] async fn start_game( window: Window, auth_state: State<'_, core::auth::AccountState>, @@ -198,92 +200,54 @@ async fn start_game( None }; - // Check if configured Java is compatible + // Resolve Java using priority-based resolution + // Priority: instance override > global config > user preference > auto-detect + // TODO: refactor into a separate function let app_handle = window.app_handle(); - let mut java_path_to_use = config.java_path.clone(); - if !java_path_to_use.is_empty() && java_path_to_use != "java" { - let is_compatible = - core::java::is_java_compatible(&java_path_to_use, required_java_major, max_java_major); - - if !is_compatible { - emit_log!( - window, - format!( - "Configured Java version may not be compatible. Looking for compatible Java..." - ) - ); - - // Try to find a compatible Java version - if let Some(compatible_java) = - core::java::get_compatible_java(app_handle, required_java_major, max_java_major) - { - emit_log!( - window, - format!( - "Found compatible Java {} at: {}", - compatible_java.version, compatible_java.path - ) - ); - java_path_to_use = compatible_java.path; - } else { - let version_constraint = if let Some(max) = max_java_major { - if let Some(min) = required_java_major { - if min == max as u64 { - format!("Java {}", min) - } else { - format!("Java {} to {}", min, max) - } - } else { - format!("Java {} (or lower)", max) - } - } else if let Some(min) = required_java_major { - format!("Java {} or higher", min) - } else { - "any Java version".to_string() - }; + let instance = instance_state + .get_instance(&instance_id) + .ok_or_else(|| format!("Instance {} not found", instance_id))?; - return Err(format!( - "No compatible Java installation found. This version requires {}. Please install a compatible Java version in settings.", - version_constraint - )); - } - } - } else { - // No Java configured, try to find a compatible one - if let Some(compatible_java) = - core::java::get_compatible_java(app_handle, required_java_major, max_java_major) - { - emit_log!( - window, - format!( - "Using Java {} at: {}", - compatible_java.version, compatible_java.path - ) - ); - java_path_to_use = compatible_java.path; - } else { - let version_constraint = if let Some(max) = max_java_major { - if let Some(min) = required_java_major { - if min == max as u64 { - format!("Java {}", min) - } else { - format!("Java {} to {}", min, max) - } + let java_installation = core::java::priority::resolve_java_for_launch( + app_handle, + instance.java_path_override.as_deref(), + Some(&config.java_path), + required_java_major, + max_java_major, + ) + .await + .ok_or_else(|| { + let version_constraint = if let Some(max) = max_java_major { + if let Some(min) = required_java_major { + if min == max as u64 { + format!("Java {}", min) } else { - format!("Java {} (or lower)", max) + format!("Java {} to {}", min, max) } - } else if let Some(min) = required_java_major { - format!("Java {} or higher", min) } else { - "any Java version".to_string() - }; + format!("Java {} (or lower)", max) + } + } else if let Some(min) = required_java_major { + format!("Java {} or higher", min) + } else { + "any Java version".to_string() + }; - return Err(format!( - "No compatible Java installation found. This version requires {}. Please install a compatible Java version in settings.", - version_constraint - )); - } - } + format!( + "No compatible Java installation found. This version requires {}. Please install a compatible Java version in settings.", + version_constraint + ) + })?; + + emit_log!( + window, + format!( + "Using Java {} at: {}", + java_installation.version, java_installation.path + ) + ); + + let java_path_to_use = java_installation.path; // 2. Prepare download tasks emit_log!(window, "Preparing download tasks...".to_string()); @@ -934,7 +898,18 @@ fn parse_jvm_arguments( } #[tauri::command] -async fn get_versions( +#[dropout_macros::api] +async fn get_versions() -> Result<Vec<core::manifest::Version>, String> { + core::manifest::fetch_version_manifest() + .await + .map(|m| m.versions) + .map_err(|e| e.to_string()) +} + +/// Get all available versions from Mojang's version manifest +#[tauri::command] +#[dropout_macros::api] +async fn get_versions_of_instance( _window: Window, instance_state: State<'_, core::instance::InstanceState>, instance_id: String, @@ -978,6 +953,7 @@ async fn get_versions( /// Check if a version is installed (has client.jar) #[tauri::command] +#[dropout_macros::api] async fn check_version_installed( _window: Window, instance_state: State<'_, core::instance::InstanceState>, @@ -1017,6 +993,7 @@ async fn check_version_installed( /// Install a version (download client, libraries, assets) without launching #[tauri::command] +#[dropout_macros::api] async fn install_version( window: Window, config_state: State<'_, core::config::ConfigState>, @@ -1340,6 +1317,7 @@ async fn install_version( } #[tauri::command] +#[dropout_macros::api] async fn login_offline( window: Window, state: State<'_, core::auth::AccountState>, @@ -1363,6 +1341,7 @@ async fn login_offline( } #[tauri::command] +#[dropout_macros::api] async fn get_active_account( state: State<'_, core::auth::AccountState>, ) -> Result<Option<core::auth::Account>, String> { @@ -1370,6 +1349,7 @@ async fn get_active_account( } #[tauri::command] +#[dropout_macros::api] async fn logout(window: Window, state: State<'_, core::auth::AccountState>) -> Result<(), String> { // Get current account UUID before clearing let uuid = state @@ -1396,6 +1376,7 @@ async fn logout(window: Window, state: State<'_, core::auth::AccountState>) -> R } #[tauri::command] +#[dropout_macros::api] async fn get_settings( state: State<'_, core::config::ConfigState>, ) -> Result<core::config::LauncherConfig, String> { @@ -1403,6 +1384,7 @@ async fn get_settings( } #[tauri::command] +#[dropout_macros::api] async fn save_settings( state: State<'_, core::config::ConfigState>, config: core::config::LauncherConfig, @@ -1413,11 +1395,13 @@ async fn save_settings( } #[tauri::command] +#[dropout_macros::api] async fn get_config_path(state: State<'_, core::config::ConfigState>) -> Result<String, String> { Ok(state.file_path.to_string_lossy().to_string()) } #[tauri::command] +#[dropout_macros::api] async fn read_raw_config(state: State<'_, core::config::ConfigState>) -> Result<String, String> { tokio::fs::read_to_string(&state.file_path) .await @@ -1425,6 +1409,7 @@ async fn read_raw_config(state: State<'_, core::config::ConfigState>) -> Result< } #[tauri::command] +#[dropout_macros::api] async fn save_raw_config( state: State<'_, core::config::ConfigState>, content: String, @@ -1445,11 +1430,13 @@ async fn save_raw_config( } #[tauri::command] +#[dropout_macros::api] async fn start_microsoft_login() -> Result<core::auth::DeviceCodeResponse, String> { core::auth::start_device_flow().await } #[tauri::command] +#[dropout_macros::api] async fn complete_microsoft_login( window: Window, state: State<'_, core::auth::AccountState>, @@ -1520,6 +1507,7 @@ async fn complete_microsoft_login( /// Refresh token for current Microsoft account #[tauri::command] +#[dropout_macros::api] async fn refresh_account( window: Window, state: State<'_, core::auth::AccountState>, @@ -1555,22 +1543,34 @@ async fn refresh_account( /// Detect Java installations on the system #[tauri::command] +#[dropout_macros::api] +async fn detect_all_java_installations( + app_handle: tauri::AppHandle, +) -> Result<Vec<core::java::JavaInstallation>, String> { + Ok(core::java::detect_all_java_installations(&app_handle).await) +} + +/// Alias for detect_all_java_installations (for backward compatibility) +#[tauri::command] +#[dropout_macros::api] async fn detect_java( app_handle: tauri::AppHandle, ) -> Result<Vec<core::java::JavaInstallation>, String> { - Ok(core::java::detect_all_java_installations(&app_handle)) + Ok(core::java::detect_all_java_installations(&app_handle).await) } /// Get recommended Java for a specific Minecraft version #[tauri::command] +#[dropout_macros::api] async fn get_recommended_java( required_major_version: Option<u64>, ) -> Result<Option<core::java::JavaInstallation>, String> { - Ok(core::java::get_recommended_java(required_major_version)) + Ok(core::java::get_recommended_java(required_major_version).await) } /// Get Adoptium Java download info #[tauri::command] +#[dropout_macros::api] async fn fetch_adoptium_java( major_version: u32, image_type: String, @@ -1579,11 +1579,14 @@ async fn fetch_adoptium_java( "jdk" => core::java::ImageType::Jdk, _ => core::java::ImageType::Jre, }; - core::java::fetch_java_release(major_version, img_type).await + core::java::fetch_java_release(major_version, img_type) + .await + .map_err(|e| e.to_string()) } /// Download and install Adoptium Java #[tauri::command] +#[dropout_macros::api] async fn download_adoptium_java( app_handle: tauri::AppHandle, major_version: u32, @@ -1595,33 +1598,45 @@ async fn download_adoptium_java( _ => core::java::ImageType::Jre, }; let path = custom_path.map(std::path::PathBuf::from); - core::java::download_and_install_java(&app_handle, major_version, img_type, path).await + core::java::download_and_install_java(&app_handle, major_version, img_type, path) + .await + .map_err(|e| e.to_string()) } /// Get available Adoptium Java versions #[tauri::command] +#[dropout_macros::api] async fn fetch_available_java_versions() -> Result<Vec<u32>, String> { - core::java::fetch_available_versions().await + core::java::fetch_available_versions() + .await + .map_err(|e| e.to_string()) } /// Fetch Java catalog with platform availability (uses cache) #[tauri::command] +#[dropout_macros::api] async fn fetch_java_catalog( app_handle: tauri::AppHandle, ) -> Result<core::java::JavaCatalog, String> { - core::java::fetch_java_catalog(&app_handle, false).await + core::java::fetch_java_catalog(&app_handle, false) + .await + .map_err(|e| e.to_string()) } /// Refresh Java catalog (bypass cache) #[tauri::command] +#[dropout_macros::api] async fn refresh_java_catalog( app_handle: tauri::AppHandle, ) -> Result<core::java::JavaCatalog, String> { - core::java::fetch_java_catalog(&app_handle, true).await + core::java::fetch_java_catalog(&app_handle, true) + .await + .map_err(|e| e.to_string()) } /// Cancel current Java download #[tauri::command] +#[dropout_macros::api] async fn cancel_java_download() -> Result<(), String> { core::java::cancel_current_download(); Ok(()) @@ -1629,6 +1644,7 @@ async fn cancel_java_download() -> Result<(), String> { /// Get pending Java downloads #[tauri::command] +#[dropout_macros::api] async fn get_pending_java_downloads( app_handle: tauri::AppHandle, ) -> Result<Vec<core::downloader::PendingJavaDownload>, String> { @@ -1637,6 +1653,7 @@ async fn get_pending_java_downloads( /// Resume pending Java downloads #[tauri::command] +#[dropout_macros::api] async fn resume_java_downloads( app_handle: tauri::AppHandle, ) -> Result<Vec<core::java::JavaInstallation>, String> { @@ -1645,6 +1662,7 @@ async fn resume_java_downloads( /// Get Minecraft versions supported by Fabric #[tauri::command] +#[dropout_macros::api] async fn get_fabric_game_versions() -> Result<Vec<core::fabric::FabricGameVersion>, String> { core::fabric::fetch_supported_game_versions() .await @@ -1653,6 +1671,7 @@ async fn get_fabric_game_versions() -> Result<Vec<core::fabric::FabricGameVersio /// Get available Fabric loader versions #[tauri::command] +#[dropout_macros::api] async fn get_fabric_loader_versions() -> Result<Vec<core::fabric::FabricLoaderVersion>, String> { core::fabric::fetch_loader_versions() .await @@ -1661,6 +1680,7 @@ async fn get_fabric_loader_versions() -> Result<Vec<core::fabric::FabricLoaderVe /// Get Fabric loaders available for a specific Minecraft version #[tauri::command] +#[dropout_macros::api] async fn get_fabric_loaders_for_version( game_version: String, ) -> Result<Vec<core::fabric::FabricLoaderEntry>, String> { @@ -1671,6 +1691,7 @@ async fn get_fabric_loaders_for_version( /// Install Fabric loader for a specific Minecraft version #[tauri::command] +#[dropout_macros::api] async fn install_fabric( window: Window, instance_state: State<'_, core::instance::InstanceState>, @@ -1715,6 +1736,7 @@ async fn install_fabric( /// List installed Fabric versions #[tauri::command] +#[dropout_macros::api] async fn list_installed_fabric_versions( _window: Window, instance_state: State<'_, core::instance::InstanceState>, @@ -1731,6 +1753,7 @@ async fn list_installed_fabric_versions( /// Get Java version requirement for a specific version #[tauri::command] +#[dropout_macros::api] async fn get_version_java_version( _window: Window, instance_state: State<'_, core::instance::InstanceState>, @@ -1749,17 +1772,18 @@ async fn get_version_java_version( } /// Version metadata for display in the UI -#[derive(serde::Serialize)] +#[derive(serde::Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "core.ts")] struct VersionMetadata { id: String, - #[serde(rename = "javaVersion")] java_version: Option<u64>, - #[serde(rename = "isInstalled")] is_installed: bool, } /// Delete a version (remove version directory) #[tauri::command] +#[dropout_macros::api] async fn delete_version( window: Window, instance_state: State<'_, core::instance::InstanceState>, @@ -1814,6 +1838,7 @@ async fn delete_version( /// Get detailed metadata for a specific version #[tauri::command] +#[dropout_macros::api] async fn get_version_metadata( _window: Window, instance_state: State<'_, core::instance::InstanceState>, @@ -1899,7 +1924,9 @@ async fn get_version_metadata( } /// Installed version info -#[derive(serde::Serialize)] +#[derive(serde::Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "core.ts")] struct InstalledVersion { id: String, #[serde(rename = "type")] @@ -1909,6 +1936,7 @@ struct InstalledVersion { /// List all installed versions from the data directory /// Simply lists all folders in the versions directory without validation #[tauri::command] +#[dropout_macros::api] async fn list_installed_versions( _window: Window, instance_state: State<'_, core::instance::InstanceState>, @@ -1995,6 +2023,7 @@ async fn list_installed_versions( /// Check if Fabric is installed for a specific version #[tauri::command] +#[dropout_macros::api] async fn is_fabric_installed( _window: Window, instance_state: State<'_, core::instance::InstanceState>, @@ -2015,6 +2044,7 @@ async fn is_fabric_installed( /// Get Minecraft versions supported by Forge #[tauri::command] +#[dropout_macros::api] async fn get_forge_game_versions() -> Result<Vec<String>, String> { core::forge::fetch_supported_game_versions() .await @@ -2023,6 +2053,7 @@ async fn get_forge_game_versions() -> Result<Vec<String>, String> { /// Get available Forge versions for a specific Minecraft version #[tauri::command] +#[dropout_macros::api] async fn get_forge_versions_for_game( game_version: String, ) -> Result<Vec<core::forge::ForgeVersion>, String> { @@ -2033,6 +2064,7 @@ async fn get_forge_versions_for_game( /// Install Forge for a specific Minecraft version #[tauri::command] +#[dropout_macros::api] async fn install_forge( window: Window, config_state: State<'_, core::config::ConfigState>, @@ -2060,7 +2092,7 @@ async fn install_forge( config.java_path.clone() } else { // Try to find a suitable Java installation - let javas = core::java::detect_all_java_installations(app_handle); + let javas = core::java::detect_all_java_installations(app_handle).await; if let Some(java) = javas.first() { java.path.clone() } else { @@ -2128,7 +2160,9 @@ async fn install_forge( Ok(result) } -#[derive(serde::Serialize)] +#[derive(serde::Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "core.ts")] struct GithubRelease { tag_name: String, name: String, @@ -2138,6 +2172,7 @@ struct GithubRelease { } #[tauri::command] +#[dropout_macros::api] async fn get_github_releases() -> Result<Vec<GithubRelease>, String> { let client = reqwest::Client::new(); let res = client @@ -2174,12 +2209,14 @@ async fn get_github_releases() -> Result<Vec<GithubRelease>, String> { Ok(result) } -#[derive(Serialize)] +#[derive(Serialize, TS)] +#[ts(export, export_to = "core.ts")] struct PastebinResponse { url: String, } #[tauri::command] +#[dropout_macros::api] async fn upload_to_pastebin( state: State<'_, core::config::ConfigState>, content: String, @@ -2249,6 +2286,7 @@ async fn upload_to_pastebin( } #[tauri::command] +#[dropout_macros::api] async fn assistant_check_health( assistant_state: State<'_, core::assistant::AssistantState>, config_state: State<'_, core::config::ConfigState>, @@ -2259,6 +2297,7 @@ async fn assistant_check_health( } #[tauri::command] +#[dropout_macros::api] async fn assistant_chat( assistant_state: State<'_, core::assistant::AssistantState>, config_state: State<'_, core::config::ConfigState>, @@ -2270,6 +2309,7 @@ async fn assistant_chat( } #[tauri::command] +#[dropout_macros::api] async fn list_ollama_models( assistant_state: State<'_, core::assistant::AssistantState>, endpoint: String, @@ -2279,6 +2319,7 @@ async fn list_ollama_models( } #[tauri::command] +#[dropout_macros::api] async fn list_openai_models( assistant_state: State<'_, core::assistant::AssistantState>, config_state: State<'_, core::config::ConfigState>, @@ -2292,6 +2333,7 @@ async fn list_openai_models( /// Create a new instance #[tauri::command] +#[dropout_macros::api] async fn create_instance( window: Window, state: State<'_, core::instance::InstanceState>, @@ -2303,6 +2345,7 @@ async fn create_instance( /// Delete an instance #[tauri::command] +#[dropout_macros::api] async fn delete_instance( state: State<'_, core::instance::InstanceState>, instance_id: String, @@ -2312,6 +2355,7 @@ async fn delete_instance( /// Update an instance #[tauri::command] +#[dropout_macros::api] async fn update_instance( state: State<'_, core::instance::InstanceState>, instance: core::instance::Instance, @@ -2321,6 +2365,7 @@ async fn update_instance( /// Get all instances #[tauri::command] +#[dropout_macros::api] async fn list_instances( state: State<'_, core::instance::InstanceState>, ) -> Result<Vec<core::instance::Instance>, String> { @@ -2329,6 +2374,7 @@ async fn list_instances( /// Get a single instance by ID #[tauri::command] +#[dropout_macros::api] async fn get_instance( state: State<'_, core::instance::InstanceState>, instance_id: String, @@ -2340,6 +2386,7 @@ async fn get_instance( /// Set the active instance #[tauri::command] +#[dropout_macros::api] async fn set_active_instance( state: State<'_, core::instance::InstanceState>, instance_id: String, @@ -2349,6 +2396,7 @@ async fn set_active_instance( /// Get the active instance #[tauri::command] +#[dropout_macros::api] async fn get_active_instance( state: State<'_, core::instance::InstanceState>, ) -> Result<Option<core::instance::Instance>, String> { @@ -2357,6 +2405,7 @@ async fn get_active_instance( /// Duplicate an instance #[tauri::command] +#[dropout_macros::api] async fn duplicate_instance( window: Window, state: State<'_, core::instance::InstanceState>, @@ -2368,6 +2417,7 @@ async fn duplicate_instance( } #[tauri::command] +#[dropout_macros::api] async fn assistant_chat_stream( window: tauri::Window, assistant_state: State<'_, core::assistant::AssistantState>, @@ -2382,7 +2432,9 @@ async fn assistant_chat_stream( } /// Migrate instance caches to shared global caches -#[derive(Serialize)] +#[derive(Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "core.ts")] struct MigrationResult { moved_files: usize, hardlinks: usize, @@ -2392,6 +2444,7 @@ struct MigrationResult { } #[tauri::command] +#[dropout_macros::api] async fn migrate_shared_caches( window: Window, instance_state: State<'_, core::instance::InstanceState>, @@ -2431,7 +2484,9 @@ async fn migrate_shared_caches( } /// File information for instance file browser -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "core.ts")] struct FileInfo { name: String, path: String, @@ -2442,6 +2497,7 @@ struct FileInfo { /// List files in an instance subdirectory (mods, resourcepacks, shaderpacks, saves, screenshots) #[tauri::command] +#[dropout_macros::api] async fn list_instance_directory( instance_state: State<'_, core::instance::InstanceState>, instance_id: String, @@ -2493,6 +2549,7 @@ async fn list_instance_directory( /// Delete a file in an instance directory #[tauri::command] +#[dropout_macros::api] async fn delete_instance_file(path: String) -> Result<(), String> { let path_buf = std::path::PathBuf::from(&path); if path_buf.is_dir() { @@ -2509,6 +2566,7 @@ async fn delete_instance_file(path: String) -> Result<(), String> { /// Open instance directory in system file explorer #[tauri::command] +#[dropout_macros::api] async fn open_file_explorer(path: String) -> Result<(), String> { #[cfg(target_os = "windows")] { @@ -2586,6 +2644,7 @@ fn main() { .invoke_handler(tauri::generate_handler![ start_game, get_versions, + get_versions_of_instance, check_version_installed, install_version, list_installed_versions, |