Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 54 additions & 5 deletions src-tauri/src/instance/commands.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::helpers::loader::fabric::remove_fabric_api_mods;
use crate::error::SJMCLResult;
use crate::instance::constants::TRANSLATION_CACHE_EXPIRY_HOURS;
use crate::instance::helpers::client_json::{replace_native_libraries, McClientInfo, PatchesInfo};
use crate::instance::helpers::game_version::{compare_game_versions, get_major_game_version};
use crate::instance::helpers::loader::common::{execute_processors, install_mod_loader};
Expand All @@ -14,6 +15,7 @@ use crate::instance::helpers::modpack::modrinth::ModrinthManifest;
use crate::instance::helpers::modpack::multimc::MultiMcManifest;
use crate::instance::helpers::mods::common::{
add_local_mod_translations, get_mod_info_from_dir, get_mod_info_from_jar,
LocalModTranslationEntry, LocalModTranslationsCache,
};
use crate::instance::helpers::options_txt::get_zh_hans_lang_tag;
use crate::instance::helpers::resourcepack::{
Expand Down Expand Up @@ -48,11 +50,12 @@ use regex::{Regex, RegexBuilder};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
use tauri::{AppHandle, Manager};
use tauri::{AppHandle, Manager, State};
use tauri_plugin_http::reqwest;
use tokio;
use tokio::sync::Semaphore;
use url::Url;
use zip::read::ZipArchive;

Expand Down Expand Up @@ -469,6 +472,7 @@ pub async fn retrieve_game_server_list(
pub async fn retrieve_local_mod_list(
app: AppHandle,
instance_id: String,
local_mod_translations_cache_state: State<'_, Mutex<LocalModTranslationsCache>>,
) -> SJMCLResult<Vec<LocalModInfo>> {
let mods_dir = match get_instance_subdir_path_by_id(&app, &instance_id, &InstanceSubdirType::Mods)
{
Expand All @@ -483,16 +487,39 @@ pub async fn retrieve_local_mod_list(

let mod_paths = get_files_with_regex(&mods_dir, &valid_extensions).unwrap_or_default();
let mut tasks = Vec::new();
let semaphore = Arc::new(Semaphore::new(
std::thread::available_parallelism().unwrap().into(),
));
for path in mod_paths {
let task = tokio::spawn(async move { get_mod_info_from_jar(&path).await.ok() });
let permit = semaphore
.clone()
.acquire_owned()
.await
.map_err(|_| InstanceError::SemaphoreAcquireFailed)?;
let task = tokio::spawn(async move {
log::debug!("Load mod info from dir: {}", path.display());
let info = get_mod_info_from_jar(&path).await.ok();
drop(permit);
info
});
tasks.push(task);
}
#[cfg(debug_assertions)]
{
// mod information detection from folders is only used for debugging.
let mod_paths = get_subdirectories(&mods_dir).unwrap_or_default();
for path in mod_paths {
let task = tokio::spawn(async move { get_mod_info_from_dir(&path).await.ok() });
let permit = semaphore
.clone()
.acquire_owned()
.await
.map_err(|_| InstanceError::SemaphoreAcquireFailed)?;
let task = tokio::spawn(async move {
log::debug!("Load mod info from dir: {}", path.display());
let info = get_mod_info_from_dir(&path).await.ok();
drop(permit);
info
});
tasks.push(task);
}
}
Expand Down Expand Up @@ -530,8 +557,15 @@ pub async fn retrieve_local_mod_list(
let mut translation_tasks = Vec::new();
for mut mod_info in mod_infos {
let app = app.clone();
let permit = semaphore
.clone()
.acquire_owned()
.await
.map_err(|_| InstanceError::SemaphoreAcquireFailed)?;
let task = tokio::spawn(async move {
log::debug!("Translating mod: {}", mod_info.file_name);
let _ = add_local_mod_translations(&app, &mut mod_info).await;
drop(permit);
mod_info
});
translation_tasks.push(task);
Expand All @@ -542,9 +576,24 @@ pub async fn retrieve_local_mod_list(
mod_infos.push(mod_info);
}
}

// sort by name (and version)
mod_infos.sort();
let mut cache = local_mod_translations_cache_state.lock()?;
for info in mod_infos.iter() {
if let Some(entry) = cache.translations.get(&info.file_name) {
if !entry.is_expired(TRANSLATION_CACHE_EXPIRY_HOURS) {
continue;
}
}
cache.translations.insert(
info.file_name.clone(),
LocalModTranslationEntry::new(
info.translated_name.clone(),
info.translated_description.clone(),
),
);
}
cache.save()?;

Ok(mod_infos)
}
Expand Down
2 changes: 2 additions & 0 deletions src-tauri/src/instance/constants.rs
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
pub const INSTANCE_CFG_FILE_NAME: &str = "sjmclcfg.json";
pub const TRANSLATION_CACHE_EXPIRY_HOURS: u64 = 24;
pub const TRANSLATION_CACHE_FILE_NAME: &str = "local_mod_translations.json";
101 changes: 36 additions & 65 deletions src-tauri/src/instance/helpers/mods/common.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::error::{SJMCLError, SJMCLResult};
use crate::instance::constants::{TRANSLATION_CACHE_EXPIRY_HOURS, TRANSLATION_CACHE_FILE_NAME};
use crate::instance::helpers::mods::{fabric, forge, legacy_forge, liteloader, quilt};
use crate::instance::models::misc::{LocalModInfo, ModLoaderType};
use crate::resource::helpers::curseforge::{
Expand All @@ -7,31 +8,43 @@ use crate::resource::helpers::curseforge::{
use crate::resource::helpers::modrinth::{
fetch_remote_resource_by_id_modrinth, fetch_remote_resource_by_local_modrinth,
};
use crate::storage::Storage;
use crate::utils::image::{load_image_from_dir_async, load_image_from_jar};
use crate::APP_DATA_DIR;
use log::info;
use serde::{Deserialize, Serialize};
use std::io::Cursor;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
use tauri::{AppHandle, Manager};
use tokio::fs;
use zip::ZipArchive;

// Cache structure for local mod translations
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
struct LocalModTranslationsCache {
pub struct LocalModTranslationsCache {
#[serde(flatten)]
translations: std::collections::HashMap<String, LocalModTranslationEntry>,
pub translations: std::collections::HashMap<String, LocalModTranslationEntry>,
}

impl Storage for LocalModTranslationsCache {
fn file_path() -> PathBuf {
APP_DATA_DIR
.get()
.unwrap()
.join(TRANSLATION_CACHE_FILE_NAME)
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct LocalModTranslationEntry {
translated_name: Option<String>,
translated_description: Option<String>,
timestamp: u64,
pub struct LocalModTranslationEntry {
pub translated_name: Option<String>,
pub translated_description: Option<String>,
pub timestamp: u64,
}

impl LocalModTranslationEntry {
fn new(translated_name: Option<String>, translated_description: Option<String>) -> Self {
pub fn new(translated_name: Option<String>, translated_description: Option<String>) -> Self {
Self {
translated_name,
translated_description,
Expand All @@ -42,7 +55,7 @@ impl LocalModTranslationEntry {
}
}

fn is_expired(&self, max_age_hours: u64) -> bool {
pub fn is_expired(&self, max_age_hours: u64) -> bool {
let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
Expand All @@ -51,37 +64,6 @@ impl LocalModTranslationEntry {
}
}

async fn load_local_mod_translations_cache(app: &AppHandle) -> LocalModTranslationsCache {
let cache_path = match app.path().app_cache_dir() {
Ok(cache_dir) => cache_dir.join("local_mod_translations.json"),
Err(_) => return LocalModTranslationsCache::default(),
};

let content = match fs::read_to_string(&cache_path).await {
Ok(content) => content,
Err(_) => return LocalModTranslationsCache::default(),
};

serde_json::from_str(&content).unwrap_or_else(|_| LocalModTranslationsCache::default())
}

async fn save_local_mod_translations_cache(
app: &AppHandle,
cache: &LocalModTranslationsCache,
) -> bool {
let cache_path = match app.path().app_cache_dir() {
Ok(cache_dir) => cache_dir.join("local_mod_translations.json"),
Err(_) => return false,
};

let content = match serde_json::to_string_pretty(cache) {
Ok(content) => content,
Err(_) => return false,
};

fs::write(cache_path, content).await.is_ok()
}

pub async fn get_mod_info_from_jar(path: &PathBuf) -> SJMCLResult<LocalModInfo> {
let file = Cursor::new(tokio::fs::read(path).await?);
let file_name = path.file_name().unwrap().to_string_lossy().to_string();
Expand Down Expand Up @@ -312,13 +294,17 @@ pub async fn add_local_mod_translations(
app: &AppHandle,
mod_info: &mut LocalModInfo,
) -> SJMCLResult<()> {
const CACHE_EXPIRY_HOURS: u64 = 24;

let cache = {
let translation_cache_state = app.state::<Mutex<LocalModTranslationsCache>>();
let cache = translation_cache_state.lock()?.clone();
cache
};
let file_path = mod_info.file_path.to_string_lossy().to_string();
let cache = load_local_mod_translations_cache(app).await;
let file_name = mod_info.file_name.clone();
Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary clone operation. The file_name field is already a String and is only used for lookups and logging. Consider borrowing it instead of cloning, or use &mod_info.file_name directly in subsequent operations.

Copilot uses AI. Check for mistakes.

if let Some(entry) = cache.translations.get(&file_path) {
if !entry.is_expired(CACHE_EXPIRY_HOURS) {
if let Some(entry) = cache.translations.get(&file_name) {
if !entry.is_expired(TRANSLATION_CACHE_EXPIRY_HOURS) {
info!("Using cached translation for mod: {}", file_name);
mod_info.translated_name = entry.translated_name.clone();
mod_info.translated_description = entry.translated_description.clone();
return Ok(());
Expand Down Expand Up @@ -358,25 +344,10 @@ pub async fn add_local_mod_translations(
_ => None,
};

let resource_info = match final_result {
Some(data) => data,
None => return Ok(()),
};

mod_info.translated_name = resource_info.translated_name.clone();
mod_info.translated_description = resource_info.translated_description.clone();

// Save to cache
let mut cache = load_local_mod_translations_cache(app).await;
cache.translations.insert(
file_path.clone(),
LocalModTranslationEntry::new(
resource_info.translated_name,
resource_info.translated_description,
),
);

save_local_mod_translations_cache(app, &cache).await;

if let Some(resource_info) = final_result {
info!("Fetched translation for mod: {}", file_name);
mod_info.translated_name = resource_info.translated_name.clone();
mod_info.translated_description = resource_info.translated_description.clone();
}
Ok(())
}
1 change: 1 addition & 0 deletions src-tauri/src/instance/models/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ pub enum InstanceError {
MainClassNotFound,
InstallationDuplicated,
ProcessorExecutionFailed,
SemaphoreAcquireFailed,
}

impl std::error::Error for InstanceError {}
4 changes: 4 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod utils;
use account::helpers::authlib_injector::info::refresh_and_update_auth_servers;
use account::models::AccountInfo;
use instance::helpers::misc::refresh_and_update_instances;
use instance::helpers::mods::common::LocalModTranslationsCache;
use instance::models::misc::Instance;
use launch::models::LaunchingState;
use launcher_config::helpers::java::refresh_and_update_javas;
Expand Down Expand Up @@ -191,6 +192,9 @@ pub async fn run() {

app.manage(Box::pin(TaskMonitor::new(app.handle().clone())));

let local_mod_translations = LocalModTranslationsCache::load().unwrap_or_default();
app.manage(Mutex::new(local_mod_translations));

let client = build_sjmcl_client(app.handle(), true, false);
app.manage(client);

Expand Down
Loading