feat(installer): 实现游戏安装与快捷方式创建功能- 添加禁用右键菜单和开发者工具的保护机制

- 引入GAME_CONFIG配置文件统一管理游戏信息
- 安装页面支持记录安装路径到localStorage
- 成功页面增加创建桌面快捷方式选项
- 实现调用系统资源管理器打开游戏目录功能
- 集成Windows COM组件创建快捷方式的能力
- 更新UI界面展示多语言标题和版本信息
- 升级daisyui依赖至5.5.2版本- 调整窗口标题和产品名称为"游戏安装工具 - NoPJ"
This commit is contained in:
Chaos
2025-11-13 23:40:22 +08:00
parent 345acdee91
commit 6b6c61fd08
14 changed files with 326 additions and 81 deletions

1
src-tauri/Cargo.lock generated
View File

@@ -104,6 +104,7 @@ dependencies = [
"tauri-plugin-shell",
"thiserror 2.0.17",
"tokio",
"windows",
"zip",
]

View File

@@ -32,3 +32,4 @@ async-runtime = "0.0.0"
tokio = { version = "1.48.0", features = ["time"] }
tauri-plugin-shell = "2.3.3"
tauri-plugin-opener = "2.5.2"
windows = "0.61.3"

View File

@@ -4,7 +4,8 @@ use thiserror::Error;
use zip::ZipArchive;
use std::fs::{self, File};
use std::io;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::process::Command;
use serde::Serialize;
use tauri::Emitter;
@@ -145,3 +146,20 @@ pub async fn install_game(window: tauri::Window, target_dir: String) -> Result<S
Ok(format!("成功解压 {} 个文件到指定目录!", total_files as u32))
}
//调用资源管理器打开游戏目录
#[tauri::command]
pub async fn open_game_dir(install_path:String) -> Result<String,String> {
let game_path = Path::new(&install_path);
if !game_path.exists() {
return Err("游戏目录不存在".to_string());
}
let result = Command::new("explorer")
.arg(game_path)
.spawn()
.map_err(|e| format!("打开游戏目录失败: {}", e))?;
println!("资源管理器进程ID: {}", result.id());
Ok("已打开游戏目录".to_string())
}

View File

@@ -1,5 +1,6 @@
mod network;
mod gameins;
mod shortcut;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
@@ -19,7 +20,9 @@ pub fn run() {
})
.invoke_handler(tauri::generate_handler![
network::get_network_interfaces,
gameins::install_game
gameins::install_game,
gameins::open_game_dir,
shortcut::create_shortcut,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

132
src-tauri/src/shortcut.rs Normal file
View File

@@ -0,0 +1,132 @@
use std::path::Path;
use windows::{
core::{Interface, PCWSTR, HRESULT},
Win32::{
System::{
Com::{
CoCreateInstance, CoInitializeEx, CoUninitialize,
IPersistFile, CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED
},
},
UI::Shell::{IShellLinkW, ShellLink},
},
};
#[tauri::command]
pub fn create_shortcut(
install_path: String,
shortcut_name: String,
exe_name: String
) -> Result<String, String> {
unsafe {
// 初始化COM库
let com_result = CoInitializeEx(None, COINIT_APARTMENTTHREADED);
let needs_uninit = com_result.is_ok() || com_result == HRESULT(0x80010106u32 as i32); // S_FALSE or RPC_E_CHANGED_MODE
let result = create_shortcut_internal(&install_path, &shortcut_name, &exe_name);
if needs_uninit {
CoUninitialize();
}
result
}
}
unsafe fn create_shortcut_internal(
install_path: &str,
shortcut_name: &str,
exe_name: &str
) -> Result<String, String> {
// 获取当前用户桌面路径
let desktop_path = get_desktop_path()?;
let shortcut_file = format!("{}.lnk", shortcut_name);
let shortcut_full_path = Path::new(&desktop_path).join(&shortcut_file);
// 构建目标程序路径
let target_path = Path::new(install_path).join(exe_name);
if !target_path.exists() {
return Err(format!("目标程序不存在: {}", target_path.display()));
}
// 创建IShellLink对象
let shell_link: IShellLinkW = CoCreateInstance(
&ShellLink,
None,
CLSCTX_INPROC_SERVER
).map_err(|e| format!("创建ShellLink失败: {:?}", e))?;
// 设置快捷方式属性
set_shell_link_properties(&shell_link, &target_path, install_path)?;
// 保存快捷方式
save_shortcut(&shell_link, &shortcut_full_path)?;
Ok(format!("快捷方式创建成功: {}", shortcut_full_path.display()))
}
unsafe fn get_desktop_path() -> Result<String, String> {
std::env::var("USERPROFILE")
.map(|profile| format!("{}\\Desktop", profile))
.or_else(|_| {
std::env::var("USERNAME")
.map(|username| format!("C:\\Users\\{}\\Desktop", username))
})
.map_err(|_| "无法获取用户桌面路径".to_string())
}
unsafe fn set_shell_link_properties(
shell_link: &IShellLinkW,
target_path: &Path,
working_dir: &str
) -> Result<(), String> {
// 设置目标路径
let target_path_str = target_path.to_string_lossy();
let wide_target: Vec<u16> = to_wide_string(&target_path_str);
// 用 map_err + ? 处理错误并转换为 String
shell_link
.SetPath(PCWSTR(wide_target.as_ptr()))
.map_err(|e| format!("设置目标路径失败: {:?}", e))?;
// 设置工作目录
let wide_working_dir: Vec<u16> = to_wide_string(working_dir);
shell_link
.SetWorkingDirectory(PCWSTR(wide_working_dir.as_ptr()))
.map_err(|e| format!("设置工作目录失败: {:?}", e))?;
// 设置描述
let description = "NoPJ创建的快捷方式 - nopj.cn";
let wide_desc: Vec<u16> = to_wide_string(description);
shell_link
.SetDescription(PCWSTR(wide_desc.as_ptr()))
.map_err(|e| format!("设置描述失败: {:?}", e))?;
Ok(())
}
unsafe fn save_shortcut(
shell_link: &IShellLinkW,
shortcut_path: &Path
) -> Result<(), String> {
// 转换为 IPersistFile 接口
let persist_file: IPersistFile = shell_link
.cast()
.map_err(|e| format!("接口转换失败: {:?}", e))?;
// 保存文件
let path_str = shortcut_path.to_string_lossy();
let wide_path: Vec<u16> = to_wide_string(&path_str);
// Save 可能返回 windows::core::Result<()>,同样用 map_err ? 处理
persist_file
.Save(PCWSTR(wide_path.as_ptr()), true)
.map_err(|e| format!("保存快捷方式失败: {:?}", e))?;
Ok(())
}
fn to_wide_string(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}

View File

@@ -1,6 +1,6 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "NoPJ安装器",
"productName": "游戏安装工具 - NoPJ",
"version": "0.1.0",
"identifier": "cn.nopj.gametools",
"build": {
@@ -12,13 +12,13 @@
"app": {
"windows": [
{
"title": "Game Install",
"title": "游戏安装工具 - NoPJ",
"width": 1280,
"height": 720,
"resizable": false,
"fullscreen": false,
"decorations": false
"decorations": false,
"devtools": false
}
],
"security": {