feat: initial support for steam integration

This commit is contained in:
neoapps-dev
2026-04-30 22:51:28 +03:00
parent eddba38829
commit 7414efce47
11 changed files with 316 additions and 42 deletions
+142 -2
View File
@@ -2,6 +2,7 @@ use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::process::Command;
use steam_shortcuts_util::{Shortcut, parse_shortcuts, shortcuts_to_bytes};
#[cfg(target_os = "macos")]
use std::process::Stdio;
@@ -1680,6 +1681,124 @@ fn delete_screenshot(path: String) -> 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<PathBuf> = 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<steam_shortcuts_util::shortcut::ShortcutOwned> = 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<Shortcut> = 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<String> = 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::<GameState>();
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");
}