From 7414efce4751817aea22c071d3087e21a8dfb57d Mon Sep 17 00:00:00 2001 From: neoapps-dev Date: Thu, 30 Apr 2026 22:51:28 +0300 Subject: [PATCH] feat: initial support for steam integration --- public/images/steam.png | Bin 0 -> 3052 bytes src-tauri/Cargo.lock | 52 ++++++++ src-tauri/Cargo.toml | 1 + src-tauri/src/lib.rs | 144 ++++++++++++++++++++- src-tauri/src/main.rs | 2 + src/components/common/AchievementToast.tsx | 13 +- src/components/views/VersionsView.tsx | 111 +++++++++++----- src/context/LauncherContext.tsx | 2 +- src/hooks/useGameManager.ts | 17 +++ src/pages/App.tsx | 7 + src/services/TauriService.ts | 9 ++ 11 files changed, 316 insertions(+), 42 deletions(-) create mode 100644 public/images/steam.png diff --git a/public/images/steam.png b/public/images/steam.png new file mode 100644 index 0000000000000000000000000000000000000000..b1639373b41c32727578d921b0645dc7b8288171 GIT binary patch literal 3052 zcmeAS@N?(olHy`uVBq!ia0y~yV44END>&GIA}7o9UokLnZ}xO?45_&F_J()SAqO7U ziyBvd-``}E%EZa|`B(8dCJp7=Z=-+xVghO)5k!PbGU}}DWw`+h7iX(JfO2eU|q!Fq}Q;m4oJ665IxJ2#h{-ieuSjHwFf&G*vUSn8FioFgL+y zn3rI5Vk(P%wlc$F=N6?Pu5X&zAo)lZwVi z=oa+8HDm0a6Pa5VU0h##{_cHiYNnDlK?a}IJqfRWUvxXAM@@r|vF^WljEljbH2$h} z{h2mMsKK%)C~%Q;IVf;p370^5qL3rOSg_EDso^g*vmmcBL*5iYAJhG1^~)dC9gwAF zmf3p1blYu~b1ZX~-{U{e{n_q3H4O*n#{ni1#L!6|EZfqDk7yD({Fv| zdGNc?NE)eG1g&C-Z4RN8ehnLpnqF=ZWN2uM?4)L`B6;xJEe?hY95E-UT4Xe+Zxv+t zvFxxeRYQQu4A_wQk#;DZnkD{J!2|AF1s_N=y*Wm`ARw_x4N~4v|C_OXuic;T943k& NzNf37%Q~loCID~ Result<(), String> { fs::remove_file(path).map_err(|e| e.to_string()) } +#[tauri::command] +async fn add_to_steam( + app: AppHandle, + instance_id: String, + name: String, + title_image: String, + panorama_image: String, +) -> Result<(), String> { + let exe_path = std::env::current_exe().map_err(|e| e.to_string())?; + let exe_str = exe_path.to_string_lossy().to_string(); + let launch_options = format!("\"{}\"", instance_id); + let start_dir = exe_path.parent().map(|p| p.to_string_lossy().to_string()).unwrap_or_default(); + let app_id_32 = steam_shortcuts_util::app_id_generator::calculate_app_id(&exe_str, &name); + let app_id_64 = ((app_id_32 as u64) << 32) | 0x02000000; + let mut userdata_dirs = Vec::new(); + #[cfg(target_os = "linux")] + { + if let Ok(home) = std::env::var("HOME") { + let h = PathBuf::from(home); + userdata_dirs.push(h.join(".steam/steam/userdata")); + userdata_dirs.push(h.join(".local/share/Steam/userdata")); + userdata_dirs.push(h.join(".var/app/com.valvesoftware.Steam/.local/share/Steam/userdata")); + } + } + #[cfg(target_os = "windows")] + { + userdata_dirs.push(PathBuf::from("C:\\Program Files (x86)\\Steam\\userdata")); + } + + let valid_userdata_dirs: Vec = userdata_dirs.into_iter().filter(|d| d.exists()).collect(); + if valid_userdata_dirs.is_empty() { + return Err("Steam userdata directory not found.".into()); + } + + for userdata_root in valid_userdata_dirs { + let entries = fs::read_dir(&userdata_root).map_err(|e| e.to_string())?; + for entry in entries.flatten() { + if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) { + let user_config_dir = entry.path().join("config"); + let shortcuts_path = user_config_dir.join("shortcuts.vdf"); + if !user_config_dir.exists() { continue; } + + let content = if shortcuts_path.exists() { + fs::read(&shortcuts_path).unwrap_or_default() + } else { + Vec::new() + }; + + let shortcuts = if !content.is_empty() { + parse_shortcuts(&content).map_err(|e| e.to_string())? + } else { + Vec::new() + }; + + let mut owned_shortcuts: Vec = shortcuts.iter().map(|s| s.to_owned()).collect(); + + if owned_shortcuts.iter().any(|s| s.app_name == name && s.exe == exe_str) { + continue; + } + + owned_shortcuts.push(steam_shortcuts_util::shortcut::ShortcutOwned { + order: owned_shortcuts.len().to_string(), + app_id: app_id_32, + app_name: name.clone(), + exe: exe_str.clone(), + start_dir: start_dir.clone(), + icon: "".to_string(), + shortcut_path: "".to_string(), + launch_options: launch_options.clone(), + is_hidden: false, + allow_desktop_config: true, + allow_overlay: true, + open_vr: 0, + dev_kit: 0, + dev_kit_game_id: "".to_string(), + dev_kit_overrite_app_id: 0, + last_play_time: 0, + tags: Vec::new(), + }); + + let final_shortcuts: Vec = owned_shortcuts.iter().map(|s| s.borrow()).collect(); + let new_content = shortcuts_to_bytes(&final_shortcuts); + fs::write(&shortcuts_path, new_content).map_err(|e| e.to_string())?; + let grid_dir = user_config_dir.join("grid"); + if !grid_dir.exists() { + let _ = fs::create_dir_all(&grid_dir); + } + + if grid_dir.exists() { + let resource_dir = app.path().resource_dir().unwrap_or_default(); + let resolve_image_path = |p: &str| { + if p.starts_with('/') { + resource_dir.join(&p[1..]) + } else { + PathBuf::from(p) + } + }; + + let title_path = resolve_image_path(&title_image); + let panorama_path = resolve_image_path(&panorama_image); + if let Ok(img_data) = fs::read(&title_path) { + let _ = fs::write(grid_dir.join(format!("{}p.png", app_id_64)), &img_data); + } + if let Ok(img_data) = fs::read(&panorama_path) { + let _ = fs::write(grid_dir.join(format!("{}_hero.png", app_id_64)), &img_data); + let _ = fs::write(grid_dir.join(format!("{}.png", app_id_64)), &img_data); + } + if let Ok(img_data) = fs::read(&title_path) { + let _ = fs::write(grid_dir.join(format!("{}_logo.png", app_id_64)), &img_data); + } + } + } + } + } + + Ok(()) +} + #[tauri::command] fn open_screenshot_folder(app: AppHandle, path: String) { let p = std::path::Path::new(&path); @@ -1708,7 +1827,7 @@ pub fn run() { .register_uri_scheme_protocol("screenshots", |_app, request| { let uri = request.uri().path(); let decoded_path = percent_encoding::percent_decode_str(uri).decode_utf8_lossy(); - let mut path_str = decoded_path.to_string(); + let path_str = decoded_path.to_string(); #[cfg(target_os = "windows")] if path_str.starts_with('/') { path_str = path_str[1..].to_string(); @@ -1731,7 +1850,28 @@ 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, 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, download_logo, pick_file, save_file_dialog, write_binary_file, read_binary_file]) + .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, download_logo, pick_file, save_file_dialog, write_binary_file, read_binary_file, add_to_steam]) + .setup(|app| { + let args: Vec = std::env::args().collect(); + if args.len() > 1 && !args[1].starts_with('-') { + let instance_id = args[1].clone(); + let app_handle = app.handle().clone(); + tauri::async_runtime::spawn(async move { + if let Some(window) = app_handle.get_webview_window("main") { + let _ = window.hide(); + } + let state = app_handle.state::(); + match launch_game(app_handle.clone(), state, instance_id, Vec::new()).await { + Ok(_) => { app_handle.exit(0); } + Err(e) => { + eprintln!("Auto-launch error: {}", e); + app_handle.exit(1); + } + } + }); + } + Ok(()) + }) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 79abeb6..753f13a 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -11,6 +11,7 @@ fn main() { let stage = env::var("EMERALD_LAUNCH_STAGE").unwrap_or_else(|_| "0".to_string()); if stage == "0" { let mut cmd = Command::new(env::current_exe().unwrap()); + cmd.args(env::args().skip(1)); cmd.env("EMERALD_LAUNCH_STAGE", "1"); let wayland_libs = ["/usr/lib64/libwayland-client.so.0", "/usr/lib/libwayland-client.so.0"]; @@ -76,6 +77,7 @@ fn main() { println!("Emerald: Automatic recovery triggered for graphics crash/invisible launch."); } let mut retry_cmd = Command::new(env::current_exe().unwrap()); + retry_cmd.args(env::args().skip(1)); retry_cmd.env("EMERALD_LAUNCH_STAGE", "2") .env("GDK_BACKEND", "x11") .env("WEBKIT_DISABLE_DMABUF_RENDERER", "1") diff --git a/src/components/common/AchievementToast.tsx b/src/components/common/AchievementToast.tsx index a6427dc..ccaae5c 100644 --- a/src/components/common/AchievementToast.tsx +++ b/src/components/common/AchievementToast.tsx @@ -6,7 +6,7 @@ interface AchievementToastProps { onClose: () => void; onClick?: () => void; title?: string; - variant?: "error" | "update"; + variant?: "error" | "update" | "steam"; } export function AchievementToast({ @@ -43,6 +43,11 @@ export function AchievementToast({ ); } + if (variant === "steam") { + return ( + Steam + ); + } return ( { - onClick(); - onClose(); - } + onClick(); + onClose(); + } : undefined } className={`fixed top-6 right-6 z-[9999] ${onClick ? "cursor-pointer" : ""}`} diff --git a/src/components/views/VersionsView.tsx b/src/components/views/VersionsView.tsx index bee195f..f5a0b1e 100644 --- a/src/components/views/VersionsView.tsx +++ b/src/components/views/VersionsView.tsx @@ -26,9 +26,8 @@ const DeleteConfirmButton = memo(function DeleteConfirmButton({ onClick={onClick} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} - className={`w-24 h-10 flex items-center justify-center mc-text-shadow transition-colors ${ - isDanger ? "text-red-500" : "text-white" - } ${isHovered ? (isDanger ? "text-red-400" : "text-[#FFFF55]") : ""}`} + className={`w-24 h-10 flex items-center justify-center mc-text-shadow transition-colors ${isDanger ? "text-red-500" : "text-white" + } ${isHovered ? (isDanger ? "text-red-400" : "text-[#FFFF55]") : ""}`} style={{ backgroundImage: isHovered ? "url('/images/button_highlighted.png')" @@ -62,7 +61,9 @@ const VersionsView = memo(function VersionsView() { downloadingId, downloadProgress, updatesAvailable, + addToSteam, } = useGame(); + const { isDayTime } = useConfig(); const [focusIndex, setFocusIndex] = useState(0); const [focusBtn, setFocusBtn] = useState(0); const [isImportModalOpen, setIsImportModalOpen] = useState(false); @@ -100,7 +101,7 @@ const VersionsView = memo(function VersionsView() { const edition = editions[focusIndex]; const isInstalled = installedVersions.includes(edition.id); const isCustom = edition.id.startsWith("custom_"); - const maxBtn = isInstalled ? (isCustom ? 4 : 3) : 1; + const maxBtn = isInstalled ? (isCustom ? 6 : 4) : 1; setFocusBtn((prev) => (prev <= 0 ? maxBtn : prev - 1)); } } else if (e.key === "ArrowRight") { @@ -109,7 +110,7 @@ const VersionsView = memo(function VersionsView() { const edition = editions[focusIndex]; const isInstalled = installedVersions.includes(edition.id); const isCustom = edition.id.startsWith("custom_"); - const maxBtn = isInstalled ? (isCustom ? 4 : 3) : 1; + const maxBtn = isInstalled ? (isCustom ? 6 : 4) : 1; setFocusBtn((prev) => (prev >= maxBtn ? 0 : prev + 1)); } } else if (e.key === "Enter") { @@ -142,10 +143,26 @@ const VersionsView = memo(function VersionsView() { playPressSound(); setDeleteConfirmEdition(edition); } - } else if (focusBtn === 4 && isCustom) { + } else if (focusBtn === 4) { + playPressSound(); + const PANORAMA_PROFILES = ["legacy_evolved", "360revived"]; + const panoId = PANORAMA_PROFILES.includes(edition.id) + ? edition.id + : "legacy_evolved"; + const panoramaUrl = `/panorama/${panoId}_Panorama_Background_${isDayTime ? "Day" : "Night"}.png`; + addToSteam( + edition.id, + edition.name, + edition.titleImage, + panoramaUrl, + ); + } else if (focusBtn === 5 && isCustom) { playPressSound(); setEditingEdition(edition); setIsImportModalOpen(true); + } else if (focusBtn === 6 && isCustom) { + playBackSound(); + onDeleteEdition(edition.id); } } else { if (focusBtn === 1) { @@ -185,6 +202,8 @@ const VersionsView = memo(function VersionsView() { setActiveView, toggleInstall, handleCancelDownload, + addToSteam, + isDayTime ]); useEffect(() => { @@ -261,11 +280,9 @@ const VersionsView = memo(function VersionsView() {
!isComingSoon && setFocusIndex(i)} >
@@ -302,18 +319,17 @@ const VersionsView = memo(function VersionsView() { !isComingSoon && handleEditionClick(edition, i) } disabled={isComingSoon} - className={`flex-1 text-left min-w-0 outline-none rounded ${ - focusIndex === i && focusBtn === 0 && !isComingSoon - ? "ring-2 ring-white" - : "" - } ${isComingSoon ? "cursor-not-allowed" : ""}`} + className={`flex-1 text-left min-w-0 outline-none rounded ${focusIndex === i && focusBtn === 0 && !isComingSoon + ? "ring-2 ring-white" + : "" + } ${isComingSoon ? "cursor-not-allowed" : ""}`} >
{edition.logo && ( )} {edition.name} @@ -346,9 +361,8 @@ const VersionsView = memo(function VersionsView() { )}

{edition.desc}

@@ -371,7 +385,7 @@ const VersionsView = memo(function VersionsView() { backgroundImage: (hoveredBtn?.row === i && hoveredBtn?.btn === "cancel") || - (focusIndex === i && focusBtn === 1) + (focusIndex === i && focusBtn === 1) ? "url('/images/Button_Square_Highlighted.png')" : "url('/images/Button_Square.png')", backgroundSize: "100% 100%", @@ -407,7 +421,7 @@ const VersionsView = memo(function VersionsView() { backgroundImage: (hoveredBtn?.row === i && hoveredBtn?.btn === "download") || - (focusIndex === i && focusBtn === 1) + (focusIndex === i && focusBtn === 1) ? "url('/images/Button_Square_Highlighted.png')" : "url('/images/Button_Square.png')", backgroundSize: "100% 100%", @@ -441,7 +455,7 @@ const VersionsView = memo(function VersionsView() { backgroundImage: (hoveredBtn?.row === i && hoveredBtn?.btn === "cancel") || - (focusIndex === i && focusBtn === 1) + (focusIndex === i && focusBtn === 1) ? "url('/images/Button_Square_Highlighted.png')" : "url('/images/Button_Square.png')", backgroundSize: "100% 100%", @@ -476,7 +490,7 @@ const VersionsView = memo(function VersionsView() { backgroundImage: (hoveredBtn?.row === i && hoveredBtn?.btn === "update") || - (focusIndex === i && focusBtn === 1) + (focusIndex === i && focusBtn === 1) ? "url('/images/Button_Square_Highlighted.png')" : "url('/images/Button_Square.png')", backgroundSize: "100% 100%", @@ -510,7 +524,7 @@ const VersionsView = memo(function VersionsView() { backgroundImage: (hoveredBtn?.row === i && hoveredBtn?.btn === "folder") || - (focusIndex === i && focusBtn === 2) + (focusIndex === i && focusBtn === 2) ? "url('/images/Button_Square_Highlighted.png')" : "url('/images/Button_Square.png')", backgroundSize: "100% 100%", @@ -539,7 +553,7 @@ const VersionsView = memo(function VersionsView() { backgroundImage: (hoveredBtn?.row === i && hoveredBtn?.btn === "delete") || - (focusIndex === i && focusBtn === 3) + (focusIndex === i && focusBtn === 3) ? "url('/images/Button_Square_Highlighted.png')" : "url('/images/Button_Square.png')", backgroundSize: "100% 100%", @@ -553,6 +567,33 @@ const VersionsView = memo(function VersionsView() { style={{ imageRendering: "pixelated" }} /> + {isCustom && ( <>