From 341acc9071ccae03e4402881b54f6d66f3db30ad Mon Sep 17 00:00:00 2001 From: neoapps-dev Date: Thu, 23 Apr 2026 15:44:00 +0300 Subject: [PATCH] feat: local custom TUs support --- src-tauri/src/lib.rs | 69 ++++++- src/components/modals/CustomTUModal.tsx | 55 ++++-- src/components/views/VersionsView.tsx | 243 ++++++++++++++---------- src/hooks/useGameManager.ts | 4 +- src/services/TauriService.ts | 5 + 5 files changed, 249 insertions(+), 127 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5a0f048..877fc6c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -35,6 +35,7 @@ pub struct CustomEdition { pub name: String, pub desc: String, pub url: String, + pub path: Option, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -146,10 +147,10 @@ fn find_executable_recursive(root: &PathBuf, file_name: &str) -> Option None } -fn is_macos_runtime_installed(app: &AppHandle) -> bool { +fn is_macos_runtime_installed(_app: &AppHandle) -> bool { #[cfg(target_os = "macos")] { - let runtime_dir = get_macos_runtime_dir(app); + let runtime_dir = get_macos_runtime_dir(_app); let toolkit_dir = runtime_dir.join("toolkit"); if !toolkit_dir.exists() { return false; @@ -280,6 +281,19 @@ fn import_theme(app: AppHandle) -> Result { } } +#[tauri::command] +fn pick_folder() -> Result { + let folder = rfd::FileDialog::new() + .set_title("Select Custom TU Folder") + .pick_folder(); + + if let Some(path) = folder { + Ok(path.to_string_lossy().to_string()) + } else { + Err("CANCELED".into()) + } +} + #[tauri::command] fn get_available_runners(app: AppHandle) -> Vec { let mut runners = Vec::new(); @@ -456,13 +470,36 @@ async fn download_runner(app: AppHandle, state: State<'_, DownloadState>, name: #[tauri::command] #[allow(non_snake_case)] fn check_game_installed(app: AppHandle, instance_id: String) -> bool { + let config = load_config(app.clone()); + if let Some(ref editions) = config.custom_editions { + if let Some(edition) = editions.iter().find(|e| e.id == instance_id) { + if let Some(ref path) = edition.path { + return PathBuf::from(path).join("Minecraft.Client.exe").exists(); + } + } + } get_app_dir(&app).join("instances").join(&instance_id).join("Minecraft.Client.exe").exists() } #[tauri::command] #[allow(non_snake_case)] fn open_instance_folder(app: AppHandle, instance_id: String) { - let dir = get_app_dir(&app).join("instances").join(&instance_id); + let config = load_config(app.clone()); + let dir = if let Some(ref editions) = config.custom_editions { + if let Some(edition) = editions.iter().find(|e| e.id == instance_id) { + if let Some(ref path) = edition.path { + Some(PathBuf::from(path)) + } else { + None + } + } else { + None + } + } else { + None + }; + + let dir = dir.unwrap_or_else(|| get_app_dir(&app).join("instances").join(&instance_id)); if dir.exists() { let _ = app.opener().open_path(dir.to_str().unwrap(), None::<&str>); } @@ -471,6 +508,15 @@ fn open_instance_folder(app: AppHandle, instance_id: String) { #[tauri::command] #[allow(non_snake_case)] fn delete_instance(app: AppHandle, instance_id: String) -> Result<(), String> { + let config = load_config(app.clone()); + if let Some(ref editions) = config.custom_editions { + if let Some(edition) = editions.iter().find(|e| e.id == instance_id) { + if edition.path.is_some() { + // Do not delete files for custom path instances + return Ok(()); + } + } + } let dir = get_app_dir(&app).join("instances").join(&instance_id); if dir.exists() { let _ = fs::remove_dir_all(dir); @@ -1187,7 +1233,18 @@ async fn launch_game(app: AppHandle, state: State<'_, GameState>, instance_id: S } let _ = perform_dlc_sync(&app, &instance_dir)?; - let game_exe = instance_dir.join("Minecraft.Client.exe"); + + let mut custom_path = None; + if let Some(ref editions) = config.custom_editions { + if let Some(edition) = editions.iter().find(|e| e.id == instance_id) { + if let Some(ref path) = edition.path { + custom_path = Some(PathBuf::from(path)); + } + } + } + + let working_dir = custom_path.unwrap_or_else(|| instance_dir.clone()); + let game_exe = working_dir.join("Minecraft.Client.exe"); if !game_exe.exists() { return Err("Game executable not found in instance folder.".into()); } @@ -1226,7 +1283,7 @@ async fn launch_game(app: AppHandle, state: State<'_, GameState>, instance_id: S } cmd.arg(&game_exe) - .current_dir(&instance_dir); + .current_dir(&working_dir); let child = cmd.spawn().map_err(|e| e.to_string())?; { @@ -1532,7 +1589,7 @@ pub fn run() { } } }) - .invoke_handler(tauri::generate_handler![setup_macos_runtime, launch_game, stop_game, check_game_installed, save_config, load_config, download_and_install, open_instance_folder, cancel_download, get_available_runners, get_external_palettes, import_theme, download_runner, delete_instance, sync_dlc, fetch_skin, workshop_install, workshop_uninstall, workshop_list_installed, get_screenshots, delete_screenshot, open_screenshot_folder, save_global_skin_pck, check_game_update, check_macos_runtime_installed, check_macos_runtime_installed_fast]) + .invoke_handler(tauri::generate_handler![setup_macos_runtime, launch_game, stop_game, check_game_installed, save_config, load_config, download_and_install, open_instance_folder, cancel_download, get_available_runners, get_external_palettes, import_theme, pick_folder, download_runner, delete_instance, sync_dlc, fetch_skin, workshop_install, workshop_uninstall, workshop_list_installed, get_screenshots, delete_screenshot, open_screenshot_folder, save_global_skin_pck, check_game_update, check_macos_runtime_installed, check_macos_runtime_installed_fast]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src/components/modals/CustomTUModal.tsx b/src/components/modals/CustomTUModal.tsx index 776b638..20e4859 100644 --- a/src/components/modals/CustomTUModal.tsx +++ b/src/components/modals/CustomTUModal.tsx @@ -18,9 +18,8 @@ const ModalButton = memo(function ModalButton({ onClick={onClick} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} - className={`flex-1 h-12 flex items-center justify-center text-xl mc-text-shadow transition-colors outline-none border-none bg-transparent ${ - isDanger ? "text-red-500" : "text-white" - } ${isHovered ? (isDanger ? "text-red-400" : "text-[#FFFF55]") : ""}`} + className={`flex-1 h-12 flex items-center justify-center text-xl mc-text-shadow transition-colors outline-none border-none bg-transparent ${isDanger ? "text-red-500" : "text-white" + } ${isHovered ? (isDanger ? "text-red-400" : "text-[#FFFF55]") : ""}`} style={{ backgroundImage: isHovered ? "url('/images/button_highlighted.png')" @@ -41,10 +40,12 @@ export default function CustomTUModal({ playPressSound, playBackSound, editingEdition = null, + initialPath = "", }: any) { const [name, setName] = useState(""); const [desc, setDesc] = useState(""); const [url, setUrl] = useState(""); + const [path, setPath] = useState(""); const [error, setError] = useState(""); const [focusIndex, setFocusIndex] = useState(0); @@ -53,13 +54,17 @@ export default function CustomTUModal({ setName(editingEdition.name); setDesc(editingEdition.desc); setUrl(editingEdition.url); + setPath(editingEdition.path || ""); + } else if (isOpen && initialPath) { + setPath(initialPath); } else if (!isOpen) { setName(""); setDesc(""); setUrl(""); + setPath(""); setError(""); } - }, [editingEdition, isOpen]); + }, [editingEdition, isOpen, initialPath]); useEffect(() => { if (!isOpen) { @@ -88,25 +93,30 @@ export default function CustomTUModal({ }; window.addEventListener("keydown", handleKey); return () => window.removeEventListener("keydown", handleKey); - }, [isOpen, focusIndex, name, desc, url]); + }, [isOpen, focusIndex, name, desc, url, path]); if (!isOpen) return null; const handleImport = () => { - if (!name || !url) { - setError("Name and URL are required"); + if (!name) { + setError("Name is required"); return; } - if (!url.startsWith("http")) { + if (!url && !path) { + setError("URL or Path is required"); + return; + } + if (url && !url.startsWith("http")) { setError("Invalid URL"); return; } setError(""); - onImport({ name, desc: desc || "Custom imported TU", url }); + onImport({ name, desc: desc || "Custom imported TU", url, path: path || undefined }); onClose(); setName(""); setDesc(""); setUrl(""); + setPath(""); }; return ( @@ -114,19 +124,19 @@ export default function CustomTUModal({
-

+

{editingEdition ? "Edit Custom TU" : "Import Custom TU"}

-