diff --git a/package-lock.json b/package-lock.json index 2c337ea..c899b04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@tailwindcss/vite": "^4.1.18", "@tauri-apps/api": "^2.10.1", "@tauri-apps/plugin-dialog": "^2.6.0", + "@tauri-apps/plugin-fs": "^2.4.5", "@tauri-apps/plugin-opener": "^2.5.3", "@tauri-apps/plugin-os": "~2.3.2", "reka-ui": "^2.8.0", @@ -1545,6 +1546,15 @@ "@tauri-apps/api": "^2.8.0" } }, + "node_modules/@tauri-apps/plugin-fs": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.4.5.tgz", + "integrity": "sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, "node_modules/@tauri-apps/plugin-opener": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.3.tgz", diff --git a/package.json b/package.json index 875dc42..aefb8ea 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@tailwindcss/vite": "^4.1.18", "@tauri-apps/api": "^2.10.1", "@tauri-apps/plugin-dialog": "^2.6.0", + "@tauri-apps/plugin-fs": "^2.4.5", "@tauri-apps/plugin-opener": "^2.5.3", "@tauri-apps/plugin-os": "~2.3.2", "reka-ui": "^2.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d544384..b51f85f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@tauri-apps/plugin-dialog': specifier: ^2.6.0 version: 2.6.0 + '@tauri-apps/plugin-fs': + specifier: ^2.4.5 + version: 2.4.5 '@tauri-apps/plugin-opener': specifier: ^2.5.3 version: 2.5.3 @@ -615,6 +618,9 @@ packages: '@tauri-apps/plugin-dialog@2.6.0': resolution: {integrity: sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==} + '@tauri-apps/plugin-fs@2.4.5': + resolution: {integrity: sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA==} + '@tauri-apps/plugin-opener@2.5.3': resolution: {integrity: sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==} @@ -1428,6 +1434,10 @@ snapshots: dependencies: '@tauri-apps/api': 2.10.1 + '@tauri-apps/plugin-fs@2.4.5': + dependencies: + '@tauri-apps/api': 2.10.1 + '@tauri-apps/plugin-opener@2.5.3': dependencies: '@tauri-apps/api': 2.10.1 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 3a961f5..fe23980 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1696,6 +1696,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + [[package]] name = "httparse" version = "1.10.1" @@ -2325,6 +2331,7 @@ dependencies = [ "tauri-build", "tauri-plugin-decorum", "tauri-plugin-dialog", + "tauri-plugin-fs", "tauri-plugin-opener", "tauri-plugin-os", ] @@ -4010,6 +4017,7 @@ dependencies = [ "gtk", "heck 0.5.0", "http", + "http-range", "jni", "libc", "log", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index fce7d82..50f0505 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -12,7 +12,7 @@ crate-type = ["staticlib", "cdylib", "rlib"] tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2.10.1", features = [] } +tauri = { version = "2.10.1", features = ["protocol-asset"] } tauri-plugin-opener = "2" tauri-plugin-dialog = "2.6.0" serde = { version = "1", features = ["derive"] } @@ -22,3 +22,4 @@ colog = "1.4.0" serini = "0.2.2" tauri-plugin-decorum = "1.1.1" tauri-plugin-os = "2" +tauri-plugin-fs = "2" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 2181336..892b6a7 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -2,9 +2,7 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Capability for the main window", - "windows": [ - "main" - ], + "windows": ["main"], "permissions": [ "core:default", "opener:default", @@ -20,6 +18,7 @@ "core:window:allow-start-dragging", "core:window:allow-toggle-maximize", "decorum:allow-show-snap-overlay", - "os:default" + "os:default", + "fs:default" ] -} \ No newline at end of file +} diff --git a/src-tauri/src/engines.rs b/src-tauri/src/engines.rs index 768a76b..3437bcf 100644 --- a/src-tauri/src/engines.rs +++ b/src-tauri/src/engines.rs @@ -37,6 +37,7 @@ pub struct EngineContext { pub folder_path: String, pub exe_path: String, pub engine_kind: EngineKind, + pub exe_type: ExecutableType, } /// Acesss the registery using: @@ -75,6 +76,7 @@ impl EngineRegistery { folder_path: string_path.clone(), exe_path: string_path, engine_kind: EngineKind::Executable, + exe_type: ExecutableType::Platform, }; for (engine_kind, detector) in &self.detectors { @@ -86,8 +88,9 @@ impl EngineRegistery { // Sort by engine kind, the highest one wins if we have multiple found_kinds.sort_by_key(|a| *a.0); - if let Some((final_kind, exe_kind)) = found_kinds.last() { + if let Some((final_kind, exe_type)) = found_kinds.last() { base_context.engine_kind = **final_kind; + base_context.exe_type = *exe_type; // Update any context properties that may change from detector to detector // Executable detector sets the exe_path internally due to the nature of how it detects @@ -98,7 +101,7 @@ impl EngineRegistery { // Manually set the exe_path for other engines if base_context.engine_kind != EngineKind::Executable { - let exe_extenstion = if exe_kind == &ExecutableType::Platform { + let exe_extenstion = if base_context.exe_type == ExecutableType::Platform { EXE_EXTENSION } else { WINDOWS_EXE_EXTENSION @@ -202,8 +205,9 @@ impl EngineDetector for ExecutableDetector { let folder_path = Path::new(&context.folder_path); let mut exe_filename = "".to_owned(); - if let Some((_, exe_path)) = check_for_any_executable(folder_path) { + if let Some((exe_type, exe_path)) = check_for_any_executable(folder_path) { context.exe_path = exe_path.to_string_lossy().to_string(); + context.exe_type = exe_type; exe_filename = exe_path.file_stem().unwrap().to_string_lossy().to_string(); } @@ -472,6 +476,8 @@ pub struct EngineCanonInfo { pub canonical_icon_path: Option, pub canonical_banner_path: Option, + pub canonical_launch_args: Option, + pub canonical_command: Option, } #[tauri::command] @@ -511,6 +517,13 @@ pub fn get_engine_contexts_internal(context_store: &ContextStore) -> Vec Vec>, + folder_path: String, +) -> Result<(), String> { + let mut store = context_store.lock().unwrap(); + remove_engine_context_internal(&mut store, folder_path) +} + +pub fn remove_engine_context_internal( + context_store: &mut ContextStore, + folder_path: String, +) -> Result<(), String> { + if let Some(_) = context_store.engine_list.remove(&folder_path) { + context_store.mods_list.remove(&folder_path); + Ok(()) + } else { + Err("No engine found at the specified folder!".to_string()) + } +} diff --git a/src-tauri/src/instances.rs b/src-tauri/src/instances.rs index 6dc6b57..582c2c1 100644 --- a/src-tauri/src/instances.rs +++ b/src-tauri/src/instances.rs @@ -65,6 +65,7 @@ pub struct EditableInstanceInfo { pub color_hex: Option, pub icon_path: Option, pub banner_path: Option, + pub launch_command: Option, pub launch_args: Option, } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 54d2ad1..85530e1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -30,12 +30,12 @@ struct InstanceStore { json_save_path: String, instance_list: InstanceList, } - #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { colog::init(); tauri::Builder::default() + .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_decorum::init()) @@ -86,9 +86,13 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ // utils.rs utils::show_main_window, + utils::get_dir_size, + utils::launch_executable, + utils::has_non_directory_path, // engines.rs engines::get_engine_contexts, engines::add_engine_context, + engines::remove_engine_context, // mods.rs mods::get_mod_contexts_in_folder, // instances.rs @@ -98,7 +102,7 @@ pub fn run() { instances::add_child_instance, instances::remove_child_instance, instances::edit_instance, - instances::remove_instance, + instances::remove_instance ]) .plugin(tauri_plugin_opener::init()) .run(tauri::generate_context!()) diff --git a/src-tauri/src/paths.rs b/src-tauri/src/paths.rs index ad4dbe8..2167c25 100644 --- a/src-tauri/src/paths.rs +++ b/src-tauri/src/paths.rs @@ -1,5 +1,5 @@ +use serde::Serialize; use std::{ - env::consts::EXE_EXTENSION, fs, path::{Path, PathBuf}, }; @@ -7,8 +7,12 @@ use std::{ /// Windows EXE suffix for unix systems where EXE_SUFFIX is empty, used for wine support pub const WINDOWS_EXE_EXTENSION: &str = "exe"; +// This is used to exclude certain file types from being detected as executables. This will match any part of the file extension! +// For example, "file.so.lol" will be excluded because it contains "so" +pub const EXCLUDED_EXE_FILE_EXTENSIONS: [&str; 2] = ["ndll", "so"]; + /// Allows engine detectors to find out what type of executable they found, used for wine support -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] pub enum ExecutableType { Platform, #[allow(dead_code)] @@ -30,6 +34,63 @@ pub fn get_paths_in_folder(folder_path: &Path) -> Vec { vec![] } +fn get_executable_type(path: &Path) -> Option { + if path.is_dir() { + return None; + } + + #[cfg(target_family = "unix")] + { + // First check if it's an ELF binary (with exclusions) + let valid_elf = loop { + if let Some(ext) = path.extension() { + if let Some(ext_str) = ext.to_str() { + if EXCLUDED_EXE_FILE_EXTENSIONS.contains(&ext_str) { + log::info!( + "Excluded file extension found: {} ({}), skipping ELF check", + ext_str, + path.display() + ); + break false; + } + } + } + break true; + }; + + if valid_elf { + if let Ok(mut file) = fs::File::open(path) { + use std::io::Read; + let mut magic_buf = [0u8; 4]; + if file.read_exact(&mut magic_buf).is_ok() && &magic_buf == b"\x7FELF" { + log::info!("ELF executable found: {}", path.display()); + return Some(ExecutableType::Platform); + } + } + } + + // if not an ELF, check if it has a windows exe suffix for wine support + if let Some(ext) = path.extension() { + if ext == WINDOWS_EXE_EXTENSION { + log::info!("Windows executable found: {}", path.display()); + return Some(ExecutableType::WindowsOnUnix); + } + } + } + + #[cfg(target_family = "windows")] + { + if let Some(ext) = path.extension() { + if ext == "exe" { + log::info!("Windows executable found: {}", path.display()); + return Some(ExecutableType::Platform); + } + } + } + + None +} + pub fn get_folders_in_folder(folder_path: &Path) -> Vec { let folders = get_paths_in_folder(folder_path); let mut found_folders = vec![]; @@ -51,20 +112,8 @@ pub fn check_for_specific_executable(folder_path: &Path, name: String) -> Option for file in paths { if let Some(file_stem) = file.file_stem() { if !file.is_dir() && *file_stem == *name { - if let Some(ext) = file.extension() { - let is_platform_executable = *ext == *EXE_EXTENSION; - - if is_platform_executable { - return Some(ExecutableType::Platform); - } else { - // A check for .exe on unix systems, since some users use Wine to run the engines - #[cfg(target_family = "unix")] - { - if *ext == *WINDOWS_EXE_EXTENSION { - return Some(ExecutableType::WindowsOnUnix); - } - } - } + if let Some(executable_type) = get_executable_type(file.as_path()) { + return Some(executable_type); } } } @@ -79,22 +128,8 @@ pub fn check_for_any_executable(folder_path: &Path) -> Option<(ExecutableType, P let mut found_executables: Vec<(ExecutableType, PathBuf)> = vec![]; for file in paths { - if let Some(ext) = file.extension() { - if !file.is_dir() { - let is_platform_executable = *ext == *EXE_EXTENSION; - - if is_platform_executable { - found_executables.push((ExecutableType::Platform, file)); - } else { - // A check for .exe on unix systems, since some users use Wine to run the engines - #[cfg(target_family = "unix")] - { - if *ext == *WINDOWS_EXE_EXTENSION { - found_executables.push((ExecutableType::WindowsOnUnix, file)); - } - } - } - } + if let Some(executable_type) = get_executable_type(file.as_path()) { + found_executables.push((executable_type, file)); } } diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 647f5da..b1abe2a 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -4,3 +4,85 @@ pub fn show_main_window(window: tauri::Window) { window.show().unwrap(); } + +#[tauri::command] +pub async fn get_dir_size(folder_path: String) -> u64 { + fn walk(path: &std::path::Path) -> u64 { + match std::fs::read_dir(path) { + Err(e) => { + log::warn!("get_dir_size: cannot read {:?}: {}", path, e); + 0 + } + Ok(entries) => entries + .filter_map(|e| e.ok()) + .map(|e| { + let p = e.path(); + if p.is_dir() { + walk(&p) + } else { + e.metadata().map(|m| m.len()).unwrap_or(0) + } + }) + .sum(), + } + } + let path = std::path::Path::new(&folder_path); + log::info!( + "get_dir_size called with: {:?} (exists: {})", + folder_path, + path.exists() + ); + walk(path) +} + +#[tauri::command] +pub fn launch_executable( + working_dir: String, + executable_path: String, + launcher_command: Option, + launch_args: Option, +) -> Result<(), String> { + let working_dir = std::path::Path::new(working_dir.trim()); + let executable_path = std::path::Path::new(executable_path.trim()); + let command = launcher_command + .as_ref() + .map(|s| s.trim()) + .filter(|s| !s.is_empty()); + let args = launch_args + .as_ref() + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(|s| s.split_whitespace().collect::>()) + .unwrap_or_default(); + + log::info!( + "launch_executable called with: working_dir={:?}, executable_path={:?}, launcher_command={:?}, launch_args={:?}", + working_dir, + executable_path, + command, + args + ); + + let mut process = if let Some(launcher) = command { + let mut cmd = std::process::Command::new(launcher); + cmd.arg(executable_path); + cmd + } else { + std::process::Command::new(executable_path) + }; + + process + .args(args) + .current_dir(working_dir) + .spawn() + .map_err(|e| format!("Failed to launch executable: {}", e))?; + return Ok(()); +} + +#[tauri::command] +pub fn has_non_directory_path(paths: Vec) -> bool { + paths.iter().any(|raw_path| { + let path = std::path::Path::new(raw_path.trim()); + !path.exists() || !path.is_dir() + }) +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 7a23eda..127df39 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -26,7 +26,15 @@ } ], "security": { - "csp": null + "csp": { + "default-src": [ + "'self' ipc: http://ipc.localhost; img-src 'self' asset: http://asset.localhost" + ] + }, + "assetProtocol": { + "enable": true, + "scope": ["**"] + } }, "withGlobalTauri": true }, diff --git a/src/Root.vue b/src/Root.vue index 7f8c60c..a0f72c5 100644 --- a/src/Root.vue +++ b/src/Root.vue @@ -50,7 +50,7 @@ const curCategoryComponent = computed(() => { @reference "./main.css"; #main { - @apply relative isolate px-2 py-4 flex flex-col space-y-2 bg-modfriend-900 overflow-hidden; + @apply relative isolate pl-2 py-4 flex flex-col space-y-2 bg-modfriend-900 overflow-hidden; > :not(.background-layers) { @apply relative z-10; diff --git a/src/assets/images/icon/delete.png b/src/assets/images/icon/delete.png new file mode 100644 index 0000000..8cb3f17 Binary files /dev/null and b/src/assets/images/icon/delete.png differ diff --git a/src/components/Background.vue b/src/components/Background.vue index 765f0f7..5c76a54 100644 --- a/src/components/Background.vue +++ b/src/components/Background.vue @@ -14,8 +14,20 @@ watch( () => globalStore.background, (background, oldBackground) => { if (oldBackgroundRef.value && backgroundRef.value) { - oldBackgroundRef.value.style.backgroundImage = `url(${oldBackground})`; - backgroundRef.value.style.backgroundImage = `url(${background})`; + if (background.startsWith('#')) { + backgroundRef.value.style.backgroundColor = background; + backgroundRef.value.style.backgroundImage = 'none'; + } else { + backgroundRef.value.style.backgroundColor = 'transparent'; + backgroundRef.value.style.backgroundImage = `url(${background})`; + } + if (oldBackground.startsWith('#')) { + oldBackgroundRef.value.style.backgroundColor = oldBackground; + oldBackgroundRef.value.style.backgroundImage = 'none'; + } else { + oldBackgroundRef.value.style.backgroundColor = 'transparent'; + oldBackgroundRef.value.style.backgroundImage = `url(${oldBackground})`; + } oldBackgroundRef.value.animate([{ opacity: 1 }, { opacity: 0 }], { duration: props.duration ?? 300, @@ -63,8 +75,8 @@ watch( mask-position: right top; mask-size: contain; mask-repeat: no-repeat; - background-position: right top; - background-size: contain; + background-position: center; + background-size: cover; background-repeat: no-repeat; } diff --git a/src/components/library/InstanceSettingsPopover.vue b/src/components/library/InstanceSettingsPopover.vue new file mode 100644 index 0000000..8ac6b35 --- /dev/null +++ b/src/components/library/InstanceSettingsPopover.vue @@ -0,0 +1,185 @@ + + +