From 53057066f508c2b6c5d7fb66cea15a69acaabbce Mon Sep 17 00:00:00 2001 From: xunying123 Date: Wed, 19 Nov 2025 15:07:16 +0800 Subject: [PATCH 01/12] feat(instance): support download optifine installer --- src-tauri/src/instance/commands.rs | 6 +- src-tauri/src/instance/helpers/client_json.rs | 32 +++++++--- .../src/instance/helpers/loader/common.rs | 14 ++++- src-tauri/src/instance/helpers/loader/mod.rs | 1 + .../src/instance/helpers/loader/optifine.rs | 60 +++++++++++++++++++ src-tauri/src/instance/helpers/misc.rs | 12 ++-- src-tauri/src/instance/models/misc.rs | 2 + .../resource/helpers/loader_meta/fabric.rs | 1 + .../src/resource/helpers/loader_meta/forge.rs | 1 + .../resource/helpers/loader_meta/neoforge.rs | 3 + src-tauri/src/resource/helpers/misc.rs | 2 +- src-tauri/src/resource/models.rs | 9 +++ 12 files changed, 126 insertions(+), 17 deletions(-) create mode 100644 src-tauri/src/instance/helpers/loader/optifine.rs diff --git a/src-tauri/src/instance/commands.rs b/src-tauri/src/instance/commands.rs index 2eb12cffe..f60512a17 100644 --- a/src-tauri/src/instance/commands.rs +++ b/src-tauri/src/instance/commands.rs @@ -34,7 +34,9 @@ use crate::launcher_config::helpers::misc::get_global_game_config; use crate::launcher_config::models::{GameConfig, GameDirectory, LauncherConfig}; use crate::partial::{PartialError, PartialUpdate}; use crate::resource::helpers::misc::get_source_priority_list; -use crate::resource::models::{GameClientResourceInfo, ModLoaderResourceInfo}; +use crate::resource::models::{ + GameClientResourceInfo, ModLoaderResourceInfo, OptifineResourceInfo, +}; use crate::storage::{load_json_async, save_json_async, Storage}; use crate::tasks::commands::schedule_progressive_task_group; use crate::tasks::download::DownloadParam; @@ -935,6 +937,7 @@ pub async fn create_instance( }, version: mod_loader.version.clone(), branch: mod_loader.branch.clone(), + optifine: mod_loader.optifine.clone(), }, description, icon_src, @@ -1209,6 +1212,7 @@ pub async fn change_mod_loader( ModLoaderStatus::NotDownloaded }, branch: new_mod_loader.branch.clone(), + optifine: new_mod_loader.optifine.clone(), }; let game_version = instance.version.clone(); let subdirs = get_instance_subdir_paths( diff --git a/src-tauri/src/instance/helpers/client_json.rs b/src-tauri/src/instance/helpers/client_json.rs index 9d33efc83..4e7a6f7ca 100644 --- a/src-tauri/src/instance/helpers/client_json.rs +++ b/src-tauri/src/instance/helpers/client_json.rs @@ -1,7 +1,7 @@ use crate::error::{SJMCLError, SJMCLResult}; -use crate::instance::helpers::game_version::compare_game_versions; use crate::instance::models::misc::{Instance, ModLoaderType}; use crate::launcher_config::models::LauncherConfig; +use crate::resource::models::OptifineResourceInfo; use crate::utils::fs::get_app_resource_filepath; use regex::RegexBuilder; use serde::{Deserialize, Deserializer, Serialize}; @@ -306,10 +306,17 @@ pub struct LoggingFile { pub fn patches_to_info( patches: &[McClientInfo], -) -> (Option, Option, ModLoaderType) { +) -> ( + Option, + Option, + ModLoaderType, + Option, +) { let mut loader_type = ModLoaderType::Unknown; let mut game_version = None; let mut loader_version = None; + let mut optifine_info: Option = None; + println!("Patches_num: {}", patches.len()); for patch in patches { if game_version.is_none() && patch.id == "game" { game_version = patch.version.clone(); @@ -320,18 +327,25 @@ pub fn patches_to_info( loader_version = patch.version.clone(); } } - - if game_version.is_some() && loader_type != ModLoaderType::Unknown { - break; + if patch.id == "optifine" { + optifine_info = Some(OptifineResourceInfo { + patch: "".to_string(), + filename: "".to_string(), + r#type: patch.version.clone().unwrap_or_default(), + }); } } - - (game_version, loader_version, loader_type) + (game_version, loader_version, loader_type, optifine_info) } pub async fn libraries_to_info( client: &McClientInfo, -) -> (Option, Option, ModLoaderType) { +) -> ( + Option, + Option, + ModLoaderType, + Option, +) { let game_version: Option = client.client_version.clone(); let mut loader_version: Option = None; let mut loader_type = ModLoaderType::Unknown; @@ -394,7 +408,7 @@ pub async fn libraries_to_info( } } - (game_version, loader_version, loader_type) + (game_version, loader_version, loader_type, None) } fn rules_is_allowed(rules: &Vec, feature: &FeaturesInfo) -> SJMCLResult { diff --git a/src-tauri/src/instance/helpers/loader/common.rs b/src-tauri/src/instance/helpers/loader/common.rs index 131e5b353..8614dda40 100644 --- a/src-tauri/src/instance/helpers/loader/common.rs +++ b/src-tauri/src/instance/helpers/loader/common.rs @@ -11,6 +11,7 @@ use crate::instance::helpers::client_json::{LibrariesValue, McClientInfo}; use crate::instance::helpers::loader::fabric::install_fabric_loader; use crate::instance::helpers::loader::forge::{install_forge_loader, InstallProfile}; use crate::instance::helpers::loader::neoforge::install_neoforge_loader; +use crate::instance::helpers::loader::optifine::install_optifine; use crate::instance::helpers::misc::get_instance_game_config; use crate::instance::models::misc::{Instance, InstanceError, ModLoader, ModLoaderType}; use crate::launch::helpers::file_validator::merge_library_lists; @@ -59,7 +60,18 @@ pub async fn install_mod_loader( .await } ModLoaderType::Forge => { - install_forge_loader(priority, game_version, loader, lib_dir, task_params).await + install_forge_loader(priority, game_version, loader, lib_dir.clone(), task_params).await?; + if loader.optifine.is_some() { + install_optifine( + priority, + game_version, + loader.optifine.as_ref().unwrap(), + lib_dir.clone(), + task_params, + ) + .await?; + } + Ok(()) } ModLoaderType::NeoForge => { install_neoforge_loader(priority, loader, lib_dir, task_params).await diff --git a/src-tauri/src/instance/helpers/loader/mod.rs b/src-tauri/src/instance/helpers/loader/mod.rs index 11476f783..0806ad901 100644 --- a/src-tauri/src/instance/helpers/loader/mod.rs +++ b/src-tauri/src/instance/helpers/loader/mod.rs @@ -2,3 +2,4 @@ pub mod common; pub mod fabric; pub mod forge; pub mod neoforge; +pub mod optifine; diff --git a/src-tauri/src/instance/helpers/loader/optifine.rs b/src-tauri/src/instance/helpers/loader/optifine.rs new file mode 100644 index 000000000..45ecf2b0f --- /dev/null +++ b/src-tauri/src/instance/helpers/loader/optifine.rs @@ -0,0 +1,60 @@ +use reqwest::redirect::Policy; +use reqwest::{Client, Error}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs::{self, File}; +use std::io::Read; +use std::path::PathBuf; +use tauri::AppHandle; +use tauri_plugin_http::reqwest; +use url::Url; +use zip::ZipArchive; + +use crate::error::SJMCLResult; +use crate::instance::helpers::client_json::{LaunchArgumentTemplate, LibrariesValue, McClientInfo}; +use crate::instance::helpers::loader::common::add_library_entry; +use crate::instance::helpers::misc::get_instance_subdir_paths; +use crate::instance::models::misc::{Instance, InstanceError, InstanceSubdirType, ModLoader}; +use crate::launch::helpers::file_validator::convert_library_name_to_path; +use crate::resource::helpers::misc::{convert_url_to_target_source, get_download_api}; +use crate::resource::models::{OptifineResourceInfo, ResourceType, SourceType}; +use crate::tasks::commands::schedule_progressive_task_group; +use crate::tasks::download::DownloadParam; +use crate::tasks::PTaskParam; + +pub async fn install_optifine( + priority: &[SourceType], + game_version: &str, + optifine: &OptifineResourceInfo, + lib_dir: PathBuf, + task_params: &mut Vec, +) -> SJMCLResult<()> { + let root = get_download_api(priority[0], ResourceType::Optifine)?; + + let installer_url = match priority.first().unwrap_or(&SourceType::Official) { + &SourceType::Official => root.join(&format!( + "{}/{}/{}", + game_version, optifine.r#type, optifine.patch + ))?, + &SourceType::BMCLAPIMirror => root.join(&format!( + "{}/{}/{}", + game_version, optifine.r#type, optifine.patch + ))?, + }; + + let installer_coord = format!( + "net.minecraftforge:optifine:{}-installer", + optifine.filename + ); + let installer_rel = convert_library_name_to_path(&installer_coord, None)?; + let installer_path = lib_dir.join(&installer_rel); + + task_params.push(PTaskParam::Download(DownloadParam { + src: installer_url, + dest: installer_path.clone(), + filename: None, + sha1: None, + })); + + Ok(()) +} diff --git a/src-tauri/src/instance/helpers/misc.rs b/src-tauri/src/instance/helpers/misc.rs index d4f26265f..e6b8fa5a6 100644 --- a/src-tauri/src/instance/helpers/misc.rs +++ b/src-tauri/src/instance/helpers/misc.rs @@ -225,11 +225,12 @@ pub async fn refresh_instances( } } - let (mut game_version, loader_version, loader_type) = if !client_data.patches.is_empty() { - patches_to_info(&client_data.patches) - } else { - libraries_to_info(&client_data).await - }; + let (mut game_version, loader_version, loader_type, optifine_info) = + if !client_data.patches.is_empty() { + patches_to_info(&client_data.patches) + } else { + libraries_to_info(&client_data).await + }; // TODO: patches related logic if game_version.is_none() { let file = Cursor::new(tokio::fs::read(jar_path).await?); @@ -255,6 +256,7 @@ pub async fn refresh_instances( version: loader_version.unwrap_or_default(), status: ModLoaderStatus::Installed, branch: None, + optifine: optifine_info.clone(), } }, ..cfg_read diff --git a/src-tauri/src/instance/models/misc.rs b/src-tauri/src/instance/models/misc.rs index b657761eb..ffb2cccb2 100644 --- a/src-tauri/src/instance/models/misc.rs +++ b/src-tauri/src/instance/models/misc.rs @@ -1,5 +1,6 @@ use crate::instance::constants::INSTANCE_CFG_FILE_NAME; use crate::launcher_config::models::GameConfig; +use crate::resource::models::OptifineResourceInfo; use crate::storage::{load_json_async, save_json_async}; use crate::utils::image::ImageWrapper; use serde::{Deserialize, Serialize}; @@ -94,6 +95,7 @@ structstruck::strike! { pub loader_type: ModLoaderType, pub version: String, pub branch: Option, // Optional branch name for mod loaders like Forge + pub optifine: Option, }, // if true, use the spec_game_config, else use the global game config pub use_spec_game_config: bool, diff --git a/src-tauri/src/resource/helpers/loader_meta/fabric.rs b/src-tauri/src/resource/helpers/loader_meta/fabric.rs index fb472d416..32bc01e09 100644 --- a/src-tauri/src/resource/helpers/loader_meta/fabric.rs +++ b/src-tauri/src/resource/helpers/loader_meta/fabric.rs @@ -47,6 +47,7 @@ pub async fn get_fabric_meta_by_game_version( description: String::new(), stable: info.loader.stable, branch: None, + optifine: None, }) .collect(), ); diff --git a/src-tauri/src/resource/helpers/loader_meta/forge.rs b/src-tauri/src/resource/helpers/loader_meta/forge.rs index ae6fd6891..32734985f 100644 --- a/src-tauri/src/resource/helpers/loader_meta/forge.rs +++ b/src-tauri/src/resource/helpers/loader_meta/forge.rs @@ -39,6 +39,7 @@ async fn get_forge_meta_by_game_version_bmcl( description: info.modified, stable: true, branch: info.branch.and_then(|v| v.as_str().map(String::from)), + optifine: None, }) .collect(), ) diff --git a/src-tauri/src/resource/helpers/loader_meta/neoforge.rs b/src-tauri/src/resource/helpers/loader_meta/neoforge.rs index 9a6331b7e..bed306fd2 100644 --- a/src-tauri/src/resource/helpers/loader_meta/neoforge.rs +++ b/src-tauri/src/resource/helpers/loader_meta/neoforge.rs @@ -82,6 +82,7 @@ async fn get_neoforge_meta_by_game_version_official( .get("is_snapshot") .map_or(true, |v| !v.as_bool().unwrap_or(false)), branch: None, + optifine: None, }, )); } @@ -147,6 +148,7 @@ async fn get_neoforge_meta_by_game_version_official( description: String::new(), stable, branch: None, + optifine: None, }, )); } @@ -199,6 +201,7 @@ async fn get_neoforge_meta_by_game_version_bmcl( description: String::new(), stable, branch: None, + optifine: None, } }) .collect(), diff --git a/src-tauri/src/resource/helpers/misc.rs b/src-tauri/src/resource/helpers/misc.rs index 89969641a..5aeebb66f 100644 --- a/src-tauri/src/resource/helpers/misc.rs +++ b/src-tauri/src/resource/helpers/misc.rs @@ -69,7 +69,7 @@ pub fn get_download_api(source: SourceType, resource_type: ResourceType) -> SJMC ResourceType::FabricMaven => Ok(Url::parse("https://bmclapi2.bangbang93.com/maven/")?), ResourceType::NeoforgeMetaForge | ResourceType::NeoforgeMetaNeoforge => Ok(Url::parse("https://bmclapi2.bangbang93.com/neoforge/")?), ResourceType::NeoforgeInstall => Ok(Url::parse("https://bmclapi2.bangbang93.com/neoforge/version/")?), - ResourceType::Optifine => Err(ResourceError::NoDownloadApi.into()), + ResourceType::Optifine => Ok(Url::parse("https://bmclapi2.bangbang93.com/optifine/")?), ResourceType::QuiltMaven => Ok(Url::parse("https://bmclapi2.bangbang93.com/maven/")?), ResourceType::QuiltMeta => Ok(Url::parse("https://bmclapi2.bangbang93.com/quilt-meta/")?), }, diff --git a/src-tauri/src/resource/models.rs b/src-tauri/src/resource/models.rs index 10707c055..da3c61bda 100644 --- a/src-tauri/src/resource/models.rs +++ b/src-tauri/src/resource/models.rs @@ -180,6 +180,15 @@ pub struct ModLoaderResourceInfo { pub description: String, pub stable: bool, pub branch: Option, + pub optifine: Option, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Default)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct OptifineResourceInfo { + pub filename: String, + pub patch: String, + pub r#type: String, } #[derive(Debug, Display)] From 4d8034a7e1a125c281868974518b18cf2cb765c5 Mon Sep 17 00:00:00 2001 From: Reqwey Date: Wed, 24 Dec 2025 15:24:15 +0800 Subject: [PATCH 02/12] feat(instance): introduce new loader selector with optifine integration --- package.json | 2 +- public/images/icons/OptiFine.png | Bin 0 -> 51040 bytes src-tauri/src/instance/commands.rs | 18 +- src-tauri/src/instance/helpers/client_json.rs | 11 +- .../src/instance/helpers/loader/common.rs | 13 +- .../src/instance/helpers/loader/optifine.rs | 29 +- src-tauri/src/instance/helpers/misc.rs | 2 +- src-tauri/src/instance/models/misc.rs | 4 +- src-tauri/src/lib.rs | 1 + src-tauri/src/resource/commands.rs | 20 +- .../resource/helpers/loader_meta/fabric.rs | 1 - .../src/resource/helpers/loader_meta/forge.rs | 5 +- .../src/resource/helpers/loader_meta/mod.rs | 1 + .../resource/helpers/loader_meta/neoforge.rs | 3 - .../resource/helpers/loader_meta/optifine.rs | 43 +++ src-tauri/src/resource/helpers/misc.rs | 4 +- src-tauri/src/resource/models.rs | 7 +- src/components/common/selectable-card.tsx | 113 +++++++ src/components/loader-selector.tsx | 309 ++++++++++++++++++ src/components/mod-loader-cards.tsx | 128 -------- src/components/mod-loader-selector.tsx | 168 ---------- .../modals/change-mod-loader-modal.tsx | 4 +- .../modals/create-instance-modal.tsx | 63 +++- .../modals/import-modpack-modal.tsx | 5 +- src/locales/en.json | 23 +- src/locales/zh-Hans.json | 21 +- src/models/mock/resource.ts | 4 +- src/models/resource.ts | 6 + src/pages/instances/details/[id]/mods.tsx | 39 ++- src/services/instance.ts | 4 + src/services/resource.ts | 15 + 31 files changed, 654 insertions(+), 412 deletions(-) create mode 100644 public/images/icons/OptiFine.png create mode 100644 src-tauri/src/resource/helpers/loader_meta/optifine.rs create mode 100644 src/components/common/selectable-card.tsx create mode 100644 src/components/loader-selector.tsx delete mode 100644 src/components/mod-loader-cards.tsx delete mode 100644 src/components/mod-loader-selector.tsx diff --git a/package.json b/package.json index a9a96c50e..a2ccd98fe 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@tauri-apps/plugin-fs": "^2.4.0", "@tauri-apps/plugin-http": "^2.5.0", "@tauri-apps/plugin-log": "^2.6.0", - "@tauri-apps/plugin-opener": "^2.4.0", + "@tauri-apps/plugin-opener": "2.5.0", "@tauri-apps/plugin-os": "^2.3.0", "@tauri-apps/plugin-process": "^2.3.0", "@tauri-apps/plugin-window-state": "^2.3.0", diff --git a/public/images/icons/OptiFine.png b/public/images/icons/OptiFine.png new file mode 100644 index 0000000000000000000000000000000000000000..b67f5d4e1b406ab79e3e7b8a3f92d0aaacaa3c7b GIT binary patch literal 51040 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%0A2(*H44lB+yFH! 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�g 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 Bl6L?A literal 0 HcmV?d00001 diff --git a/src-tauri/src/instance/commands.rs b/src-tauri/src/instance/commands.rs index 441b444d5..fbb58f42f 100644 --- a/src-tauri/src/instance/commands.rs +++ b/src-tauri/src/instance/commands.rs @@ -5,6 +5,7 @@ use crate::instance::helpers::client_json::{replace_native_libraries, McClientIn use crate::instance::helpers::game_version::{build_game_version_cmp_fn, compare_game_versions}; use crate::instance::helpers::loader::common::{execute_processors, install_mod_loader}; use crate::instance::helpers::loader::forge::InstallProfile; +use crate::instance::helpers::loader::optifine::install_optifine; use crate::instance::helpers::misc::{ get_instance_game_config, get_instance_subdir_path_by_id, get_instance_subdir_paths, refresh_and_update_instances, unify_instance_name, @@ -36,7 +37,7 @@ use crate::launcher_config::models::{GameConfig, GameDirectory, LauncherConfig}; use crate::partial::{PartialError, PartialUpdate}; use crate::resource::helpers::misc::get_source_priority_list; use crate::resource::models::{ - GameClientResourceInfo, ModLoaderResourceInfo, OptifineResourceInfo, + GameClientResourceInfo, ModLoaderResourceInfo, OptiFineResourceInfo, }; use crate::storage::{load_json_async, save_json_async, Storage}; use crate::tasks::commands::schedule_progressive_task_group; @@ -864,6 +865,7 @@ pub async fn create_instance( icon_src: String, game: GameClientResourceInfo, mod_loader: ModLoaderResourceInfo, + optifine: Option, modpack_path: Option, is_install_fabric_api: Option, ) -> SJMCLResult<()> { @@ -899,8 +901,8 @@ pub async fn create_instance( }, version: mod_loader.version.clone(), branch: mod_loader.branch.clone(), - optifine: mod_loader.optifine.clone(), }, + optifine, description, icon_src, starred: false, @@ -987,6 +989,17 @@ pub async fn create_instance( .await?; } + if let Some(optifine_info) = &instance.optifine { + install_optifine( + &priority_list, + &instance.version, + optifine_info, + libraries_dir.to_path_buf(), + &mut task_params, + ) + .await?; + } + // If modpack path is provided, install it if let Some(modpack_path) = modpack_path { let path = PathBuf::from(modpack_path); @@ -1174,7 +1187,6 @@ pub async fn change_mod_loader( ModLoaderStatus::NotDownloaded }, branch: new_mod_loader.branch.clone(), - optifine: new_mod_loader.optifine.clone(), }; let game_version = instance.version.clone(); let subdirs = get_instance_subdir_paths( diff --git a/src-tauri/src/instance/helpers/client_json.rs b/src-tauri/src/instance/helpers/client_json.rs index 4e7a6f7ca..c04459869 100644 --- a/src-tauri/src/instance/helpers/client_json.rs +++ b/src-tauri/src/instance/helpers/client_json.rs @@ -1,7 +1,7 @@ use crate::error::{SJMCLError, SJMCLResult}; use crate::instance::models::misc::{Instance, ModLoaderType}; use crate::launcher_config::models::LauncherConfig; -use crate::resource::models::OptifineResourceInfo; +use crate::resource::models::OptiFineResourceInfo; use crate::utils::fs::get_app_resource_filepath; use regex::RegexBuilder; use serde::{Deserialize, Deserializer, Serialize}; @@ -310,13 +310,12 @@ pub fn patches_to_info( Option, Option, ModLoaderType, - Option, + Option, ) { let mut loader_type = ModLoaderType::Unknown; let mut game_version = None; let mut loader_version = None; - let mut optifine_info: Option = None; - println!("Patches_num: {}", patches.len()); + let mut optifine_info: Option = None; for patch in patches { if game_version.is_none() && patch.id == "game" { game_version = patch.version.clone(); @@ -328,7 +327,7 @@ pub fn patches_to_info( } } if patch.id == "optifine" { - optifine_info = Some(OptifineResourceInfo { + optifine_info = Some(OptiFineResourceInfo { patch: "".to_string(), filename: "".to_string(), r#type: patch.version.clone().unwrap_or_default(), @@ -344,7 +343,7 @@ pub async fn libraries_to_info( Option, Option, ModLoaderType, - Option, + Option, ) { let game_version: Option = client.client_version.clone(); let mut loader_version: Option = None; diff --git a/src-tauri/src/instance/helpers/loader/common.rs b/src-tauri/src/instance/helpers/loader/common.rs index 8614dda40..fd15ec343 100644 --- a/src-tauri/src/instance/helpers/loader/common.rs +++ b/src-tauri/src/instance/helpers/loader/common.rs @@ -60,18 +60,7 @@ pub async fn install_mod_loader( .await } ModLoaderType::Forge => { - install_forge_loader(priority, game_version, loader, lib_dir.clone(), task_params).await?; - if loader.optifine.is_some() { - install_optifine( - priority, - game_version, - loader.optifine.as_ref().unwrap(), - lib_dir.clone(), - task_params, - ) - .await?; - } - Ok(()) + install_forge_loader(priority, game_version, loader, lib_dir.clone(), task_params).await } ModLoaderType::NeoForge => { install_neoforge_loader(priority, loader, lib_dir, task_params).await diff --git a/src-tauri/src/instance/helpers/loader/optifine.rs b/src-tauri/src/instance/helpers/loader/optifine.rs index 45ecf2b0f..5533bf12a 100644 --- a/src-tauri/src/instance/helpers/loader/optifine.rs +++ b/src-tauri/src/instance/helpers/loader/optifine.rs @@ -1,42 +1,27 @@ -use reqwest::redirect::Policy; -use reqwest::{Client, Error}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::fs::{self, File}; -use std::io::Read; use std::path::PathBuf; -use tauri::AppHandle; -use tauri_plugin_http::reqwest; -use url::Url; -use zip::ZipArchive; use crate::error::SJMCLResult; -use crate::instance::helpers::client_json::{LaunchArgumentTemplate, LibrariesValue, McClientInfo}; -use crate::instance::helpers::loader::common::add_library_entry; -use crate::instance::helpers::misc::get_instance_subdir_paths; -use crate::instance::models::misc::{Instance, InstanceError, InstanceSubdirType, ModLoader}; use crate::launch::helpers::file_validator::convert_library_name_to_path; -use crate::resource::helpers::misc::{convert_url_to_target_source, get_download_api}; -use crate::resource::models::{OptifineResourceInfo, ResourceType, SourceType}; -use crate::tasks::commands::schedule_progressive_task_group; +use crate::resource::helpers::misc::get_download_api; +use crate::resource::models::{OptiFineResourceInfo, ResourceType, SourceType}; use crate::tasks::download::DownloadParam; use crate::tasks::PTaskParam; pub async fn install_optifine( priority: &[SourceType], game_version: &str, - optifine: &OptifineResourceInfo, + optifine: &OptiFineResourceInfo, lib_dir: PathBuf, task_params: &mut Vec, ) -> SJMCLResult<()> { - let root = get_download_api(priority[0], ResourceType::Optifine)?; + let root = get_download_api(priority[0], ResourceType::OptiFine)?; - let installer_url = match priority.first().unwrap_or(&SourceType::Official) { - &SourceType::Official => root.join(&format!( + let installer_url = match *priority.first().unwrap_or(&SourceType::Official) { + SourceType::Official => root.join(&format!( "{}/{}/{}", game_version, optifine.r#type, optifine.patch ))?, - &SourceType::BMCLAPIMirror => root.join(&format!( + SourceType::BMCLAPIMirror => root.join(&format!( "{}/{}/{}", game_version, optifine.r#type, optifine.patch ))?, diff --git a/src-tauri/src/instance/helpers/misc.rs b/src-tauri/src/instance/helpers/misc.rs index e40adb271..36178bfb8 100644 --- a/src-tauri/src/instance/helpers/misc.rs +++ b/src-tauri/src/instance/helpers/misc.rs @@ -256,9 +256,9 @@ pub async fn refresh_instances( version: loader_version.unwrap_or_default(), status: ModLoaderStatus::Installed, branch: None, - optifine: optifine_info.clone(), } }, + optifine: optifine_info.clone(), ..cfg_read }; // ignore error here, for now diff --git a/src-tauri/src/instance/models/misc.rs b/src-tauri/src/instance/models/misc.rs index bbbf88773..8d25c9c65 100644 --- a/src-tauri/src/instance/models/misc.rs +++ b/src-tauri/src/instance/models/misc.rs @@ -1,7 +1,7 @@ use crate::instance::constants::INSTANCE_CFG_FILE_NAME; use crate::instance::helpers::game_version::{compare_game_versions, get_major_game_version}; use crate::launcher_config::models::GameConfig; -use crate::resource::models::OptifineResourceInfo; +use crate::resource::models::OptiFineResourceInfo; use crate::storage::{load_json_async, save_json_async}; use crate::utils::image::ImageWrapper; use serde::{Deserialize, Serialize}; @@ -97,8 +97,8 @@ structstruck::strike! { pub loader_type: ModLoaderType, pub version: String, pub branch: Option, // Optional branch name for mod loaders like Forge - pub optifine: Option, }, + pub optifine: Option, // if true, use the spec_game_config, else use the global game config pub use_spec_game_config: bool, // if use_spec_game_config is false, this field is ignored diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 86ea217f9..e81adc8a6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -135,6 +135,7 @@ pub async fn run() { resource::commands::fetch_game_version_list, resource::commands::fetch_game_version_specific, resource::commands::fetch_mod_loader_version_list, + resource::commands::fetch_optifine_version_list, resource::commands::fetch_resource_list_by_name, resource::commands::fetch_resource_version_packs, resource::commands::download_game_server, diff --git a/src-tauri/src/resource/commands.rs b/src-tauri/src/resource/commands.rs index 50b8c4d19..3bcfd05dd 100644 --- a/src-tauri/src/resource/commands.rs +++ b/src-tauri/src/resource/commands.rs @@ -10,6 +10,7 @@ use crate::resource::helpers::curseforge::{ use crate::resource::helpers::loader_meta::fabric::get_fabric_meta_by_game_version; use crate::resource::helpers::loader_meta::forge::get_forge_meta_by_game_version; use crate::resource::helpers::loader_meta::neoforge::get_neoforge_meta_by_game_version; +use crate::resource::helpers::loader_meta::optifine::get_optifine_meta_by_game_version; use crate::resource::helpers::misc::get_source_priority_list; use crate::resource::helpers::modrinth::{ fetch_remote_resource_by_id_modrinth, fetch_remote_resource_by_local_modrinth, @@ -17,9 +18,9 @@ use crate::resource::helpers::modrinth::{ }; use crate::resource::helpers::version_manifest::get_game_version_manifest; use crate::resource::models::{ - GameClientResourceInfo, ModLoaderResourceInfo, ModUpdateQuery, OtherResourceFileInfo, - OtherResourceInfo, OtherResourceSearchQuery, OtherResourceSearchRes, OtherResourceSource, - OtherResourceVersionPack, OtherResourceVersionPackQuery, ResourceError, + GameClientResourceInfo, ModLoaderResourceInfo, ModUpdateQuery, OptiFineResourceInfo, + OtherResourceFileInfo, OtherResourceInfo, OtherResourceSearchQuery, OtherResourceSearchRes, + OtherResourceSource, OtherResourceVersionPack, OtherResourceVersionPackQuery, ResourceError, }; use crate::tasks::commands::schedule_progressive_task_group; use crate::tasks::download::DownloadParam; @@ -80,6 +81,19 @@ pub async fn fetch_mod_loader_version_list( } } +#[tauri::command] +pub async fn fetch_optifine_version_list( + app: AppHandle, + game_version: String, +) -> SJMCLResult> { + let priority_list = { + let launcher_config_state = app.state::>(); + let launcher_config = launcher_config_state.lock()?; + get_source_priority_list(&launcher_config) + }; + get_optifine_meta_by_game_version(&app, &priority_list, &game_version).await +} + #[tauri::command] pub async fn fetch_resource_list_by_name( app: AppHandle, diff --git a/src-tauri/src/resource/helpers/loader_meta/fabric.rs b/src-tauri/src/resource/helpers/loader_meta/fabric.rs index 32bc01e09..fb472d416 100644 --- a/src-tauri/src/resource/helpers/loader_meta/fabric.rs +++ b/src-tauri/src/resource/helpers/loader_meta/fabric.rs @@ -47,7 +47,6 @@ pub async fn get_fabric_meta_by_game_version( description: String::new(), stable: info.loader.stable, branch: None, - optifine: None, }) .collect(), ); diff --git a/src-tauri/src/resource/helpers/loader_meta/forge.rs b/src-tauri/src/resource/helpers/loader_meta/forge.rs index 32734985f..faf165e27 100644 --- a/src-tauri/src/resource/helpers/loader_meta/forge.rs +++ b/src-tauri/src/resource/helpers/loader_meta/forge.rs @@ -1,4 +1,4 @@ -use crate::error::{SJMCLError, SJMCLResult}; +use crate::error::SJMCLResult; use crate::instance::models::misc::ModLoaderType; use crate::resource::helpers::misc::get_download_api; use crate::resource::models::{ModLoaderResourceInfo, ResourceError, ResourceType, SourceType}; @@ -39,7 +39,6 @@ async fn get_forge_meta_by_game_version_bmcl( description: info.modified, stable: true, branch: info.branch.and_then(|v| v.as_str().map(String::from)), - optifine: None, }) .collect(), ) @@ -80,5 +79,5 @@ pub async fn get_forge_meta_by_game_version( } } } - Err(SJMCLError(String::new())) + Err(ResourceError::NoDownloadApi.into()) } diff --git a/src-tauri/src/resource/helpers/loader_meta/mod.rs b/src-tauri/src/resource/helpers/loader_meta/mod.rs index 2df19e58f..567cef5d4 100644 --- a/src-tauri/src/resource/helpers/loader_meta/mod.rs +++ b/src-tauri/src/resource/helpers/loader_meta/mod.rs @@ -1,3 +1,4 @@ pub mod fabric; pub mod forge; pub mod neoforge; +pub mod optifine; diff --git a/src-tauri/src/resource/helpers/loader_meta/neoforge.rs b/src-tauri/src/resource/helpers/loader_meta/neoforge.rs index 7ea3cb9fb..8601aaf26 100644 --- a/src-tauri/src/resource/helpers/loader_meta/neoforge.rs +++ b/src-tauri/src/resource/helpers/loader_meta/neoforge.rs @@ -82,7 +82,6 @@ async fn get_neoforge_meta_by_game_version_official( .get("is_snapshot") .is_none_or(|v| !v.as_bool().unwrap_or(false)), branch: None, - optifine: None, }, )); } @@ -148,7 +147,6 @@ async fn get_neoforge_meta_by_game_version_official( description: String::new(), stable, branch: None, - optifine: None, }, )); } @@ -201,7 +199,6 @@ async fn get_neoforge_meta_by_game_version_bmcl( description: String::new(), stable, branch: None, - optifine: None, } }) .collect(), diff --git a/src-tauri/src/resource/helpers/loader_meta/optifine.rs b/src-tauri/src/resource/helpers/loader_meta/optifine.rs new file mode 100644 index 000000000..4aef9ed3b --- /dev/null +++ b/src-tauri/src/resource/helpers/loader_meta/optifine.rs @@ -0,0 +1,43 @@ +use crate::error::SJMCLResult; +use crate::resource::helpers::misc::get_download_api; +use crate::resource::models::{OptiFineResourceInfo, ResourceError, ResourceType, SourceType}; +use tauri::Manager; +use tauri_plugin_http::reqwest; + +async fn get_optifine_meta_by_game_version_bmcl( + app: &tauri::AppHandle, + game_version: &str, +) -> SJMCLResult> { + let client = app.state::(); + let url = + get_download_api(SourceType::BMCLAPIMirror, ResourceType::OptiFine)?.join(game_version)?; + match client.get(url).send().await { + Ok(response) => { + if response.status().is_success() { + response + .json::>() + .await + .map_err(|_| ResourceError::ParseError.into()) + } else { + Err(ResourceError::NetworkError.into()) + } + } + Err(_) => Err(ResourceError::NetworkError.into()), + } +} + +pub async fn get_optifine_meta_by_game_version( + app: &tauri::AppHandle, + priority_list: &[SourceType], + game_version: &str, +) -> SJMCLResult> { + for source_type in priority_list.iter() { + match source_type { + SourceType::BMCLAPIMirror => { + return get_optifine_meta_by_game_version_bmcl(app, game_version).await; + } + _ => continue, + } + } + Err(ResourceError::NoDownloadApi.into()) +} diff --git a/src-tauri/src/resource/helpers/misc.rs b/src-tauri/src/resource/helpers/misc.rs index 5aeebb66f..834a87120 100644 --- a/src-tauri/src/resource/helpers/misc.rs +++ b/src-tauri/src/resource/helpers/misc.rs @@ -41,7 +41,7 @@ pub fn get_download_api(source: SourceType, resource_type: ResourceType) -> SJMC ResourceType::ForgeInstall => Ok(Url::parse("https://maven.minecraftforge.net/net/minecraftforge/forge/")?), ResourceType::ForgeMeta => Err(ResourceError::NoDownloadApi.into()), // https://github.com/HMCL-dev/HMCL/pull/3259/files ResourceType::Liteloader => Ok(Url::parse("https://dl.liteloader.com/versions/versions.json")?), - ResourceType::Optifine => Err(ResourceError::NoDownloadApi.into()), // + ResourceType::OptiFine => Err(ResourceError::NoDownloadApi.into()), // ResourceType::AuthlibInjector => Ok(Url::parse("https://authlib-injector.yushi.moe/")?), ResourceType::FabricMeta => Ok(Url::parse("https://meta.fabricmc.net/")?), ResourceType::FabricMaven => Ok(Url::parse("https://maven.fabricmc.net/")?), @@ -69,7 +69,7 @@ pub fn get_download_api(source: SourceType, resource_type: ResourceType) -> SJMC ResourceType::FabricMaven => Ok(Url::parse("https://bmclapi2.bangbang93.com/maven/")?), ResourceType::NeoforgeMetaForge | ResourceType::NeoforgeMetaNeoforge => Ok(Url::parse("https://bmclapi2.bangbang93.com/neoforge/")?), ResourceType::NeoforgeInstall => Ok(Url::parse("https://bmclapi2.bangbang93.com/neoforge/version/")?), - ResourceType::Optifine => Ok(Url::parse("https://bmclapi2.bangbang93.com/optifine/")?), + ResourceType::OptiFine => Ok(Url::parse("https://bmclapi2.bangbang93.com/optifine/")?), ResourceType::QuiltMaven => Ok(Url::parse("https://bmclapi2.bangbang93.com/maven/")?), ResourceType::QuiltMeta => Ok(Url::parse("https://bmclapi2.bangbang93.com/quilt-meta/")?), }, diff --git a/src-tauri/src/resource/models.rs b/src-tauri/src/resource/models.rs index da3c61bda..2db19c1ed 100644 --- a/src-tauri/src/resource/models.rs +++ b/src-tauri/src/resource/models.rs @@ -18,7 +18,7 @@ pub enum ResourceType { ForgeMavenNew, ForgeInstall, Liteloader, - Optifine, + OptiFine, AuthlibInjector, FabricMeta, FabricMaven, @@ -180,12 +180,11 @@ pub struct ModLoaderResourceInfo { pub description: String, pub stable: bool, pub branch: Option, - pub optifine: Option, } #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Default)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct OptifineResourceInfo { +#[serde(rename_all = "camelCase")] +pub struct OptiFineResourceInfo { pub filename: String, pub patch: String, pub r#type: String, diff --git a/src/components/common/selectable-card.tsx b/src/components/common/selectable-card.tsx new file mode 100644 index 000000000..0a7f6f1ca --- /dev/null +++ b/src/components/common/selectable-card.tsx @@ -0,0 +1,113 @@ +import { + Card, + CardProps, + Flex, + HStack, + Icon, + IconButton, + Image, + Text, + VStack, +} from "@chakra-ui/react"; +import { LuChevronRight, LuX } from "react-icons/lu"; +import { useLauncherConfig } from "@/contexts/config"; +import { useThemedCSSStyle } from "@/hooks/themed-css"; + +export interface SelectableCardProps extends CardProps { + title: string; + iconSrc?: string; + description?: string; + displayMode: "entry" | "selector"; + isLoading?: boolean; + isDisabled?: boolean; + isSelected: boolean; + isChevronShown?: boolean; + onSelect: () => void; + onCancel?: () => void; +} + +const SelectableCard: React.FC = ({ + title, + iconSrc, + description, + displayMode, + isLoading = false, + isDisabled = false, + isSelected, + isChevronShown = true, + onSelect, + onCancel, + ...boxProps +}) => { + const { config } = useLauncherConfig(); + const primaryColor = config.appearance.theme.primaryColor; + const themedStyles = useThemedCSSStyle(); + + const borderWidth = "1px"; + const basePadding = boxProps.padding || "12px"; + const selectedPadding = `calc(${basePadding} - ${borderWidth})`; + + return ( + + + + {!!iconSrc && ( + {title} + )} + + + {title} + + {!!description && ( + + {description} + + )} + + + + {displayMode === "selector" && isSelected && !!onCancel && ( + } + variant="ghost" + size="xs" + disabled={isLoading || isDisabled} + onClick={() => onCancel()} + /> + )} + {isChevronShown && ( + } + variant="ghost" + size="xs" + disabled={isLoading || isDisabled} + onClick={() => onSelect()} + /> + )} + + + + ); +}; + +export default SelectableCard; diff --git a/src/components/loader-selector.tsx b/src/components/loader-selector.tsx new file mode 100644 index 000000000..8b7ba684f --- /dev/null +++ b/src/components/loader-selector.tsx @@ -0,0 +1,309 @@ +import { + Center, + HStack, + Image, + Radio, + RadioGroup, + Tag, + VStack, +} from "@chakra-ui/react"; +import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { BeatLoader } from "react-spinners"; +import Empty from "@/components/common/empty"; +import { + OptionItemProps, + VirtualOptionItemGroup, +} from "@/components/common/option-item-virtual"; +import { Section } from "@/components/common/section"; +import SelectableCard, { + SelectableCardProps, +} from "@/components/common/selectable-card"; +import { useLauncherConfig } from "@/contexts/config"; +import { useToast } from "@/contexts/toast"; +import { ModLoaderType } from "@/enums/instance"; +import { + GameClientResourceInfo, + ModLoaderResourceInfo, + OptiFineResourceInfo, + defaultModLoaderResourceInfo, +} from "@/models/resource"; +import { ResourceService } from "@/services/resource"; +import { ISOToDatetime } from "@/utils/datetime"; + +export const modLoaderTypes: ModLoaderType[] = [ + ModLoaderType.Forge, + ModLoaderType.Fabric, + ModLoaderType.NeoForge, +]; + +export const modLoaderTypesToIcon: Record = { + Unknown: "", + Fabric: "Fabric.png", + Forge: "Forge.png", + NeoForge: "NeoForge.png", +}; + +interface LoaderSelectorProps { + selectedGameVersion: GameClientResourceInfo; + selectedModLoader: ModLoaderResourceInfo; + onSelectModLoader: (v: ModLoaderResourceInfo) => void; + selectedOptiFine?: OptiFineResourceInfo | undefined; + onSelectOptiFine?: (v: OptiFineResourceInfo | undefined) => void; +} + +export const LoaderSelector: React.FC = ({ + selectedGameVersion, + selectedModLoader, + onSelectModLoader, + selectedOptiFine, + onSelectOptiFine, + ...props +}) => { + const { t } = useTranslation(); + const { config } = useLauncherConfig(); + const toast = useToast(); + const primaryColor = config.appearance.theme.primaryColor; + const [versionList, setVersionList] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [selectedType, setSelectedType] = useState( + ModLoaderType.Unknown + ); + const [selectedId, setSelectedId] = useState(""); + + function isModLoaderResourceInfo( + version: ModLoaderResourceInfo | OptiFineResourceInfo + ): version is ModLoaderResourceInfo { + return (version as ModLoaderResourceInfo).loaderType !== undefined; + } + + const buildOptionItems = useCallback( + ( + version: ModLoaderResourceInfo | OptiFineResourceInfo + ): OptionItemProps => { + let title = isModLoaderResourceInfo(version) + ? version.version + : version?.filename; + return { + title, + description: isModLoaderResourceInfo(version) && version.description, + prefixElement: ( + + + {title} + + ), + titleExtra: ( + + {t( + `LoaderSelector.${(isModLoaderResourceInfo(version) ? version.stable : !version.patch.startsWith("pre")) ? "stable" : "beta"}` + )} + + ), + children: <>, + isFullClickZone: true, + onClick: () => { + if (isModLoaderResourceInfo(version)) { + onSelectModLoader(version); + } else { + onSelectOptiFine?.(version); + } + setSelectedId(title); + }, + }; + }, + [primaryColor, t, onSelectModLoader, onSelectOptiFine] + ); + + const handleFetchModLoaderVersionList = useCallback( + (type: ModLoaderType) => { + setIsLoading(true); + ResourceService.fetchModLoaderVersionList(selectedGameVersion.id, type) + .then((res) => { + if (res.status === "success") { + setVersionList( + res.data + .map((loader) => ({ + ...loader, + description: + loader.description && + t("LoaderSelector.releaseDate", { + date: ISOToDatetime(loader.description), + }), + })) + .map(buildOptionItems) + ); + } else { + setVersionList([]); + toast({ + status: "error", + title: res.message, + description: res.details, + }); + } + }) + .finally(() => setIsLoading(false)); + }, + [selectedGameVersion.id, buildOptionItems, t, toast] + ); + + const handleFetchOptiFineVersionList = useCallback(() => { + setIsLoading(true); + ResourceService.fetchOptiFineVersionList(selectedGameVersion.id) + .then((res) => { + if (res.status === "success") { + setVersionList(res.data.map(buildOptionItems)); + } else { + setVersionList([]); + toast({ + status: "error", + title: res.message, + description: res.details, + }); + } + }) + .finally(() => setIsLoading(false)); + }, [selectedGameVersion.id, buildOptionItems, toast]); + + let selectableCardItems = modLoaderTypes.map( + (type): SelectableCardProps => ({ + title: type, + iconSrc: `/images/icons/${modLoaderTypesToIcon[type]}`, + description: + selectedModLoader.loaderType !== ModLoaderType.Unknown + ? selectedModLoader.loaderType === type + ? selectedModLoader.version || t("LoaderSelector.noVersionSelected") + : t("LoaderSelector.notCompatibleWith", { + item: selectedModLoader.loaderType, + }) + : t("LoaderSelector.noVersionSelected"), + displayMode: "selector", + isLoading, + isSelected: type === selectedModLoader.loaderType, + isChevronShown: selectedType !== type, + onSelect: () => { + setSelectedType(type); + if (selectedModLoader.loaderType !== type) { + onSelectModLoader({ + loaderType: type, + version: "", + description: "", + stable: false, + }); + setSelectedId(""); + } else { + setSelectedId(selectedModLoader.version); + } + if ( + type !== ModLoaderType.Forge || + (selectedOptiFine && !selectedOptiFine.filename) + ) { + // When OptiFine is not compatible with the selected mod loader, or selected without a version, clear it + onSelectOptiFine?.(undefined); + } + }, + onCancel: () => { + if (selectedType === type) { + setSelectedType(ModLoaderType.Unknown); + setSelectedId(""); + } + onSelectModLoader(defaultModLoaderResourceInfo); + }, + }) + ); + + if (typeof onSelectOptiFine === "function") { + selectableCardItems.push({ + title: "OptiFine", + iconSrc: "/images/icons/OptiFine.png", + description: selectedOptiFine + ? selectedOptiFine.type + " " + selectedOptiFine.patch + : selectedModLoader.loaderType === ModLoaderType.Forge || + selectedModLoader.loaderType === ModLoaderType.Unknown + ? t("LoaderSelector.noVersionSelected") + : t("LoaderSelector.notCompatibleWith", { + item: selectedModLoader.loaderType, + }), + displayMode: "selector", + isLoading, + isSelected: !!selectedOptiFine, + isDisabled: !( + selectedModLoader.loaderType === ModLoaderType.Forge || + selectedModLoader.loaderType === ModLoaderType.Unknown + ), + isChevronShown: selectedType !== "OptiFine", + onSelect: () => { + setSelectedType("OptiFine"); + if (!selectedOptiFine) { + onSelectOptiFine?.({ + filename: "", + patch: "", + type: "", + }); + setSelectedId(""); + } else { + setSelectedId(selectedOptiFine.filename); + } + + if ( + selectedModLoader.loaderType !== ModLoaderType.Unknown && + !selectedModLoader.version + ) { + // When some mod loader was selected without a version, clear it + onSelectModLoader(defaultModLoaderResourceInfo); + } + }, + onCancel: () => { + if (selectedType === "OptiFine") { + setSelectedType(ModLoaderType.Unknown); + setSelectedId(""); + } + onSelectOptiFine?.(undefined); + }, + }); + } + + useEffect(() => { + if (selectedType === "OptiFine") { + handleFetchOptiFineVersionList(); + } else if (selectedType !== ModLoaderType.Unknown) { + handleFetchModLoaderVersionList(selectedType); + } else { + setVersionList([]); + } + }, [ + handleFetchModLoaderVersionList, + handleFetchOptiFineVersionList, + selectedType, + ]); + + return ( + + + {selectableCardItems.map((item, index) => ( + + ))} + +
+ {isLoading ? ( +
+ +
+ ) : versionList.length === 0 ? ( +
+ +
+ ) : ( + + + + )} +
+
+ ); +}; diff --git a/src/components/mod-loader-cards.tsx b/src/components/mod-loader-cards.tsx deleted file mode 100644 index cd56c62a9..000000000 --- a/src/components/mod-loader-cards.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { - BoxProps, - Card, - Flex, - Grid, - HStack, - Icon, - IconButton, - Image, - Text, - VStack, -} from "@chakra-ui/react"; -import { useTranslation } from "react-i18next"; -import { LuChevronRight, LuX } from "react-icons/lu"; -import { useLauncherConfig } from "@/contexts/config"; -import { ModLoaderType } from "@/enums/instance"; -import { useThemedCSSStyle } from "@/hooks/themed-css"; -import { parseModLoaderVersion } from "@/utils/instance"; - -interface ModLoaderCardsProps extends BoxProps { - currentType: ModLoaderType; - currentVersion?: string; - displayMode: "entry" | "selector"; - loading?: boolean; - onTypeSelect?: (type: ModLoaderType) => void; -} - -const ModLoaderCards: React.FC = ({ - currentType, - currentVersion, - displayMode, - loading = false, - onTypeSelect, - ...boxProps -}) => { - const { t } = useTranslation(); - const { config } = useLauncherConfig(); - const primaryColor = config.appearance.theme.primaryColor; - const themedStyles = useThemedCSSStyle(); - - const borderWidth = "1px"; - const basePadding = boxProps.padding || "12px"; - const selectedPadding = `calc(${basePadding} - ${borderWidth})`; - - const loaderTypes: ModLoaderType[] = [ - ModLoaderType.Fabric, - ModLoaderType.Forge, - ModLoaderType.NeoForge, - ]; - - const renderCard = (type: ModLoaderType) => { - const isSelected = - type === currentType && currentType !== ModLoaderType.Unknown; - - return ( - - - - {type} - - - {type} - - - {displayMode === "entry" - ? isSelected - ? `${t("ModLoaderCards.installed")} ${parseModLoaderVersion(currentVersion || "")}` - : t("ModLoaderCards.unInstalled") - : isSelected - ? currentVersion || t("ModLoaderCards.versionNotSelected") - : currentType === ModLoaderType.Unknown - ? t("ModLoaderCards.versionNotSelected") - : t("ModLoaderCards.notCompatibleWith", { - modLoader: currentType, - })} - - - - - } - variant="ghost" - size="xs" - disabled={loading} - onClick={() => onTypeSelect?.(type)} - /> - - - ); - }; - - return ( - <> - - {loaderTypes.map(renderCard)} - - - ); -}; - -export default ModLoaderCards; diff --git a/src/components/mod-loader-selector.tsx b/src/components/mod-loader-selector.tsx deleted file mode 100644 index fa4f42767..000000000 --- a/src/components/mod-loader-selector.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { - Center, - HStack, - Image, - Radio, - RadioGroup, - Tag, - VStack, -} from "@chakra-ui/react"; -import { useCallback, useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { BeatLoader } from "react-spinners"; -import Empty from "@/components/common/empty"; -import { - OptionItemProps, - VirtualOptionItemGroup, -} from "@/components/common/option-item-virtual"; -import { Section } from "@/components/common/section"; -import ModLoaderCards from "@/components/mod-loader-cards"; -import { useLauncherConfig } from "@/contexts/config"; -import { - GameClientResourceInfo, - ModLoaderResourceInfo, - defaultModLoaderResourceInfo, -} from "@/models/resource"; -import { ResourceService } from "@/services/resource"; -import { ISOToDatetime } from "@/utils/datetime"; - -const modLoaderTypesToIcon: Record = { - Unknown: "", - Fabric: "Fabric.png", - Forge: "Forge.png", - NeoForge: "NeoForge.png", -}; - -interface ModLoaderSelectorProps { - selectedGameVersion: GameClientResourceInfo; - selectedModLoader: ModLoaderResourceInfo; - onSelectModLoader: (v: ModLoaderResourceInfo) => void; -} - -export const ModLoaderSelector: React.FC = ({ - selectedGameVersion, - selectedModLoader, - onSelectModLoader, - ...props -}) => { - const { t } = useTranslation(); - const { config } = useLauncherConfig(); - const primaryColor = config.appearance.theme.primaryColor; - const [modLoaders, setModLoaders] = useState([]); - const [loading, setLoading] = useState(false); - - useEffect(() => { - setLoading(true); - ResourceService.fetchModLoaderVersionList( - selectedGameVersion.id, - selectedModLoader.loaderType - ) - .then((res) => { - if (res.status === "success") { - setModLoaders( - res.data.map((loader) => ({ - ...loader, - description: - loader.description && - t("ModLoaderSelector.releaseDate", { - date: ISOToDatetime(loader.description), - }), - })) - ); - } else { - setModLoaders([]); - } - }) - .finally(() => setLoading(false)); - }, [selectedGameVersion.id, selectedModLoader.loaderType, t]); - - const onSelectModLoaderVersion = useCallback( - (version: string) => { - if (version === selectedModLoader.version) { - onSelectModLoader(defaultModLoaderResourceInfo); - } else { - let _modLoader = modLoaders.filter( - (loader) => loader.version === version - )[0]; - onSelectModLoader(_modLoader); - } - }, - [modLoaders, onSelectModLoader, selectedModLoader.version] - ); - - const buildOptionItems = useCallback( - (version: ModLoaderResourceInfo): OptionItemProps => ({ - title: version.version, - description: version.description, - prefixElement: ( - - - {version.loaderType} - - ), - titleExtra: ( - - {t(`ModLoaderSelector.${version.stable ? "stable" : "beta"}`)} - - ), - children: <>, - isFullClickZone: true, - onClick: () => { - if (version.version !== "") { - onSelectModLoader(version); - } - }, - }), - [primaryColor, t, onSelectModLoader] - ); - - return ( - - { - if (loaderType !== selectedModLoader.loaderType) { - onSelectModLoader({ - loaderType, - version: "", - description: "", - stable: false, - }); - } else { - onSelectModLoader(defaultModLoaderResourceInfo); - } - }} - w="100%" - /> - -
- {loading ? ( -
- -
- ) : modLoaders.length === 0 ? ( - - ) : ( - - - - )} -
-
- ); -}; diff --git a/src/components/modals/change-mod-loader-modal.tsx b/src/components/modals/change-mod-loader-modal.tsx index b5b4b63d1..ae5c57101 100644 --- a/src/components/modals/change-mod-loader-modal.tsx +++ b/src/components/modals/change-mod-loader-modal.tsx @@ -21,7 +21,7 @@ import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { LuArrowRight } from "react-icons/lu"; import { OptionItem } from "@/components/common/option-item"; -import { ModLoaderSelector } from "@/components/mod-loader-selector"; +import { LoaderSelector } from "@/components/loader-selector"; import { useLauncherConfig } from "@/contexts/config"; import { useInstanceSharedData } from "@/contexts/instance"; import { useToast } from "@/contexts/toast"; @@ -197,7 +197,7 @@ export const ChangeModLoaderModal: React.FC = ({ )} {summary?.version && ( - = { april_fools: "/images/icons/YellowGlazedTerracotta.png", }; -export const modLoaderTypesToIcon: Record = { +export const loaderTypesToIcon: Record = { Unknown: "", Fabric: "/images/icons/Fabric.png", Forge: "/images/icons/Anvil.png", // differ from that in mod-loader-selector NeoForge: "/images/icons/NeoForge.png", + OptiFine: "/images/icons/OptiFine.png", }; export const CreateInstanceModal: React.FC> = ({ @@ -74,6 +76,9 @@ export const CreateInstanceModal: React.FC> = ({ useState(); const [selectedModLoader, setSelectedModLoader] = useState(defaultModLoaderResourceInfo); + const [selectedOptiFine, setSelectedOptiFine] = useState< + OptiFineResourceInfo | undefined + >(undefined); const [instanceName, setInstanceName] = useState(""); const [instanceDescription, setInstanceDescription] = useState(""); const [instanceIconSrc, setInstanceIconSrc] = useState(""); @@ -102,6 +107,7 @@ export const CreateInstanceModal: React.FC> = ({ instanceIconSrc, selectedGameVersion, selectedModLoader, + selectedOptiFine, undefined, // modpackPath isInstallFabricApi ) @@ -126,10 +132,11 @@ export const CreateInstanceModal: React.FC> = ({ instanceDescription, instanceIconSrc, selectedModLoader, + selectedOptiFine, isInstallFabricApi, - toast, modalProps, router, + toast, ]); const step1Content = useMemo(() => { @@ -164,10 +171,12 @@ export const CreateInstanceModal: React.FC> = ({ selectedGameVersion && ( <> - @@ -198,7 +207,8 @@ export const CreateInstanceModal: React.FC> = ({ colorScheme={primaryColor} onClick={() => { if (!selectedModLoader.version) { - setSelectedModLoader(defaultModLoaderResourceInfo); // if the user selected the loader but did not choose a version from the list + // if the user selected the loader but did not choose a version from the list + setSelectedModLoader(defaultModLoaderResourceInfo); setInstanceName(selectedGameVersion.id); setInstanceIconSrc( gameTypesToIcon[selectedGameVersion.gameType] @@ -208,9 +218,19 @@ export const CreateInstanceModal: React.FC> = ({ `${selectedGameVersion.id}-${selectedModLoader.loaderType}` ); setInstanceIconSrc( - modLoaderTypesToIcon[selectedModLoader.loaderType] + loaderTypesToIcon[selectedModLoader.loaderType] ); } + + if (selectedOptiFine) { + if (!selectedOptiFine.filename) { + // if the user selected OptiFine but did not choose a version from the list + setSelectedOptiFine(undefined); + } else { + setInstanceName((prev) => `${prev}-OptiFine`); + setInstanceIconSrc(loaderTypesToIcon["OptiFine"]); + } + } setActiveStep(2); }} > @@ -222,13 +242,14 @@ export const CreateInstanceModal: React.FC> = ({ ) ); }, [ - modalProps.onClose, - primaryColor, selectedGameVersion, selectedModLoader, + selectedOptiFine, + primaryColor, isInstallFabricApi, - setActiveStep, t, + modalProps.onClose, + setActiveStep, ]); const step3Content = useMemo(() => { @@ -289,10 +310,21 @@ export const CreateInstanceModal: React.FC> = ({ { key: "loader", content: step2Content, - description: - selectedModLoader.loaderType === ModLoaderType.Unknown - ? t("CreateInstanceModal.stepper.skipped") - : `${selectedModLoader.loaderType} ${selectedModLoader.version}`, + description: (() => { + if (selectedModLoader.loaderType === ModLoaderType.Unknown) { + return selectedOptiFine + ? "OptiFine" + : t("LoaderSelector.noVersionSelected"); + } else { + let desc = `${selectedModLoader.loaderType} ${ + selectedModLoader.version || t("LoaderSelector.noVersionSelected") + }`; + if (selectedOptiFine) { + desc += ` + OptiFine`; + } + return desc; + } + })(), }, { key: "info", @@ -302,12 +334,13 @@ export const CreateInstanceModal: React.FC> = ({ ], [ step1Content, + selectedGameVersion, + t, step2Content, step3Content, - selectedGameVersion, selectedModLoader.loaderType, selectedModLoader.version, - t, + selectedOptiFine, ] ); diff --git a/src/components/modals/import-modpack-modal.tsx b/src/components/modals/import-modpack-modal.tsx index f0f7e3353..5472490d8 100644 --- a/src/components/modals/import-modpack-modal.tsx +++ b/src/components/modals/import-modpack-modal.tsx @@ -27,7 +27,7 @@ import { import { InstanceIconSelectorPopover } from "@/components/instance-icon-selector"; import { gameTypesToIcon, - modLoaderTypesToIcon, + loaderTypesToIcon, } from "@/components/modals/create-instance-modal"; import { useLauncherConfig } from "@/contexts/config"; import { useToast } from "@/contexts/toast"; @@ -220,6 +220,7 @@ const ImportModpackModal: React.FC = ({ stable: true, } as ModLoaderResourceInfo) : defaultModLoaderResourceInfo, + undefined, path ); if (createResp.status === "success") { @@ -260,7 +261,7 @@ const ImportModpackModal: React.FC = ({ setDescription(response.data.description || ""); setIconSrc( response.data.modLoader - ? modLoaderTypesToIcon[response.data.modLoader.loaderType] + ? loaderTypesToIcon[response.data.modLoader.loaderType] : gameTypesToIcon["release"] ); } else { diff --git a/src/locales/en.json b/src/locales/en.json index 610df5055..ac2eb5a53 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -414,7 +414,7 @@ }, "stepper": { "game": "Game Client", - "loader": "Mod Loader", + "loader": "Game Loader", "skipped": "Skipped", "info": "Basic Settings" }, @@ -1196,7 +1196,8 @@ }, "InstanceModsPage": { "modLoaderList": { - "title": "Mod Loader" + "title": "Mod Loader", + "notInstalled": "Not Installed" }, "modList": { "title": "Mods", @@ -1359,6 +1360,13 @@ "noSelectedPlayer": "Please add and select a player first" } }, + "LoaderSelector": { + "stable": "Stable", + "beta": "Beta", + "releaseDate": "Released at {{date}}", + "notCompatibleWith":"Not compatible with {{item}}", + "noVersionSelected":"No version selected" + }, "ManageSkinModal": { "skinManage": "Manage Skin", "default": "Default", @@ -1400,17 +1408,6 @@ "MenuSelector": { "selectedCount": "{{count}} selected" }, - "ModLoaderCards": { - "installed": "Installed", - "unInstalled": "Not Installed", - "notCompatibleWith": "Not compatible with {{modLoader}}", - "versionNotSelected": "No version selected" - }, - "ModLoaderSelector": { - "stable": "Stable", - "beta": "Beta", - "releaseDate": "Released at {{date}}" - }, "NotFoundPage": { "text": "Page not found, redirecting to launch page in {{seconds}} seconds..." }, diff --git a/src/locales/zh-Hans.json b/src/locales/zh-Hans.json index e2fbce3cb..1b43958f3 100644 --- a/src/locales/zh-Hans.json +++ b/src/locales/zh-Hans.json @@ -1196,7 +1196,8 @@ }, "InstanceModsPage": { "modLoaderList": { - "title": "加载器" + "title": "加载器", + "notInstalled": "未安装" }, "modList": { "title": "模组", @@ -1359,6 +1360,13 @@ "noSelectedPlayer": "请先添加并选择游戏角色" } }, + "LoaderSelector": { + "stable": "正式版", + "beta": "测试版", + "releaseDate": "发布于 {{date}}", + "notCompatibleWith": "与 {{item}} 不兼容", + "noVersionSelected": "未选择版本" + }, "ManageSkinModal": { "skinManage": "管理皮肤", "default": "默认", @@ -1400,17 +1408,6 @@ "MenuSelector": { "selectedCount": "已选 {{count}} 项" }, - "ModLoaderCards": { - "installed": "已安装", - "unInstalled": "未安装", - "notCompatibleWith": "与 {{modLoader}} 不兼容", - "versionNotSelected": "未选择版本" - }, - "ModLoaderSelector": { - "stable": "正式版", - "beta": "测试版", - "releaseDate": "发布于 {{date}}" - }, "NotFoundPage": { "text": "页面不存在,即将在 {{seconds}} 秒后跳转" }, diff --git a/src/models/mock/resource.ts b/src/models/mock/resource.ts index e4398a359..54f79576b 100644 --- a/src/models/mock/resource.ts +++ b/src/models/mock/resource.ts @@ -543,7 +543,7 @@ export const mockDownloadResourceList: OtherResourceInfo[] = [ }, { type: OtherResourceType.Mod, - name: "Optifine", + name: "OptiFine", description: "A Minecraft mod that optimizes Minecraft's graphics.", iconSrc: "/images/icons/GrassBlock.png", tags: ["Graphics", "Optimization"], @@ -589,7 +589,7 @@ export const mockDownloadResourceList: OtherResourceInfo[] = [ }, { type: OtherResourceType.Mod, - name: "Optifine", + name: "OptiFine", description: "A Minecraft mod that optimizes Minecraft's graphics.", iconSrc: "/images/icons/GrassBlock.png", tags: ["Graphics", "Optimization"], diff --git a/src/models/resource.ts b/src/models/resource.ts index 9fbd4f4c2..ad729b6db 100644 --- a/src/models/resource.ts +++ b/src/models/resource.ts @@ -74,6 +74,12 @@ export const defaultModLoaderResourceInfo: ModLoaderResourceInfo = { stable: true, }; +export interface OptiFineResourceInfo { + filename: string; + patch: string; + type: string; +} + export interface ModUpdateRecord { name: string; curVersion: string; diff --git a/src/pages/instances/details/[id]/mods.tsx b/src/pages/instances/details/[id]/mods.tsx index d21d6f78e..4e6bc6151 100644 --- a/src/pages/instances/details/[id]/mods.tsx +++ b/src/pages/instances/details/[id]/mods.tsx @@ -27,7 +27,13 @@ import CountTag from "@/components/common/count-tag"; import Empty from "@/components/common/empty"; import { OptionItem, OptionItemGroup } from "@/components/common/option-item"; import { Section } from "@/components/common/section"; -import ModLoaderCards from "@/components/mod-loader-cards"; +import SelectableCard, { + SelectableCardProps, +} from "@/components/common/selectable-card"; +import { + modLoaderTypes, + modLoaderTypesToIcon, +} from "@/components/loader-selector"; import { ChangeModLoaderModal } from "@/components/modals/change-mod-loader-modal"; import CheckModUpdateModal from "@/components/modals/check-mod-update-modal"; import ModInfoModal from "@/components/modals/mod-info-modal"; @@ -343,6 +349,20 @@ const InstanceModsPage = () => { }, ]; + const selectableCardItems = modLoaderTypes.map( + (type): SelectableCardProps => ({ + title: type, + iconSrc: `/images/icons/${modLoaderTypesToIcon[type]}`, + description: + summary?.modLoader.loaderType === type + ? summary?.modLoader.version + : t("InstanceModsPage.modLoaderList.notInstalled"), + displayMode: "entry", + isSelected: summary?.modLoader.loaderType === type, + onSelect: () => handleTypeSelect(type), + }) + ); + return ( <>
{ ); }} > - + + {selectableCardItems.map((item, index) => ( + + ))} +
>} @@ -54,6 +56,7 @@ export class InstanceService { iconSrc: string, game: GameClientResourceInfo, modLoader: ModLoaderResourceInfo, + optifine?: OptiFineResourceInfo, modpackPath?: string, isInstallFabricApi?: boolean ): Promise> { @@ -64,6 +67,7 @@ export class InstanceService { iconSrc, game, modLoader, + optifine, modpackPath, isInstallFabricApi, }); diff --git a/src/services/resource.ts b/src/services/resource.ts index d8d982c8e..b978fa972 100644 --- a/src/services/resource.ts +++ b/src/services/resource.ts @@ -6,6 +6,7 @@ import { GameClientResourceInfo, ModLoaderResourceInfo, ModUpdateQuery, + OptiFineResourceInfo, OtherResourceFileInfo, OtherResourceInfo, OtherResourceSearchRes, @@ -43,6 +44,8 @@ export class ResourceService { /** * FETCH the list of mode loader versions. + * @param {string} gameVersion - The game version to fetch mod loaders for. + * @param {ModLoaderType} modLoaderType - The type of mod loader to fetch. * @returns {Promise>} */ @responseHandler("resource") @@ -56,6 +59,18 @@ export class ResourceService { }); } + /** + * FETCH the list of OptiFine versions. + * @param {string} gameVersion - The game version to fetch OptiFine versions for. + * @returns {Promise>} + */ + @responseHandler("resource") + static async fetchOptiFineVersionList( + gameVersion: string + ): Promise> { + return await invoke("fetch_optifine_version_list", { gameVersion }); + } + /** * FETCH the list of resources according to the given parameters. * @returns {Promise>} From db31ecddab6120aed12c560af168b8f346ea03f7 Mon Sep 17 00:00:00 2001 From: Reqwey Date: Wed, 24 Dec 2025 15:29:23 +0800 Subject: [PATCH 03/12] fix(instance): initial state loss in loader selector --- src/components/loader-selector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/loader-selector.tsx b/src/components/loader-selector.tsx index 8b7ba684f..6c1e00fa7 100644 --- a/src/components/loader-selector.tsx +++ b/src/components/loader-selector.tsx @@ -67,7 +67,7 @@ export const LoaderSelector: React.FC = ({ const [versionList, setVersionList] = useState([]); const [isLoading, setIsLoading] = useState(false); const [selectedType, setSelectedType] = useState( - ModLoaderType.Unknown + selectedOptiFine ? "OptiFine" : selectedModLoader.loaderType ); const [selectedId, setSelectedId] = useState(""); From f9dd3962d6e9ef95d90ad5ea00d3f0e92c5e614e Mon Sep 17 00:00:00 2001 From: Reqwey Date: Wed, 24 Dec 2025 16:03:35 +0800 Subject: [PATCH 04/12] fix(instance): change mod loader modal's selector do not switch properly --- src/components/loader-selector.tsx | 13 +++++++++- .../modals/change-mod-loader-modal.tsx | 25 ++++++++++--------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/components/loader-selector.tsx b/src/components/loader-selector.tsx index 6c1e00fa7..2e97b688d 100644 --- a/src/components/loader-selector.tsx +++ b/src/components/loader-selector.tsx @@ -67,10 +67,21 @@ export const LoaderSelector: React.FC = ({ const [versionList, setVersionList] = useState([]); const [isLoading, setIsLoading] = useState(false); const [selectedType, setSelectedType] = useState( - selectedOptiFine ? "OptiFine" : selectedModLoader.loaderType + ModLoaderType.Unknown ); const [selectedId, setSelectedId] = useState(""); + useEffect(() => { + if (selectedOptiFine) { + setSelectedType("OptiFine"); + setSelectedId(selectedOptiFine ? selectedOptiFine.filename : ""); + } else { + setSelectedType(selectedModLoader.loaderType); + setSelectedId(selectedModLoader.version); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + function isModLoaderResourceInfo( version: ModLoaderResourceInfo | OptiFineResourceInfo ): version is ModLoaderResourceInfo { diff --git a/src/components/modals/change-mod-loader-modal.tsx b/src/components/modals/change-mod-loader-modal.tsx index ae5c57101..44b6c2590 100644 --- a/src/components/modals/change-mod-loader-modal.tsx +++ b/src/components/modals/change-mod-loader-modal.tsx @@ -196,18 +196,19 @@ export const ChangeModLoaderModal: React.FC = ({ )} - {summary?.version && ( - - )} + {summary?.version && + selectedModLoader.loaderType !== ModLoaderType.Unknown && ( + + )} {selectedModLoader.loaderType === ModLoaderType.Fabric && ( From 2441ecb7a254563446f82afb1a5dd0a92015a4d3 Mon Sep 17 00:00:00 2001 From: Reqwey Date: Wed, 24 Dec 2025 16:24:59 +0800 Subject: [PATCH 05/12] chore: add optifine icon & revert package json --- package.json | 2 +- src/components/instance-icon-selector.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index a2ccd98fe..a9a96c50e 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@tauri-apps/plugin-fs": "^2.4.0", "@tauri-apps/plugin-http": "^2.5.0", "@tauri-apps/plugin-log": "^2.6.0", - "@tauri-apps/plugin-opener": "2.5.0", + "@tauri-apps/plugin-opener": "^2.4.0", "@tauri-apps/plugin-os": "^2.3.0", "@tauri-apps/plugin-process": "^2.3.0", "@tauri-apps/plugin-window-state": "^2.3.0", diff --git a/src/components/instance-icon-selector.tsx b/src/components/instance-icon-selector.tsx index 3ec2d070e..1fd052792 100644 --- a/src/components/instance-icon-selector.tsx +++ b/src/components/instance-icon-selector.tsx @@ -140,6 +140,7 @@ export const InstanceIconSelector: React.FC = ({ "/images/icons/Fabric.png", "/images/icons/Anvil.png", "/images/icons/NeoForge.png", + "/images/icons/OptiFine.png", ...(instanceId ? [ , From eabe83eb5230d2f603f8be53e0c180093888d63f Mon Sep 17 00:00:00 2001 From: xunying123 Date: Wed, 31 Dec 2025 05:04:13 +0800 Subject: [PATCH 06/12] feat(instance): support install modern optifine --- src-tauri/src/instance/commands.rs | 45 +- .../src/instance/helpers/loader/forge.rs | 18 +- .../src/instance/helpers/loader/neoforge.rs | 11 +- .../src/instance/helpers/loader/optifine.rs | 396 +++++++++++++++++- src-tauri/src/instance/helpers/misc.rs | 104 ++++- src-tauri/src/instance/models/misc.rs | 12 +- src/contexts/task.tsx | 1 + 7 files changed, 536 insertions(+), 51 deletions(-) diff --git a/src-tauri/src/instance/commands.rs b/src-tauri/src/instance/commands.rs index fbb58f42f..96f9a6ee7 100644 --- a/src-tauri/src/instance/commands.rs +++ b/src-tauri/src/instance/commands.rs @@ -5,7 +5,7 @@ use crate::instance::helpers::client_json::{replace_native_libraries, McClientIn use crate::instance::helpers::game_version::{build_game_version_cmp_fn, compare_game_versions}; use crate::instance::helpers::loader::common::{execute_processors, install_mod_loader}; use crate::instance::helpers::loader::forge::InstallProfile; -use crate::instance::helpers::loader::optifine::install_optifine; +use crate::instance::helpers::loader::optifine::{finish_optifine_installer, install_optifine}; use crate::instance::helpers::misc::{ get_instance_game_config, get_instance_subdir_path_by_id, get_instance_subdir_paths, refresh_and_update_instances, unify_instance_name, @@ -27,7 +27,8 @@ use crate::instance::helpers::server::{ use crate::instance::helpers::world::{load_level_data_from_nbt, load_world_info_from_dir}; use crate::instance::models::misc::{ Instance, InstanceError, InstanceSubdirType, InstanceSummary, LocalModInfo, ModLoader, - ModLoaderStatus, ModLoaderType, ResourcePackInfo, SchematicInfo, ScreenshotInfo, ShaderPackInfo, + ModLoaderStatus, ModLoaderType, OptiFine, ResourcePackInfo, SchematicInfo, ScreenshotInfo, + ShaderPackInfo, }; use crate::instance::models::world::base::WorldInfo; use crate::instance::models::world::level::LevelData; @@ -882,6 +883,11 @@ pub async fn create_instance( if version_path.exists() { return Err(InstanceError::ConflictNameError.into()); } + let optifine_info = optifine.as_ref().map(|info| OptiFine { + filename: info.filename.clone(), + version: format!("{}_{}", info.r#type, info.patch), + status: ModLoaderStatus::NotDownloaded, + }); // Create instance config let instance = Instance { @@ -902,7 +908,7 @@ pub async fn create_instance( version: mod_loader.version.clone(), branch: mod_loader.branch.clone(), }, - optifine, + optifine: optifine_info, description, icon_src, starred: false, @@ -989,11 +995,11 @@ pub async fn create_instance( .await?; } - if let Some(optifine_info) = &instance.optifine { + if let Some(info) = optifine.as_ref() { install_optifine( &priority_list, &instance.version, - optifine_info, + info, libraries_dir.to_path_buf(), &mut task_params, ) @@ -1095,6 +1101,32 @@ pub async fn finish_mod_loader_install(app: AppHandle, instance_id: String) -> S execute_processors(&app, &instance, &client_info, &install_profile).await?; } + if let Some(optifine) = &instance.optifine { + println!("Finish optifine installation for instance: {}", instance_id); + match optifine.status { + // prevent duplicated installation + ModLoaderStatus::DownloadFailed => { + return Err(InstanceError::ProcessorExecutionFailed.into()); + } + ModLoaderStatus::Installing => { + return Err(InstanceError::InstallationDuplicated.into()); + } + ModLoaderStatus::Installed => { + return Ok(()); + } + _ => {} + } + { + let binding = app.state::>>(); + let mut state = binding.lock()?; + let instance = state + .get_mut(&instance_id) + .ok_or(InstanceError::InstanceNotFoundByID)?; + instance.optifine.as_mut().unwrap().status = ModLoaderStatus::Installing; + }; + finish_optifine_installer(&app, &instance, &client_info).await?; + } + let instance = { let binding = app.state::>>(); let mut state = binding.lock()?; @@ -1102,6 +1134,9 @@ pub async fn finish_mod_loader_install(app: AppHandle, instance_id: String) -> S .get_mut(&instance_id) .ok_or(InstanceError::InstanceNotFoundByID)?; instance.mod_loader.status = ModLoaderStatus::Installed; + if let Some(optifine) = &mut instance.optifine { + optifine.status = ModLoaderStatus::Installed; + } instance.clone() }; instance.save_json_cfg().await?; diff --git a/src-tauri/src/instance/helpers/loader/forge.rs b/src-tauri/src/instance/helpers/loader/forge.rs index a0f59c8e6..a8d856f11 100644 --- a/src-tauri/src/instance/helpers/loader/forge.rs +++ b/src-tauri/src/instance/helpers/loader/forge.rs @@ -95,8 +95,7 @@ pub async fn download_forge_libraries( app: &AppHandle, priority: &[SourceType], instance: &Instance, - client_info: &McClientInfo, - is_retry: bool, // do not modify client info, just download necessary files + client_info: &mut McClientInfo, ) -> SJMCLResult<()> { let subdirs = get_instance_subdir_paths( app, @@ -109,8 +108,6 @@ pub async fn download_forge_libraries( }; let mut task_params = vec![]; - let mut client_info = client_info.clone(); - let installer_coord = format!( "net.minecraftforge:forge:{}-installer", instance.mod_loader.version @@ -304,14 +301,14 @@ pub async fn download_forge_libraries( })); } - let (arguments, minecraft_arguments) = if let Some(v_args) = client_info.arguments { + let (arguments, minecraft_arguments) = if let Some(v_args) = &client_info.arguments { let nf_args = forge_info .arguments .ok_or(InstanceError::ModLoaderVersionParseError)?; let new_args = LaunchArgumentTemplate { - game: [v_args.game, nf_args.game].concat(), - jvm: [v_args.jvm, nf_args.jvm].concat(), + game: [v_args.game.clone(), nf_args.game.clone()].concat(), + jvm: [v_args.jvm.clone(), nf_args.jvm.clone()].concat(), }; (Some(new_args), None) } else { @@ -447,13 +444,6 @@ pub async fn download_forge_libraries( ) .await?; - if !is_retry { - let vjson_path = instance - .version_path - .join(format!("{}.json", instance.name)); - fs::write(vjson_path, serde_json::to_vec_pretty(&client_info)?)?; - } - Ok(()) } diff --git a/src-tauri/src/instance/helpers/loader/neoforge.rs b/src-tauri/src/instance/helpers/loader/neoforge.rs index 21feffe4c..495bde9af 100644 --- a/src-tauri/src/instance/helpers/loader/neoforge.rs +++ b/src-tauri/src/instance/helpers/loader/neoforge.rs @@ -72,8 +72,7 @@ pub async fn download_neoforge_libraries( app: &AppHandle, priority: &[SourceType], instance: &Instance, - client_info: &McClientInfo, - is_retry: bool, // do not modify client info, just download necessary files + client_info: &mut McClientInfo, ) -> SJMCLResult<()> { let subdirs = get_instance_subdir_paths( app, @@ -86,8 +85,6 @@ pub async fn download_neoforge_libraries( }; let mut task_params = vec![]; - let mut client_info = client_info.clone(); - let name = if instance.mod_loader.version.starts_with("1.20.1-") { "forge" } else { @@ -341,11 +338,5 @@ pub async fn download_neoforge_libraries( ) .await?; - if !is_retry { - let vjson_path = instance - .version_path - .join(format!("{}.json", instance.name)); - fs::write(vjson_path, serde_json::to_vec_pretty(&client_info)?)?; - } Ok(()) } diff --git a/src-tauri/src/instance/helpers/loader/optifine.rs b/src-tauri/src/instance/helpers/loader/optifine.rs index 5533bf12a..02ca5f682 100644 --- a/src-tauri/src/instance/helpers/loader/optifine.rs +++ b/src-tauri/src/instance/helpers/loader/optifine.rs @@ -1,12 +1,26 @@ -use std::path::PathBuf; - use crate::error::SJMCLResult; +use crate::instance::helpers::client_json::{ArgumentsItem, LaunchArgumentTemplate}; +use crate::instance::helpers::client_json::{LibrariesValue, McClientInfo}; +use crate::instance::helpers::loader::common::add_library_entry; +use crate::instance::helpers::misc::{get_instance_game_config, get_instance_subdir_paths}; +use crate::instance::models::misc::{Instance, InstanceError, InstanceSubdirType}; use crate::launch::helpers::file_validator::convert_library_name_to_path; -use crate::resource::helpers::misc::get_download_api; +use crate::launch::helpers::jre_selector::select_java_runtime; +use crate::launcher_config::models::JavaInfo; +use crate::launcher_config::models::LauncherConfig; +use crate::resource::helpers::misc::get_source_priority_list; +use crate::resource::helpers::misc::{convert_url_to_target_source, get_download_api}; use crate::resource::models::{OptiFineResourceInfo, ResourceType, SourceType}; +use crate::tasks::commands::schedule_progressive_task_group; use crate::tasks::download::DownloadParam; use crate::tasks::PTaskParam; - +use std::fs; +use std::io::{self, Read}; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::Mutex; +use tauri::{AppHandle, Manager}; +use zip::{write::FileOptions, ZipArchive, ZipWriter}; pub async fn install_optifine( priority: &[SourceType], game_version: &str, @@ -43,3 +57,377 @@ pub async fn install_optifine( Ok(()) } + +pub async fn download_optifine_libraries_and_patch( + app: &AppHandle, + priority: &[SourceType], + instance: &Instance, + client_info: &mut McClientInfo, +) -> SJMCLResult<()> { + let optifine = instance + .optifine + .as_ref() + .ok_or(InstanceError::ClientJsonParseError)?; + + let subdirs = get_instance_subdir_paths( + app, + instance, + &[&InstanceSubdirType::Root, &InstanceSubdirType::Libraries], + ) + .ok_or(InstanceError::InvalidSourcePath)?; + let [_root_dir, lib_dir] = subdirs.as_slice() else { + return Err(InstanceError::InvalidSourcePath.into()); + }; + + let mut task_params: Vec = vec![]; + let installer_coord = format!("net.minecraftforge:optifine:{}", optifine.filename); + let installer_rel = convert_library_name_to_path(&installer_coord, None)?; + let installer_path = lib_dir.join(&installer_rel); + + if !installer_path.exists() { + return Err(InstanceError::ProcessorExecutionFailed.into()); + } + let mut has_launchwrapper = false; + let mut lw_coord = "".to_string(); + { + let file = std::fs::File::open(&installer_path)?; + let mut archive = ZipArchive::new(file)?; + let ver_opt: Option = match archive.by_name("launchwrapper-of.txt") { + Ok(mut txt) => { + let mut s = String::new(); + txt.read_to_string(&mut s)?; + let v = s.trim().to_string(); + if v.is_empty() { + None + } else { + Some(v) + } + } + Err(_) => None, + }; + + if let Some(ver) = ver_opt { + let jar_name = format!("launchwrapper-of-{}.jar", ver); + + if let Ok(mut lwo) = archive.by_name(&jar_name) { + let lwo_coord = format!("optifine:launchwrapper-of:{}", ver); + lw_coord = lwo_coord.clone(); + let lwo_rel = convert_library_name_to_path(&lwo_coord, None)?; + let lwo_path = lib_dir.join(lwo_rel); + if let Some(p) = lwo_path.parent() { + if !p.exists() { + fs::create_dir_all(p)?; + } + } + let mut out = std::fs::File::create(&lwo_path)?; + std::io::copy(&mut lwo, &mut out)?; + has_launchwrapper = true; + + add_library_entry(&mut client_info.libraries, &lwo_coord, None)?; + } + } + + if !has_launchwrapper { + if let Ok(mut lw2) = archive.by_name("launchwrapper-2.0.jar") { + let lw2_coord = "optifine:launchwrapper:2.0".to_string(); + lw_coord = lw2_coord.clone(); + let lw2_rel = convert_library_name_to_path(&lw2_coord, None)?; + let lw2_path = lib_dir.join(lw2_rel); + if let Some(p) = lw2_path.parent() { + if !p.exists() { + fs::create_dir_all(p)?; + } + } + let mut out = std::fs::File::create(&lw2_path)?; + std::io::copy(&mut lw2, &mut out)?; + has_launchwrapper = true; + + add_library_entry(&mut client_info.libraries, &lw2_coord, None)?; + } + } + } + + if !has_launchwrapper { + lw_coord = "net.minecraft:launchwrapper:1.12".to_string(); + add_library_entry(&mut client_info.libraries, &lw_coord, None)?; + + let lw_rel = convert_library_name_to_path(&lw_coord, None)?; + let lw_dest = lib_dir.join(&lw_rel); + + let base = get_download_api(priority[0], ResourceType::Libraries)?; + let src = convert_url_to_target_source( + &base.join(&lw_rel)?, + &[ResourceType::Libraries], + &priority[0], + )?; + + task_params.push(PTaskParam::Download(DownloadParam { + src, + dest: lw_dest, + filename: None, + sha1: None, + })); + } + + let optifine_runtime_coord = format!("net.minecraftforge:optifine:{}", optifine.filename); + add_library_entry(&mut client_info.libraries, &optifine_runtime_coord, None)?; + let lw_main = "net.minecraft.launchwrapper.Launch".to_string(); + + // 是否需要考虑到已经有过了 + if let Some(v_args) = client_info.arguments.clone() { + let mut g: Vec = v_args.game.clone(); + let flag = ArgumentsItem { + value: vec!["--tweakClass".to_string()], + rules: vec![], + }; + let val = ArgumentsItem { + value: vec!["optifine.OptiFineTweaker".to_string()], + rules: vec![], + }; + if let Some(pos) = g.iter().position(|item| { + item + .value + .first() + .map(|v| v == "--launchTarget") + .unwrap_or(false) + }) { + g.insert(pos, val); + g.insert(pos, flag); + } else { + g.insert(0, val); + g.insert(0, flag); + } + + let new_args = LaunchArgumentTemplate { + game: g, + jvm: v_args.jvm.clone(), + }; + client_info.arguments = Some(new_args); + } else { + let mut s = client_info.minecraft_arguments.clone().unwrap_or_default(); + if !s.is_empty() && !s.ends_with(' ') { + s.push(' '); + } + s.push_str("--tweakClass optifine.OptiFineTweaker"); + client_info.minecraft_arguments = Some(s); + }; + + let (patch_arguments, patch_minecraft_arguments) = if client_info.arguments.is_some() { + let patch_args = LaunchArgumentTemplate { + game: vec![ + ArgumentsItem { + value: vec!["--tweakClass".to_string()], + rules: vec![], + }, + ArgumentsItem { + value: vec!["optifine.OptiFineTweaker".to_string()], + rules: vec![], + }, + ], + jvm: vec![], + }; + (Some(patch_args), None) + } else { + ( + None, + Some("--tweakClass optifine.OptiFineTweaker".to_string()), + ) + }; + + client_info.patches.push(McClientInfo { + id: "optifine".to_string(), + version: Some(optifine.version.clone()), + priority: Some(10000), + main_class: Some(lw_main.clone()), + arguments: patch_arguments, + minecraft_arguments: patch_minecraft_arguments, + libraries: vec![ + LibrariesValue { + name: optifine_runtime_coord.clone(), + ..Default::default() + }, + LibrariesValue { + name: lw_coord.clone(), + ..Default::default() + }, + ], + ..Default::default() + }); + if client_info.main_class == Some("net.minecraft.client.main.Main".to_string()) { + client_info.main_class = Some(lw_main.clone()); + } + + if !task_params.is_empty() { + schedule_progressive_task_group( + app.clone(), + format!("optifine-libraries?{}", instance.id), + task_params, + true, + ) + .await?; + } + + Ok(()) +} + +async fn run_optifine_patcher( + app: &AppHandle, + instance: &Instance, + client_info: &McClientInfo, + installer_jar: &Path, + base_client_jar: &Path, + out_optifine_jar: &Path, +) -> SJMCLResult<()> { + let javas_state = app.state::>>(); + let javas = javas_state.lock()?.clone(); + + let game_config = get_instance_game_config(app, instance); + + let selected_java = select_java_runtime( + app, + &game_config.game_java, + &javas, + instance, + client_info + .java_version + .as_ref() + .ok_or(InstanceError::ProcessorExecutionFailed)? + .major_version, + ) + .await?; + + let mut cmd = Command::new(&selected_java.exec_path); + + #[cfg(target_os = "windows")] + { + use std::os::windows::process::CommandExt; + cmd.creation_flags(0x08000000); + } + + cmd + .arg("-cp") + .arg(installer_jar) + .arg("optifine.Patcher") + .arg(base_client_jar) + .arg(installer_jar) + .arg(out_optifine_jar); + + let output = cmd.output()?; + + if !output.status.success() { + return Err(InstanceError::ProcessorExecutionFailed.into()); + } + Ok(()) +} + +pub async fn finish_optifine_installer( + app: &AppHandle, + instance: &Instance, + client_info: &McClientInfo, +) -> SJMCLResult<()> { + let subdirs = get_instance_subdir_paths(&app, &instance, &[&InstanceSubdirType::Libraries]) + .ok_or(InstanceError::InstanceNotFoundByID)?; + let libraries_dir = subdirs.first().ok_or(InstanceError::InstanceNotFoundByID)?; + let optifine = instance + .optifine + .as_ref() + .ok_or(InstanceError::ModLoaderVersionParseError)?; + let installer_coord = format!( + "net.minecraftforge:optifine:{}-installer", + optifine.filename + ); + let optifine_coord = format!("net.minecraftforge:optifine:{}", optifine.filename); + let installer_rel = convert_library_name_to_path(&installer_coord, None)?; + let installer_path = libraries_dir.join(&installer_rel); + let optifine_rel = convert_library_name_to_path(&optifine_coord, None)?; + let optifine_path = libraries_dir.join(&optifine_rel); + if !installer_path.exists() { + return Err(InstanceError::LoaderNotDownloaded.into()); + } + + let f = fs::File::open(&installer_path)?; + let mut archive = ZipArchive::new(f)?; + + let candidate = "optifine/Patcher.class"; + let has_patcher = if archive.by_name(candidate).is_ok() { + true + } else { + false + }; + + let base_client_jar = instance.version_path.join(format!("{}.jar", instance.name)); + + if let Some(parent) = optifine_path.parent() { + std::fs::create_dir_all(parent)?; + } + if has_patcher { + run_optifine_patcher( + app, + instance, + client_info, + &installer_path, + &base_client_jar, + &optifine_path, + ) + .await?; + } else { + fs::copy(&installer_path, &optifine_path)?; + } + + remove_entry_from_zip_inplace(&optifine_path, "META-INF/mods.toml")?; + + let priority_list = { + let launcher_config_state = app.state::>(); + let launcher_config = launcher_config_state.lock()?; + get_source_priority_list(&launcher_config) + }; + Ok(()) +} + +fn remove_entry_from_zip_inplace(zip_path: &Path, entry_name: &str) -> SJMCLResult<()> { + if !zip_path.exists() { + return Ok(()); + } + + let tmp_path = { + let mut p = zip_path.to_path_buf(); + let ext = p + .extension() + .map(|e| e.to_string_lossy().to_string()) + .unwrap_or_else(|| "jar".to_string()); + p.set_extension(format!("{}.tmp", ext)); + p + }; + + let src = fs::File::open(zip_path)?; + let mut archive = ZipArchive::new(src)?; + + let dst = fs::File::create(&tmp_path)?; + let mut writer = ZipWriter::new(dst); + + for i in 0..archive.len() { + let mut file = archive.by_index(i)?; + let name = file.name().to_string(); + + if name == entry_name { + continue; + } + + if name.ends_with('/') { + writer.add_directory(name, FileOptions::<()>::default())?; + continue; + } + + let mut options = FileOptions::<()>::default().compression_method(file.compression()); + + writer.start_file(name, options)?; + + io::copy(&mut file, &mut writer)?; + } + + writer.finish()?; + + fs::rename(&tmp_path, zip_path)?; + + Ok(()) +} diff --git a/src-tauri/src/instance/helpers/misc.rs b/src-tauri/src/instance/helpers/misc.rs index 36178bfb8..7aca63478 100644 --- a/src-tauri/src/instance/helpers/misc.rs +++ b/src-tauri/src/instance/helpers/misc.rs @@ -3,8 +3,9 @@ use crate::instance::helpers::client_jar::load_game_version_from_jar; use crate::instance::helpers::client_json::{libraries_to_info, patches_to_info, McClientInfo}; use crate::instance::helpers::loader::forge::download_forge_libraries; use crate::instance::helpers::loader::neoforge::download_neoforge_libraries; +use crate::instance::helpers::loader::optifine::download_optifine_libraries_and_patch; use crate::instance::models::misc::{ - Instance, InstanceError, InstanceSubdirType, ModLoader, ModLoaderStatus, ModLoaderType, + Instance, InstanceError, InstanceSubdirType, ModLoader, ModLoaderStatus, ModLoaderType, OptiFine, }; use crate::launcher_config::helpers::misc::get_global_game_config; use crate::launcher_config::models::{GameConfig, GameDirectory, LauncherConfig}; @@ -154,7 +155,7 @@ pub async fn refresh_instances( continue; // not a valid instance } - let client_data = match load_json_async::(&json_path).await { + let mut client_data = match load_json_async::(&json_path).await { Ok(v) => v, Err(e) => { println!("Failed to load client info for {}: {}", name, e); @@ -186,25 +187,34 @@ pub async fn refresh_instances( }; if let Err(e) = { match cfg_read.mod_loader.status { - ModLoaderStatus::NotDownloaded => match cfg_read.mod_loader.loader_type { - ModLoaderType::Forge => { - cfg_read.mod_loader.status = ModLoaderStatus::Downloading; - download_forge_libraries(app, &priority_list, &cfg_read, &client_data, false).await - } - ModLoaderType::NeoForge => { - cfg_read.mod_loader.status = ModLoaderStatus::Downloading; - download_neoforge_libraries(app, &priority_list, &cfg_read, &client_data, false).await + ModLoaderStatus::NotDownloaded => { + match cfg_read.mod_loader.loader_type { + ModLoaderType::Forge => { + cfg_read.mod_loader.status = ModLoaderStatus::Downloading; + download_forge_libraries(app, &priority_list, &cfg_read, &mut client_data).await?; + } + ModLoaderType::NeoForge => { + cfg_read.mod_loader.status = ModLoaderStatus::Downloading; + download_neoforge_libraries(app, &priority_list, &cfg_read, &mut client_data) + .await?; + } + _ => {} } - _ => Ok(()), - }, + let vjson_path = cfg_read + .version_path + .join(format!("{}.json", cfg_read.name)); + fs::write(vjson_path, serde_json::to_vec_pretty(&client_data)?)?; + + Ok(()) + } ModLoaderStatus::DownloadFailed => match cfg_read.mod_loader.loader_type { ModLoaderType::Forge => { cfg_read.mod_loader.status = ModLoaderStatus::Downloading; - download_forge_libraries(app, &priority_list, &cfg_read, &client_data, true).await + download_forge_libraries(app, &priority_list, &cfg_read, &mut client_data).await } ModLoaderType::NeoForge => { cfg_read.mod_loader.status = ModLoaderStatus::Downloading; - download_neoforge_libraries(app, &priority_list, &cfg_read, &client_data, true).await + download_neoforge_libraries(app, &priority_list, &cfg_read, &mut client_data).await } _ => Ok(()), }, @@ -224,6 +234,52 @@ pub async fn refresh_instances( continue; } } + if cfg_read + .optifine + .as_ref() + .map_or(false, |o| o.status != ModLoaderStatus::Installed) + { + let priority_list = { + let launcher_config_state = app.state::>(); + let launcher_config = launcher_config_state.lock()?; + get_source_priority_list(&launcher_config) + }; + println!("Start OptiFine installation for instance: {}", name); + if let Err(e) = { + let of_status = cfg_read.optifine.as_ref().map(|o| o.status.clone()); + match of_status { + Some(ModLoaderStatus::NotDownloaded) => { + download_optifine_libraries_and_patch(app, &priority_list, &cfg_read, &mut client_data) + .await?; + let vjson_path = cfg_read + .version_path + .join(format!("{}.json", cfg_read.name)); + fs::write(vjson_path, serde_json::to_vec_pretty(&client_data)?)?; + Ok(()) + } + Some(ModLoaderStatus::DownloadFailed) => { + download_optifine_libraries_and_patch(app, &priority_list, &cfg_read, &mut client_data) + .await + } + Some(ModLoaderStatus::Downloading) | Some(ModLoaderStatus::Installing) => { + if is_first_run { + if let Some(o) = &mut cfg_read.optifine { + o.status = ModLoaderStatus::DownloadFailed; + } + } + Ok(()) + } + _ => Ok(()), + } + } { + log::warn!("Failed to install OptiFine for {}: {:?}", name, e); + if let Some(o) = &mut cfg_read.optifine { + o.status = ModLoaderStatus::DownloadFailed; + } + cfg_read.save_json_cfg().await?; + continue; + } + } let (mut game_version, loader_version, loader_type, optifine_info) = if !client_data.patches.is_empty() { @@ -243,11 +299,18 @@ pub async fn refresh_instances( cfg_read.icon_src = loader_type.to_icon_path().to_string(); } + let mod_loader_installed = cfg_read.mod_loader.status == ModLoaderStatus::Installed; + let optifine_installed = cfg_read + .optifine + .as_ref() + .map_or(false, |o| o.status == ModLoaderStatus::Installed); + let optifine_filename = optifine_info.as_ref().map(|info| info.filename.clone()); + let optifine_version = optifine_info.map(|info| format!("{}_{}", info.r#type, info.patch)); let instance = Instance { name, version: game_version.unwrap_or_default(), version_path, - mod_loader: if cfg_read.mod_loader.status != ModLoaderStatus::Installed { + mod_loader: if !mod_loader_installed { // pass mod loader check if download is not ready cfg_read.mod_loader } else { @@ -258,7 +321,15 @@ pub async fn refresh_instances( branch: None, } }, - optifine: optifine_info.clone(), + optifine: if !optifine_installed { + cfg_read.optifine.clone() + } else { + Some(OptiFine { + filename: optifine_filename.unwrap_or_default(), + version: optifine_version.unwrap_or_default(), + status: ModLoaderStatus::Installed, + }) + }, ..cfg_read }; // ignore error here, for now @@ -281,6 +352,7 @@ pub async fn refresh_all_instances( match refresh_instances(app, game_directory, is_first_run).await { Ok(vs) => { for mut instance in vs { + println!("composed id: {}:{}", dir_name, instance.name); let composed_id = format!("{}:{}", dir_name, instance.name); instance.id = composed_id.clone(); instance_map.insert(composed_id, instance); diff --git a/src-tauri/src/instance/models/misc.rs b/src-tauri/src/instance/models/misc.rs index 8d25c9c65..dbac1b85e 100644 --- a/src-tauri/src/instance/models/misc.rs +++ b/src-tauri/src/instance/models/misc.rs @@ -1,7 +1,6 @@ use crate::instance::constants::INSTANCE_CFG_FILE_NAME; use crate::instance::helpers::game_version::{compare_game_versions, get_major_game_version}; use crate::launcher_config::models::GameConfig; -use crate::resource::models::OptiFineResourceInfo; use crate::storage::{load_json_async, save_json_async}; use crate::utils::image::ImageWrapper; use serde::{Deserialize, Serialize}; @@ -98,7 +97,7 @@ structstruck::strike! { pub version: String, pub branch: Option, // Optional branch name for mod loaders like Forge }, - pub optifine: Option, + pub optifine: Option, // if true, use the spec_game_config, else use the global game config pub use_spec_game_config: bool, // if use_spec_game_config is false, this field is ignored @@ -271,6 +270,15 @@ pub enum InstanceError { InstallationDuplicated, ProcessorExecutionFailed, SemaphoreAcquireFailed, + LoaderNotDownloaded, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct OptiFine { + pub filename: String, + pub version: String, + pub status: ModLoaderStatus, } impl std::error::Error for InstanceError {} diff --git a/src/contexts/task.tsx b/src/contexts/task.tsx index f4b9608e7..a31b36d46 100644 --- a/src/contexts/task.tsx +++ b/src/contexts/task.tsx @@ -437,6 +437,7 @@ export const TaskContextProvider: React.FC<{ children: React.ReactNode }> = ({ break; case "forge-libraries": case "neoforge-libraries": + case "optifine-libraries": if (version) { let instanceName = getInstanceList()?.find( (i) => i.id === version From 42ac0bb809e2df41252af9f0e10f30baa9e81acd Mon Sep 17 00:00:00 2001 From: xunying123 Date: Wed, 31 Dec 2025 15:51:52 +0800 Subject: [PATCH 07/12] feat(instance): support install old version optifine --- src-tauri/src/instance/commands.rs | 7 +-- .../src/instance/helpers/loader/common.rs | 1 - .../src/instance/helpers/loader/optifine.rs | 41 ++++++++++++---- src-tauri/src/instance/helpers/misc.rs | 48 ------------------- 4 files changed, 35 insertions(+), 62 deletions(-) diff --git a/src-tauri/src/instance/commands.rs b/src-tauri/src/instance/commands.rs index 96f9a6ee7..562497b03 100644 --- a/src-tauri/src/instance/commands.rs +++ b/src-tauri/src/instance/commands.rs @@ -5,7 +5,9 @@ use crate::instance::helpers::client_json::{replace_native_libraries, McClientIn use crate::instance::helpers::game_version::{build_game_version_cmp_fn, compare_game_versions}; use crate::instance::helpers::loader::common::{execute_processors, install_mod_loader}; use crate::instance::helpers::loader::forge::InstallProfile; -use crate::instance::helpers::loader::optifine::{finish_optifine_installer, install_optifine}; +use crate::instance::helpers::loader::optifine::{ + download_optifine_installer, finish_optifine_installer, +}; use crate::instance::helpers::misc::{ get_instance_game_config, get_instance_subdir_path_by_id, get_instance_subdir_paths, refresh_and_update_instances, unify_instance_name, @@ -996,7 +998,7 @@ pub async fn create_instance( } if let Some(info) = optifine.as_ref() { - install_optifine( + download_optifine_installer( &priority_list, &instance.version, info, @@ -1102,7 +1104,6 @@ pub async fn finish_mod_loader_install(app: AppHandle, instance_id: String) -> S } if let Some(optifine) = &instance.optifine { - println!("Finish optifine installation for instance: {}", instance_id); match optifine.status { // prevent duplicated installation ModLoaderStatus::DownloadFailed => { diff --git a/src-tauri/src/instance/helpers/loader/common.rs b/src-tauri/src/instance/helpers/loader/common.rs index fd15ec343..c553f6b90 100644 --- a/src-tauri/src/instance/helpers/loader/common.rs +++ b/src-tauri/src/instance/helpers/loader/common.rs @@ -11,7 +11,6 @@ use crate::instance::helpers::client_json::{LibrariesValue, McClientInfo}; use crate::instance::helpers::loader::fabric::install_fabric_loader; use crate::instance::helpers::loader::forge::{install_forge_loader, InstallProfile}; use crate::instance::helpers::loader::neoforge::install_neoforge_loader; -use crate::instance::helpers::loader::optifine::install_optifine; use crate::instance::helpers::misc::get_instance_game_config; use crate::instance::models::misc::{Instance, InstanceError, ModLoader, ModLoaderType}; use crate::launch::helpers::file_validator::merge_library_lists; diff --git a/src-tauri/src/instance/helpers/loader/optifine.rs b/src-tauri/src/instance/helpers/loader/optifine.rs index 02ca5f682..80da35edf 100644 --- a/src-tauri/src/instance/helpers/loader/optifine.rs +++ b/src-tauri/src/instance/helpers/loader/optifine.rs @@ -3,7 +3,7 @@ use crate::instance::helpers::client_json::{ArgumentsItem, LaunchArgumentTemplat use crate::instance::helpers::client_json::{LibrariesValue, McClientInfo}; use crate::instance::helpers::loader::common::add_library_entry; use crate::instance::helpers::misc::{get_instance_game_config, get_instance_subdir_paths}; -use crate::instance::models::misc::{Instance, InstanceError, InstanceSubdirType}; +use crate::instance::models::misc::{Instance, InstanceError, InstanceSubdirType, ModLoaderType}; use crate::launch::helpers::file_validator::convert_library_name_to_path; use crate::launch::helpers::jre_selector::select_java_runtime; use crate::launcher_config::models::JavaInfo; @@ -21,7 +21,7 @@ use std::process::Command; use std::sync::Mutex; use tauri::{AppHandle, Manager}; use zip::{write::FileOptions, ZipArchive, ZipWriter}; -pub async fn install_optifine( +pub async fn download_optifine_installer( priority: &[SourceType], game_version: &str, optifine: &OptiFineResourceInfo, @@ -58,12 +58,13 @@ pub async fn install_optifine( Ok(()) } -pub async fn download_optifine_libraries_and_patch( +async fn download_optifine_libraries( app: &AppHandle, priority: &[SourceType], instance: &Instance, - client_info: &mut McClientInfo, + client_info: &McClientInfo, ) -> SJMCLResult<()> { + let mut client_info = client_info.clone(); let optifine = instance .optifine .as_ref() @@ -180,10 +181,18 @@ pub async fn download_optifine_libraries_and_patch( value: vec!["--tweakClass".to_string()], rules: vec![], }; - let val = ArgumentsItem { - value: vec!["optifine.OptiFineTweaker".to_string()], - rules: vec![], + let val = if instance.mod_loader.loader_type == ModLoaderType::Forge { + ArgumentsItem { + value: vec!["optifine.OptiFineForgeTweaker".to_string()], + rules: vec![], + } + } else { + ArgumentsItem { + value: vec!["optifine.OptiFineTweaker".to_string()], + rules: vec![], + } }; + if let Some(pos) = g.iter().position(|item| { item .value @@ -208,7 +217,11 @@ pub async fn download_optifine_libraries_and_patch( if !s.is_empty() && !s.ends_with(' ') { s.push(' '); } - s.push_str("--tweakClass optifine.OptiFineTweaker"); + if instance.mod_loader.loader_type == ModLoaderType::Forge { + s.push_str("--tweakClass optifine.OptiFineForgeTweaker"); + } else { + s.push_str("--tweakClass optifine.OptiFineTweaker"); + } client_info.minecraft_arguments = Some(s); }; @@ -267,6 +280,11 @@ pub async fn download_optifine_libraries_and_patch( .await?; } + let vjson_path = instance + .version_path + .join(format!("{}.json", instance.name)); + fs::write(vjson_path, serde_json::to_vec_pretty(&client_info)?)?; + Ok(()) } @@ -374,17 +392,20 @@ pub async fn finish_optifine_installer( fs::copy(&installer_path, &optifine_path)?; } - remove_entry_from_zip_inplace(&optifine_path, "META-INF/mods.toml")?; + remove_entry_from_zip(&optifine_path, "META-INF/mods.toml")?; let priority_list = { let launcher_config_state = app.state::>(); let launcher_config = launcher_config_state.lock()?; get_source_priority_list(&launcher_config) }; + + download_optifine_libraries(app, &priority_list, &instance, &client_info).await?; + Ok(()) } -fn remove_entry_from_zip_inplace(zip_path: &Path, entry_name: &str) -> SJMCLResult<()> { +fn remove_entry_from_zip(zip_path: &Path, entry_name: &str) -> SJMCLResult<()> { if !zip_path.exists() { return Ok(()); } diff --git a/src-tauri/src/instance/helpers/misc.rs b/src-tauri/src/instance/helpers/misc.rs index 7aca63478..d2ab59db5 100644 --- a/src-tauri/src/instance/helpers/misc.rs +++ b/src-tauri/src/instance/helpers/misc.rs @@ -3,7 +3,6 @@ use crate::instance::helpers::client_jar::load_game_version_from_jar; use crate::instance::helpers::client_json::{libraries_to_info, patches_to_info, McClientInfo}; use crate::instance::helpers::loader::forge::download_forge_libraries; use crate::instance::helpers::loader::neoforge::download_neoforge_libraries; -use crate::instance::helpers::loader::optifine::download_optifine_libraries_and_patch; use crate::instance::models::misc::{ Instance, InstanceError, InstanceSubdirType, ModLoader, ModLoaderStatus, ModLoaderType, OptiFine, }; @@ -234,52 +233,6 @@ pub async fn refresh_instances( continue; } } - if cfg_read - .optifine - .as_ref() - .map_or(false, |o| o.status != ModLoaderStatus::Installed) - { - let priority_list = { - let launcher_config_state = app.state::>(); - let launcher_config = launcher_config_state.lock()?; - get_source_priority_list(&launcher_config) - }; - println!("Start OptiFine installation for instance: {}", name); - if let Err(e) = { - let of_status = cfg_read.optifine.as_ref().map(|o| o.status.clone()); - match of_status { - Some(ModLoaderStatus::NotDownloaded) => { - download_optifine_libraries_and_patch(app, &priority_list, &cfg_read, &mut client_data) - .await?; - let vjson_path = cfg_read - .version_path - .join(format!("{}.json", cfg_read.name)); - fs::write(vjson_path, serde_json::to_vec_pretty(&client_data)?)?; - Ok(()) - } - Some(ModLoaderStatus::DownloadFailed) => { - download_optifine_libraries_and_patch(app, &priority_list, &cfg_read, &mut client_data) - .await - } - Some(ModLoaderStatus::Downloading) | Some(ModLoaderStatus::Installing) => { - if is_first_run { - if let Some(o) = &mut cfg_read.optifine { - o.status = ModLoaderStatus::DownloadFailed; - } - } - Ok(()) - } - _ => Ok(()), - } - } { - log::warn!("Failed to install OptiFine for {}: {:?}", name, e); - if let Some(o) = &mut cfg_read.optifine { - o.status = ModLoaderStatus::DownloadFailed; - } - cfg_read.save_json_cfg().await?; - continue; - } - } let (mut game_version, loader_version, loader_type, optifine_info) = if !client_data.patches.is_empty() { @@ -352,7 +305,6 @@ pub async fn refresh_all_instances( match refresh_instances(app, game_directory, is_first_run).await { Ok(vs) => { for mut instance in vs { - println!("composed id: {}:{}", dir_name, instance.name); let composed_id = format!("{}:{}", dir_name, instance.name); instance.id = composed_id.clone(); instance_map.insert(composed_id, instance); From fb3fc4382f1acb8155bfbb70e83a94d2f83c715b Mon Sep 17 00:00:00 2001 From: UNIkeEN <94227543+UNIkeEN@users.noreply.github.com> Date: Thu, 1 Jan 2026 11:07:11 +0800 Subject: [PATCH 08/12] style(backend): resolve clippy warning --- src-tauri/src/instance/helpers/loader/optifine.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src-tauri/src/instance/helpers/loader/optifine.rs b/src-tauri/src/instance/helpers/loader/optifine.rs index 80da35edf..1f0c728f9 100644 --- a/src-tauri/src/instance/helpers/loader/optifine.rs +++ b/src-tauri/src/instance/helpers/loader/optifine.rs @@ -433,21 +433,17 @@ fn remove_entry_from_zip(zip_path: &Path, entry_name: &str) -> SJMCLResult<()> { if name == entry_name { continue; } - if name.ends_with('/') { writer.add_directory(name, FileOptions::<()>::default())?; continue; } - let mut options = FileOptions::<()>::default().compression_method(file.compression()); - + let options = FileOptions::<()>::default().compression_method(file.compression()); writer.start_file(name, options)?; - io::copy(&mut file, &mut writer)?; } writer.finish()?; - fs::rename(&tmp_path, zip_path)?; Ok(()) From 75256d73c8c2fa55317e6f7c1d4ba2a64936b0aa Mon Sep 17 00:00:00 2001 From: Reqwey Date: Sat, 3 Jan 2026 16:14:15 +0800 Subject: [PATCH 09/12] fix(frontend): overflow problem with loader selector cards --- src/components/loader-selector.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/loader-selector.tsx b/src/components/loader-selector.tsx index 2e97b688d..e06dc9a72 100644 --- a/src/components/loader-selector.tsx +++ b/src/components/loader-selector.tsx @@ -294,8 +294,8 @@ export const LoaderSelector: React.FC = ({ ]); return ( - - + + {selectableCardItems.map((item, index) => ( ))} From 874b8aaa5f0a5f8b760946b3cd28378b92d676dd Mon Sep 17 00:00:00 2001 From: Reqwey Date: Sat, 3 Jan 2026 17:02:40 +0800 Subject: [PATCH 10/12] feat(instance): initially add shader loader display cards in instance details page --- .../src/instance/helpers/loader/optifine.rs | 10 +- src-tauri/src/instance/models/misc.rs | 4 +- src-tauri/src/launcher_config/models.rs | 6 +- src/locales/en.json | 8 +- src/locales/zh-Hans.json | 4 + src/models/config.ts | 10 +- src/models/instance/misc.ts | 7 + .../instances/details/[id]/resourcepacks.tsx | 2 +- .../instances/details/[id]/shaderpacks.tsx | 133 ++++++++++++------ 9 files changed, 130 insertions(+), 54 deletions(-) diff --git a/src-tauri/src/instance/helpers/loader/optifine.rs b/src-tauri/src/instance/helpers/loader/optifine.rs index 1f0c728f9..8284d8399 100644 --- a/src-tauri/src/instance/helpers/loader/optifine.rs +++ b/src-tauri/src/instance/helpers/loader/optifine.rs @@ -343,7 +343,7 @@ pub async fn finish_optifine_installer( instance: &Instance, client_info: &McClientInfo, ) -> SJMCLResult<()> { - let subdirs = get_instance_subdir_paths(&app, &instance, &[&InstanceSubdirType::Libraries]) + let subdirs = get_instance_subdir_paths(app, instance, &[&InstanceSubdirType::Libraries]) .ok_or(InstanceError::InstanceNotFoundByID)?; let libraries_dir = subdirs.first().ok_or(InstanceError::InstanceNotFoundByID)?; let optifine = instance @@ -367,11 +367,7 @@ pub async fn finish_optifine_installer( let mut archive = ZipArchive::new(f)?; let candidate = "optifine/Patcher.class"; - let has_patcher = if archive.by_name(candidate).is_ok() { - true - } else { - false - }; + let has_patcher = archive.by_name(candidate).is_ok(); let base_client_jar = instance.version_path.join(format!("{}.jar", instance.name)); @@ -400,7 +396,7 @@ pub async fn finish_optifine_installer( get_source_priority_list(&launcher_config) }; - download_optifine_libraries(app, &priority_list, &instance, &client_info).await?; + download_optifine_libraries(app, &priority_list, instance, client_info).await?; Ok(()) } diff --git a/src-tauri/src/instance/models/misc.rs b/src-tauri/src/instance/models/misc.rs index dbac1b85e..0c6c08d26 100644 --- a/src-tauri/src/instance/models/misc.rs +++ b/src-tauri/src/instance/models/misc.rs @@ -81,7 +81,7 @@ pub enum ModLoaderStatus { structstruck::strike! { #[strikethrough[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Default)]] - #[strikethrough[serde(rename_all = "camelCase", deny_unknown_fields, default)]] + #[strikethrough[serde(rename_all = "camelCase", default)]] pub struct Instance { pub id: String, pub name: String, @@ -135,6 +135,7 @@ pub struct InstanceSummary { pub version: String, pub major_version: String, pub mod_loader: ModLoader, + pub optifine: Option, pub support_quick_play: bool, pub use_spec_game_config: bool, pub is_version_isolated: bool, @@ -157,6 +158,7 @@ impl InstanceSummary { version_path: instance.version_path.clone(), version: instance.version.clone(), mod_loader: instance.mod_loader.clone(), + optifine: instance.optifine.clone(), // skip fallback remote fetch in `get_major_game_version` and `compare_game_versions` to avoid instance list load delay. // ref: https://github.com/UNIkeEN/SJMCL/pull/799 major_version: get_major_game_version(app, &instance.version, false).await, diff --git a/src-tauri/src/launcher_config/models.rs b/src-tauri/src/launcher_config/models.rs index 8b2dcd29a..418c7c7fc 100644 --- a/src-tauri/src/launcher_config/models.rs +++ b/src-tauri/src/launcher_config/models.rs @@ -296,7 +296,7 @@ structstruck::strike! { #[default([true, true])] pub accordion_states: [bool; 2], }, - pub instance_resourcepack_page: struct { + pub instance_resource_packs_page: struct { #[default([true, true])] pub accordion_states: [bool; 2], }, @@ -304,6 +304,10 @@ structstruck::strike! { #[default([true, true])] pub accordion_states: [bool; 2], }, + pub instance_shader_packs_page: struct { + #[default([true, true])] + pub accordion_states: [bool; 2], + } } } } diff --git a/src/locales/en.json b/src/locales/en.json index ac2eb5a53..be2f3ea59 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1243,6 +1243,10 @@ } }, "InstanceShaderPacksPage": { + "shaderLoaderList": { + "title": "Loaders", + "notInstalled": "Not Installed" + }, "shaderPackList": { "title": "Shader Packs" } @@ -1364,8 +1368,8 @@ "stable": "Stable", "beta": "Beta", "releaseDate": "Released at {{date}}", - "notCompatibleWith":"Not compatible with {{item}}", - "noVersionSelected":"No version selected" + "notCompatibleWith": "Not compatible with {{item}}", + "noVersionSelected": "No version selected" }, "ManageSkinModal": { "skinManage": "Manage Skin", diff --git a/src/locales/zh-Hans.json b/src/locales/zh-Hans.json index 1b43958f3..08c87ab60 100644 --- a/src/locales/zh-Hans.json +++ b/src/locales/zh-Hans.json @@ -1243,6 +1243,10 @@ } }, "InstanceShaderPacksPage": { + "shaderLoaderList": { + "title": "加载器", + "notInstalled": "未安装" + }, "shaderPackList": { "title": "光影包" } diff --git a/src/models/config.ts b/src/models/config.ts index 9e4545d6c..49cb32065 100644 --- a/src/models/config.ts +++ b/src/models/config.ts @@ -147,12 +147,15 @@ export interface LauncherConfig { instanceModsPage: { accordionStates: boolean[]; }; - instanceResourcepackPage: { + instanceResourcePacksPage: { accordionStates: boolean[]; }; instanceWorldsPage: { accordionStates: boolean[]; }; + instanceShaderPacksPage: { + accordionStates: boolean[]; + }; }; } @@ -303,12 +306,15 @@ export const defaultConfig: LauncherConfig = { instanceModsPage: { accordionStates: [true, true], }, - instanceResourcepackPage: { + instanceResourcePacksPage: { accordionStates: [true, true], }, instanceWorldsPage: { accordionStates: [true, true], }, + instanceShaderPacksPage: { + accordionStates: [true, true], + }, }, }; diff --git a/src/models/instance/misc.ts b/src/models/instance/misc.ts index 7dcd41656..a157cf079 100644 --- a/src/models/instance/misc.ts +++ b/src/models/instance/misc.ts @@ -16,6 +16,12 @@ export interface ModLoader { branch?: string; } +export interface OptiFine { + filename: string; + version: string; + status: ModLoaderStatus; +} + export interface InstanceSummary { id: string; iconSrc: string; @@ -27,6 +33,7 @@ export interface InstanceSummary { version: string; majorVersion: string; modLoader: ModLoader; + optifine?: OptiFine; supportQuickPlay: boolean; useSpecGameConfig: boolean; isVersionIsolated: boolean; diff --git a/src/pages/instances/details/[id]/resourcepacks.tsx b/src/pages/instances/details/[id]/resourcepacks.tsx index 0078c1d17..636949627 100755 --- a/src/pages/instances/details/[id]/resourcepacks.tsx +++ b/src/pages/instances/details/[id]/resourcepacks.tsx @@ -31,7 +31,7 @@ const InstanceResourcePacksPage = () => { isServerResourcePackListLoading, } = useInstanceSharedData(); const accordionStates = - config.states.instanceResourcepackPage.accordionStates; + config.states.instanceResourcePacksPage.accordionStates; const { openSharedModal } = useSharedModals(); const [resourcePacks, setResourcePacks] = useState([]); diff --git a/src/pages/instances/details/[id]/shaderpacks.tsx b/src/pages/instances/details/[id]/shaderpacks.tsx index c506de4a1..e51e40924 100644 --- a/src/pages/instances/details/[id]/shaderpacks.tsx +++ b/src/pages/instances/details/[id]/shaderpacks.tsx @@ -8,6 +8,10 @@ import CountTag from "@/components/common/count-tag"; import Empty from "@/components/common/empty"; import { OptionItem, OptionItemGroup } from "@/components/common/option-item"; import { Section } from "@/components/common/section"; +import SelectableCard, { + SelectableCardProps, +} from "@/components/common/selectable-card"; +import { useLauncherConfig } from "@/contexts/config"; import { useInstanceSharedData } from "@/contexts/instance"; import { useSharedModals } from "@/contexts/shared-modal"; import { InstanceSubdirType } from "@/enums/instance"; @@ -17,6 +21,7 @@ import { ShaderPackInfo } from "@/models/instance/misc"; import { ResourceService } from "@/services/resource"; const InstanceShaderPacksPage = () => { + const { config, update } = useLauncherConfig(); const { t } = useTranslation(); const { summary, @@ -26,6 +31,7 @@ const InstanceShaderPacksPage = () => { isShaderPackListLoading: isLoading, } = useInstanceSharedData(); const { openSharedModal } = useSharedModals(); + const accordionStates = config.states.instanceShaderPacksPage.accordionStates; const [shaderPacks, setShaderPacks] = useState([]); @@ -107,51 +113,98 @@ const InstanceShaderPacksPage = () => { }, ]; + const selectableCardItems: SelectableCardProps[] = [ + { + title: "OptiFine", + iconSrc: "/images/icons/OptiFine.png", + description: + summary?.optifine?.version || + t("InstanceShaderPacksPage.shaderLoaderList.notInstalled"), + displayMode: "entry", + isSelected: summary?.optifine !== undefined, + onSelect: () => {}, + isDisabled: true, // TODO: add OptiFine installation support + }, + ]; + return ( -
} - headExtra={ - - {shaderSecMenuOperations.map((btn, index) => ( - +
{ + update( + "states.instanceShaderPacksPage.accordionStates", + accordionStates.toSpliced(0, 1, isOpen) + ); + }} + > + + {selectableCardItems.map((item, index) => ( + ))} - } - > - {isLoading ? ( -
- -
- ) : shaderPacks.length > 0 ? ( - ( - - - {shaderItemMenuOperations(pack).map((item, index) => ( - - ))} - - - ))} - /> - ) : ( - - )} -
+
+
{ + update( + "states.instanceShaderPacksPage.accordionStates", + accordionStates.toSpliced(1, 1, isOpen) + ); + }} + titleExtra={} + headExtra={ + + {shaderSecMenuOperations.map((btn, index) => ( + + ))} + + } + > + {isLoading ? ( +
+ +
+ ) : shaderPacks.length > 0 ? ( + ( + + + {shaderItemMenuOperations(pack).map((item, index) => ( + + ))} + + + ))} + /> + ) : ( + + )} +
+ ); }; From b85a42688cc4ba3de40000484e24b6a62728e7b9 Mon Sep 17 00:00:00 2001 From: xunying123 Date: Sun, 4 Jan 2026 13:43:12 +0800 Subject: [PATCH 11/12] fix(instance): use bmclapi to download optifine --- .../src/instance/helpers/loader/forge.rs | 3 +++ .../src/instance/helpers/loader/neoforge.rs | 3 +++ .../src/instance/helpers/loader/optifine.rs | 20 ++++++------------- src-tauri/src/instance/models/misc.rs | 2 +- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src-tauri/src/instance/helpers/loader/forge.rs b/src-tauri/src/instance/helpers/loader/forge.rs index a8d856f11..0c9034b16 100644 --- a/src-tauri/src/instance/helpers/loader/forge.rs +++ b/src-tauri/src/instance/helpers/loader/forge.rs @@ -121,6 +121,9 @@ pub async fn download_forge_libraries( ), None, )?); + if !installer_path.exists() { + return Err(InstanceError::LoaderInstallerNotFound.into()); + } let file = File::open(&installer_path)?; let mut archive = ZipArchive::new(file)?; diff --git a/src-tauri/src/instance/helpers/loader/neoforge.rs b/src-tauri/src/instance/helpers/loader/neoforge.rs index 495bde9af..30565cbdb 100644 --- a/src-tauri/src/instance/helpers/loader/neoforge.rs +++ b/src-tauri/src/instance/helpers/loader/neoforge.rs @@ -104,6 +104,9 @@ pub async fn download_neoforge_libraries( ), None, )?); + if !installer_path.exists() { + return Err(InstanceError::LoaderInstallerNotFound.into()); + } let (content, version) = { let file = File::open(&installer_path)?; let mut archive = ZipArchive::new(file)?; diff --git a/src-tauri/src/instance/helpers/loader/optifine.rs b/src-tauri/src/instance/helpers/loader/optifine.rs index 8284d8399..52e7f5f8f 100644 --- a/src-tauri/src/instance/helpers/loader/optifine.rs +++ b/src-tauri/src/instance/helpers/loader/optifine.rs @@ -28,18 +28,11 @@ pub async fn download_optifine_installer( lib_dir: PathBuf, task_params: &mut Vec, ) -> SJMCLResult<()> { - let root = get_download_api(priority[0], ResourceType::OptiFine)?; - - let installer_url = match *priority.first().unwrap_or(&SourceType::Official) { - SourceType::Official => root.join(&format!( - "{}/{}/{}", - game_version, optifine.r#type, optifine.patch - ))?, - SourceType::BMCLAPIMirror => root.join(&format!( - "{}/{}/{}", - game_version, optifine.r#type, optifine.patch - ))?, - }; + let root = get_download_api(SourceType::BMCLAPIMirror, ResourceType::OptiFine)?; + let installer_url = root.join(&format!( + "{}/{}/{}", + game_version, optifine.r#type, optifine.patch + ))?; let installer_coord = format!( "net.minecraftforge:optifine:{}-installer", @@ -174,7 +167,6 @@ async fn download_optifine_libraries( add_library_entry(&mut client_info.libraries, &optifine_runtime_coord, None)?; let lw_main = "net.minecraft.launchwrapper.Launch".to_string(); - // 是否需要考虑到已经有过了 if let Some(v_args) = client_info.arguments.clone() { let mut g: Vec = v_args.game.clone(); let flag = ArgumentsItem { @@ -360,7 +352,7 @@ pub async fn finish_optifine_installer( let optifine_rel = convert_library_name_to_path(&optifine_coord, None)?; let optifine_path = libraries_dir.join(&optifine_rel); if !installer_path.exists() { - return Err(InstanceError::LoaderNotDownloaded.into()); + return Err(InstanceError::LoaderInstallerNotFound.into()); } let f = fs::File::open(&installer_path)?; diff --git a/src-tauri/src/instance/models/misc.rs b/src-tauri/src/instance/models/misc.rs index 0c6c08d26..6ab48a0ec 100644 --- a/src-tauri/src/instance/models/misc.rs +++ b/src-tauri/src/instance/models/misc.rs @@ -272,7 +272,7 @@ pub enum InstanceError { InstallationDuplicated, ProcessorExecutionFailed, SemaphoreAcquireFailed, - LoaderNotDownloaded, + LoaderInstallerNotFound, } #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize, Default)] From f5f3d6ddf7ee9fcc08ca4b2a3c04ff2fb6173fb5 Mon Sep 17 00:00:00 2001 From: Yujie Sun Date: Wed, 7 Jan 2026 16:52:59 +0800 Subject: [PATCH 12/12] fix(optifine): fix some compile, clippy and frontend style error, add missing toast locale --- src-tauri/src/instance/commands.rs | 6 +++--- src-tauri/src/instance/helpers/client_json.rs | 1 + src-tauri/src/instance/helpers/loader/optifine.rs | 5 +++-- src-tauri/src/instance/helpers/misc.rs | 2 +- src/components/loader-selector.tsx | 10 ++++++++-- src/locales/en.json | 3 ++- src/locales/zh-Hans.json | 3 ++- src/pages/instances/details/[id]/mods.tsx | 3 ++- src/pages/instances/details/[id]/shaderpacks.tsx | 11 +++++++---- src/styles/globals.css | 4 ++++ 10 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src-tauri/src/instance/commands.rs b/src-tauri/src/instance/commands.rs index 562497b03..c7082cbb2 100644 --- a/src-tauri/src/instance/commands.rs +++ b/src-tauri/src/instance/commands.rs @@ -6,7 +6,7 @@ use crate::instance::helpers::game_version::{build_game_version_cmp_fn, compare_ use crate::instance::helpers::loader::common::{execute_processors, install_mod_loader}; use crate::instance::helpers::loader::forge::InstallProfile; use crate::instance::helpers::loader::optifine::{ - download_optifine_installer, finish_optifine_installer, + download_optifine_installer, finish_optifine_install, }; use crate::instance::helpers::misc::{ get_instance_game_config, get_instance_subdir_path_by_id, get_instance_subdir_paths, @@ -982,6 +982,7 @@ pub async fn create_instance( task_params .extend(get_invalid_assets(&app, &version_info, priority_list[0], assets_dir, false).await?); + // download loader (installer) if instance.mod_loader.loader_type != ModLoaderType::Unknown { install_mod_loader( app.clone(), @@ -999,7 +1000,6 @@ pub async fn create_instance( if let Some(info) = optifine.as_ref() { download_optifine_installer( - &priority_list, &instance.version, info, libraries_dir.to_path_buf(), @@ -1125,7 +1125,7 @@ pub async fn finish_mod_loader_install(app: AppHandle, instance_id: String) -> S .ok_or(InstanceError::InstanceNotFoundByID)?; instance.optifine.as_mut().unwrap().status = ModLoaderStatus::Installing; }; - finish_optifine_installer(&app, &instance, &client_info).await?; + finish_optifine_install(&app, &instance, &client_info).await?; } let instance = { diff --git a/src-tauri/src/instance/helpers/client_json.rs b/src-tauri/src/instance/helpers/client_json.rs index c04459869..6fbae6348 100644 --- a/src-tauri/src/instance/helpers/client_json.rs +++ b/src-tauri/src/instance/helpers/client_json.rs @@ -491,6 +491,7 @@ pub async fn replace_native_libraries( #[cfg(all(target_arch = "aarch64", target_os = "macos"))] { + use crate::instance::helpers::game_version::compare_game_versions; if compare_game_versions(app, instance.version.as_str(), "1.20.1", true).await == Ordering::Greater { diff --git a/src-tauri/src/instance/helpers/loader/optifine.rs b/src-tauri/src/instance/helpers/loader/optifine.rs index 52e7f5f8f..7ecb70fcf 100644 --- a/src-tauri/src/instance/helpers/loader/optifine.rs +++ b/src-tauri/src/instance/helpers/loader/optifine.rs @@ -21,13 +21,14 @@ use std::process::Command; use std::sync::Mutex; use tauri::{AppHandle, Manager}; use zip::{write::FileOptions, ZipArchive, ZipWriter}; + pub async fn download_optifine_installer( - priority: &[SourceType], game_version: &str, optifine: &OptiFineResourceInfo, lib_dir: PathBuf, task_params: &mut Vec, ) -> SJMCLResult<()> { + // only have BMCLAPI source let root = get_download_api(SourceType::BMCLAPIMirror, ResourceType::OptiFine)?; let installer_url = root.join(&format!( "{}/{}/{}", @@ -330,7 +331,7 @@ async fn run_optifine_patcher( Ok(()) } -pub async fn finish_optifine_installer( +pub async fn finish_optifine_install( app: &AppHandle, instance: &Instance, client_info: &McClientInfo, diff --git a/src-tauri/src/instance/helpers/misc.rs b/src-tauri/src/instance/helpers/misc.rs index d2ab59db5..739b614a7 100644 --- a/src-tauri/src/instance/helpers/misc.rs +++ b/src-tauri/src/instance/helpers/misc.rs @@ -256,7 +256,7 @@ pub async fn refresh_instances( let optifine_installed = cfg_read .optifine .as_ref() - .map_or(false, |o| o.status == ModLoaderStatus::Installed); + .is_some_and(|o| o.status == ModLoaderStatus::Installed); let optifine_filename = optifine_info.as_ref().map(|info| info.filename.clone()); let optifine_version = optifine_info.map(|info| format!("{}_{}", info.r#type, info.patch)); let instance = Instance { diff --git a/src/components/loader-selector.tsx b/src/components/loader-selector.tsx index e06dc9a72..d43a7451c 100644 --- a/src/components/loader-selector.tsx +++ b/src/components/loader-selector.tsx @@ -295,9 +295,15 @@ export const LoaderSelector: React.FC = ({ return ( - + {selectableCardItems.map((item, index) => ( - + ))}
diff --git a/src/locales/en.json b/src/locales/en.json index be2f3ea59..a0dc40145 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -2365,7 +2365,8 @@ "INSTANCE_NOT_FOUND_BY_ID": "Instance ID does not exist", "INSTALLATION_DUPLICATED": "Another installation process is already in progress", "MAIN_CLASS_NOT_FOUND": "Main class not found in the mod loader version", - "PROCESSOR_EXECUTION_FAILED": "Failed to execute the install process" + "PROCESSOR_EXECUTION_FAILED": "Failed to execute the install process", + "LOADER_INSTALLER_NOT_FOUND": "Mod loader installer file not found" } } }, diff --git a/src/locales/zh-Hans.json b/src/locales/zh-Hans.json index 08c87ab60..69ad4e228 100644 --- a/src/locales/zh-Hans.json +++ b/src/locales/zh-Hans.json @@ -2365,7 +2365,8 @@ "INSTANCE_NOT_FOUND_BY_ID": "实例 ID 不存在", "INSTALLATION_DUPLICATED": "检测到另一个进程已在安装中", "MAIN_CLASS_NOT_FOUND": "主类未找到", - "PROCESSOR_EXECUTION_FAILED": "安装任务执行失败" + "PROCESSOR_EXECUTION_FAILED": "安装任务执行失败", + "LOADER_INSTALLER_NOT_FOUND": "加载器安装程序不存在" } } }, diff --git a/src/pages/instances/details/[id]/mods.tsx b/src/pages/instances/details/[id]/mods.tsx index 4e6bc6151..f17af3284 100644 --- a/src/pages/instances/details/[id]/mods.tsx +++ b/src/pages/instances/details/[id]/mods.tsx @@ -49,6 +49,7 @@ import { LocalModInfo } from "@/models/instance/misc"; import { InstanceService } from "@/services/instance"; import { ResourceService } from "@/services/resource"; import { UtilsService } from "@/services/utils"; +import { parseModLoaderVersion } from "@/utils/instance"; import { base64ImgSrc } from "@/utils/string"; const InstanceModsPage = () => { @@ -355,7 +356,7 @@ const InstanceModsPage = () => { iconSrc: `/images/icons/${modLoaderTypesToIcon[type]}`, description: summary?.modLoader.loaderType === type - ? summary?.modLoader.version + ? parseModLoaderVersion(summary?.modLoader.version || "") : t("InstanceModsPage.modLoaderList.notInstalled"), displayMode: "entry", isSelected: summary?.modLoader.loaderType === type, diff --git a/src/pages/instances/details/[id]/shaderpacks.tsx b/src/pages/instances/details/[id]/shaderpacks.tsx index e51e40924..9a9cfc4c0 100644 --- a/src/pages/instances/details/[id]/shaderpacks.tsx +++ b/src/pages/instances/details/[id]/shaderpacks.tsx @@ -118,12 +118,15 @@ const InstanceShaderPacksPage = () => { title: "OptiFine", iconSrc: "/images/icons/OptiFine.png", description: - summary?.optifine?.version || - t("InstanceShaderPacksPage.shaderLoaderList.notInstalled"), + summary?.optifine?.status === "Installed" + ? summary?.optifine?.version + : t("InstanceShaderPacksPage.shaderLoaderList.notInstalled"), displayMode: "entry", - isSelected: summary?.optifine !== undefined, + isSelected: summary?.optifine?.status === "Installed", onSelect: () => {}, - isDisabled: true, // TODO: add OptiFine installation support + // TODO: add OptiFine installation support + isDisabled: true, + isChevronShown: false, }, ]; diff --git a/src/styles/globals.css b/src/styles/globals.css index 1c79edcd9..d15c4a8d2 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -68,6 +68,10 @@ body img { text-overflow: ellipsis; } +.force-break { + word-break: break-all; +} + /* card-style content container */ .content-full-y {