[feat] optimization for getting cs2 path + add Toast for auto path

This commit is contained in:
Purp1e
2025-03-23 01:08:50 +08:00
parent ad5a1bd870
commit e7b6d81319
12 changed files with 184 additions and 81 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -19,7 +19,7 @@
"@heroui/react": "^2.7.5",
"@icon-park/react": "^1.4.2",
"@reactuses/core": "6.0.1",
"@supabase/ssr": "^0.5.2",
"@supabase/ssr": "0.6.1",
"@tauri-apps/api": "2.1.0",
"@tauri-apps/plugin-clipboard-manager": "2.0.0",
"@tauri-apps/plugin-deep-link": "~2.2.0",
@@ -43,7 +43,6 @@
"react-dom": "^19.0.0",
"swr": "^2.3.3",
"tauri-plugin-system-info-api": "^2.0.10",
"tauri-plugin-valtio": "1.1.2",
"throttle-debounce": "^5.0.2",
"zustand": "5.0.1"
},

View File

@@ -39,8 +39,8 @@ pub fn get_steam_path() -> Result<String, String> {
}
#[tauri::command]
pub fn get_cs_path(name: &str) -> Result<String, String> {
wrap_err!(steam::path::get_cs_path(name))
pub fn get_cs_path(name: &str, steam_dir: &str) -> Result<String, String> {
wrap_err!(steam::path::get_cs_path(name, steam_dir))
}
#[tauri::command]

View File

@@ -3,35 +3,102 @@
// - CS2(CS:GO) Path
#![allow(unused)]
use anyhow::Result;
use std::fs::{self, exists};
use std::path::{Path, PathBuf};
use crate::tool::common::get_exe_path;
use anyhow::Result;
use super::reg;
pub fn get_steam_path() -> Result<String, String> {
// Running steam.exe
#[cfg(target_os = "windows")]
if let Ok(steam_path) = get_exe_path("steam") {
// 去除结尾的 steam.exe 不区分大小写
if let Some(parent_path) = Path::new(&steam_path).parent() {
return Ok(parent_path.to_string_lossy().to_string());
} else {
return Err("Failed to get parent directory".into());
}
}
// Windows Registry
#[cfg(target_os = "windows")]
if let Ok(reg) = reg::SteamReg::get_all() {
return Ok(reg.steam_path);
}
// Running steam.exe
#[cfg(target_os = "windows")]
if let Ok(steam_path) = get_exe_path("steam.exe") {
return Ok(steam_path);
}
Err("no steam path found".into())
}
pub fn get_cs_path(name: &str) -> Result<String, String> {
pub fn get_cs_path(name: &str, steam_dir: &str) -> Result<String, String> {
if name != "csgo" && name != "cs2" {
return Err("invalid cs name".into());
}
// 1. 优先检查 steam_dir 参数
let steam_path = if steam_dir.is_empty() || !Path::new(steam_dir).exists() {
// 如果 steam_dir 不满足条件,调用 get_steam_path 获取路径
let p = get_steam_path().map_err(|e| e.to_string())?;
PathBuf::from(p)
} else {
PathBuf::from(steam_dir) // 同样转换为PathBuf
};
let cs_path = steam_path
.join("steamapps\\common\\Counter-Strike Global Offensive\\game\\bin\\win64\\cs2.exe");
if cs_path.exists() {
if let Some(parent) = cs_path.parent() {
return Ok(parent.to_string_lossy().to_string());
}
}
// 2. 通过注册表读取所有磁盘盘符
#[cfg(target_os = "windows")]
if let Ok(cs_path) = get_exe_path(&(name.to_owned() + ".exe")) {
return Ok(cs_path);
{
use winreg::enums::HKEY_LOCAL_MACHINE;
use winreg::RegKey;
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
let mounted_devices = hklm
.open_subkey("SYSTEM\\MountedDevices")
.map_err(|e| e.to_string())?;
// 获取所有盘符
let drives: Vec<String> = mounted_devices
.enum_values()
.filter_map(|v| {
let (name, _) = v.ok()?;
let name = name.to_string();
if name.starts_with("\\DosDevices\\") {
Some(name.replace("\\DosDevices\\", ""))
} else {
None
}
})
.collect();
// 遍历盘符,尝试查找 cs2.exe
let cs_path_suffix = "SteamLibrary\\steamapps\\common\\Counter-Strike Global Offensive\\game\\bin\\win64\\cs2.exe";
for drive in drives {
let cs_path = Path::new(&drive).join(cs_path_suffix);
if cs_path.exists() {
if let Some(parent) = cs_path.parent() {
return Ok(parent.to_string_lossy().to_string());
}
}
}
}
// 3. 查找正在运行的cs2进程
#[cfg(target_os = "windows")]
if let Ok(cs2_path) = get_exe_path("cs2") {
// 去除结尾的 steam.exe 不区分大小写
if let Some(parent_path) = Path::new(&cs2_path).parent() {
return Ok(parent_path.to_string_lossy().to_string());
} else {
return Err("Failed to get parent directory".into());
}
}
Err("no cs path found".into())
@@ -46,4 +113,17 @@ mod tests {
let path = get_steam_path().unwrap();
println!("{}", path);
}
#[test]
fn test_get_cs_path() {
let result = get_cs_path("cs2", "");
assert!(result.is_ok() || result.is_err());
println!("CS2 Path: {:?}", result);
// 使用get_steam_path给到的路径
let steam_dir = get_steam_path().unwrap();
let result = get_cs_path("cs2", &steam_dir);
assert!(result.is_ok() || result.is_err());
println!("CS2 Path: {:?}", result)
}
}

View File

@@ -1,5 +1,5 @@
use std::process::Command;
use std::os::windows::process::CommandExt;
use std::process::Command;
const CREATE_NO_WINDOW: u32 = 0x08000000;
// const DETACHED_PROCESS: u32 = 0x00000008;
@@ -21,18 +21,29 @@ pub fn run_steam() -> std::io::Result<std::process::Output> {
.output()
}
// FIXME wmic is deprecated
pub fn get_exe_path(name: &str) -> Result<String, std::io::Error> {
let command = format!("/C wmic process where name='{}' get ExecutablePath", name);
// [原理]
// Powershell 运行 Get-Process name | Select-Object path
// 有name.exe运行时返回
// Path
// ----
// 进程路径
let command = format!("Get-Process {} | Select-Object path", name);
let args = command.split_whitespace().collect::<Vec<&str>>();
let output = Command::new("cmd.exe").args(&args).output()?;
let output = Command::new("powershell.exe")
.args(&args)
.creation_flags(CREATE_NO_WINDOW)
.output()?;
let out = String::from_utf8_lossy(&output.stdout).trim().to_string();
let out = String::from_utf8_lossy(&output.stdout).to_string();
if out.contains("ExecutablePath") {
if out.contains("Path") {
let out = out.trim();
let spt: Vec<&str> = out.split("\r\n").collect();
if spt.len() >= 2 {
return Ok(spt[1].to_string());
if spt.len() > 2 {
return Ok(spt[2].to_string());
}
}
Err(std::io::Error::new(
@@ -68,4 +79,12 @@ mod tests {
println!("test open path: {}", path);
open_path(path).unwrap()
}
#[test]
fn test_get_exe_path() {
let path = get_exe_path("steam").expect("failed");
println!("test get steam path: {}", path);
get_exe_path("not_running").expect("failed");
}
}

View File

@@ -1,26 +1,13 @@
"use client"
import SteamUsers from "@/components/cstb/SteamUsers"
import {
Card,
CardBody,
CardHeader,
CardIcon,
CardTool,
} from "@/components/window/Card"
import { ToolButton } from "@/components/window/ToolButton"
import { cn } from "@heroui/react"
import {
AssemblyLine, HardDisk, SettingConfig,
UploadOne
} from "@icon-park/react"
import { usePathname, useRouter } from "next/navigation"
// import { usePathname, useRouter } from "next/navigation"
// import { platform } from "@tauri-apps/plugin-os"
export default function PreferenceLayout({
children,
}: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
// const router = useRouter()
// const pathname = usePathname()
return (
<div className="flex w-full gap-3">

View File

@@ -13,13 +13,10 @@ export default function Providers({ children }: { children: React.ReactNode }) {
return (
<HeroUIProvider
className={cn(
"h-full bg-zinc-100/90 dark:bg-zinc-900",
os === "macos" && "rounded-lg",
)}
className={cn("h-full bg-zinc-200/90 dark:bg-zinc-900", os === "macos" && "rounded-lg")}
>
<NextThemesProvider attribute="class" defaultTheme="light">
<ToastProvider toastOffset={10} placement="top-center" />
<ToastProvider toastOffset={10} placement="top-center" toastProps={{ timeout: 3000 }} />
{children}
</NextThemesProvider>
</HeroUIProvider>

View File

@@ -2,7 +2,6 @@ import { addToast } from "@heroui/react"
import { FolderFocusOne } from "@icon-park/react"
import { Card, CardBody, CardHeader, CardIcon } from "../window/Card"
import { invoke } from "@tauri-apps/api/core"
import { configDir } from "@tauri-apps/api/path"
import { useSteamStore } from "@/store/steam"
import path from "path"
@@ -17,7 +16,7 @@ const RoundedButton = ({
return (
<button
type="button"
className="flex items-center justify-center px-3 py-1 transition rounded-full select-none min-w-fit active:scale-95 hover:bg-black/10 text-zinc-700 dark:text-zinc-100 bg-black/5"
className="flex items-center justify-center px-3 py-1 transition rounded-full select-none min-w-fit active:scale-95 hover:bg-black/10 text-zinc-700 dark:text-zinc-100 bg-black/5 dark:bg-white/5"
{...props}
>
{children}

View File

@@ -1,4 +1,4 @@
import { addToast, Button, Chip, Input, Spinner } from "@heroui/react"
import { addToast, Button, Input, Spinner } from "@heroui/react"
import { useRouter } from "next/navigation"
import { useEffect, useState } from "react"
import { useSteamStore } from "@/store/steam"
@@ -6,6 +6,7 @@ import { open } from "@tauri-apps/plugin-dialog"
import { invoke } from "@tauri-apps/api/core"
import { onOpenUrl } from "@tauri-apps/plugin-deep-link"
import { useDebounce } from "ahooks"
import { useAppStore } from "@/store/app"
/**
* 检查指定路径的有效性
@@ -28,6 +29,7 @@ function trim_end_string(str: string, suffix: string): string {
export function Prepare() {
const steam = useSteamStore()
const app = useAppStore()
const router = useRouter()
const [loading, setLoading] = useState(true)
const [, setSteamDir] = useState(steam.state.steamDir)
@@ -35,7 +37,12 @@ export function Prepare() {
// Init
useEffect(() => {
void steam.store.start()
const init = async () => {
await steam.store.start()
await app.store.start()
if (!app.state.inited) await autoGetPaths()
}
void init()
}, [])
// valid变动后调整State
@@ -45,22 +52,12 @@ export function Prepare() {
trailing: true,
maxWait: 2500,
})
const [checkCount, setCheckCount] = useState(0)
useEffect(() => {
setCheckCount((prev) => (prev >= 10 ? 10 : prev + 1))
console.log(checkCount, "触发", debounceValid, steam.state.steamDir, steam.state.cs2Dir)
if (checkCount < 2) {
if (debounceValid) {
console.log("跳转")
setTimeout(() => {
router.push("/home")
}, 500)
} else {
setTimeout(() => {
setLoading(false)
}, 1200)
}
// console.log(checkCount, "触发", debounceValid, steam.state.steamDir, steam.state.cs2Dir)
if (debounceValid && app.state.inited) {
setTimeout(() => {
router.push("/home")
}, 500)
} else {
setTimeout(() => {
setLoading(false)
@@ -94,10 +91,17 @@ export function Prepare() {
}
try {
const cs2_path = await invoke<string>("get_cs_path", { name: "cs2" })
if (cs2_path) steam.setCsDir(cs2_path.replace(/\\[^\\]+$/, ""))
const cs2_path = await invoke<string>("get_cs_path", {
name: "cs2",
steamDir: steam.state.steamDir,
})
if (cs2_path) {
steam.setCsDir(cs2_path)
addToast({ title: "自动获取路径成功", color: "success" })
}
} catch (e) {
addToast({ title: "自动获取CS2路径失败", color: "danger" })
console.log(e)
}
}
@@ -129,7 +133,7 @@ export function Prepare() {
return (
<div className="flex flex-col w-full max-w-3xl gap-2 p-5">
<p className="text-center"></p>
<p className="text-center"></p>
<br />
<h3 className="font-semibold">Steam所在文件夹</h3>
<div className="flex gap-2">
@@ -145,7 +149,13 @@ export function Prepare() {
errorMessage={"路径无效"}
isInvalid={!steam.state.steamDirValid}
/>
<Button onPress={handleSelectSteamDir} variant="solid" color="primary" size="sm" isLoading={steam.state.steamDirChecking}>
<Button
onPress={handleSelectSteamDir}
variant="solid"
color="primary"
size="sm"
isLoading={steam.state.steamDirChecking}
>
</Button>
</div>
@@ -160,10 +170,16 @@ export function Prepare() {
steam.setCsDir(value)
}}
description="cs2.exe所在文件夹"
errorMessage={"路径无效"}
errorMessage={"路径无效建议启动游戏后点击自动获取可以检测运行中的cs2"}
isInvalid={!steam.state.cs2DirValid}
/>
<Button onPress={handleSelectCs2Dir} variant="solid" color="primary" size="sm" isLoading={steam.state.cs2DirChecking}>
<Button
onPress={handleSelectCs2Dir}
variant="solid"
color="primary"
size="sm"
isLoading={steam.state.cs2DirChecking}
>
</Button>
</div>
@@ -175,16 +191,19 @@ export function Prepare() {
onPress={() => void autoGetPaths()}
variant="ghost"
color="default"
size="sm"
size="md"
className="w-24"
>
</Button>
<Button
onPress={() => router.push("/home")}
onPress={() => {
app.setInited(true)
router.push("/home")
}}
variant="solid"
color="primary"
size="sm"
size="md"
className="w-24"
isLoading={steam.state.steamDirChecking || steam.state.cs2DirChecking}
isDisabled={!steam.state.steamDirValid || !steam.state.cs2DirValid}

View File

@@ -1,6 +1,6 @@
import { User } from "@icon-park/react"
import { Card, CardBody, CardHeader, CardIcon } from "../window/Card"
import { Button, Chip, cn, Code } from "@heroui/react"
import { Button, Chip } from "@heroui/react"
import { useSteamStore } from "@/store/steam"
const SteamUsers = ({ className }: { className?: string }) => {

View File

@@ -42,15 +42,18 @@ const Nav = () => {
const router = useRouter()
const pathname = usePathname()
const app = useAppStore()
return (
<nav className="absolute top-0 right-0 flex flex-row h-16 gap-0.5 p-4" data-tauri-drag-region>
<ResetModal />
{pathname !== "/" && (
<button
type="button"
className="px-2 py-0 transition duration-150 rounded hover:bg-zinc-200/80 dark:hover:bg-zinc-100/10 active:scale-95"
onClick={() => (pathname !== "/" ? router.push("/") : router.back())}
onClick={() => {
app.setInited(false)
if(pathname !== "/") router.push("/")
}}
>
<RocketOne size={16} />
</button>
@@ -64,6 +67,8 @@ const Nav = () => {
{theme === "light" ? <SunOne size={16} /> : <Moon size={16} />}
</button>
<ResetModal />
{/* { platform() === "windows" && ( */}
<>
<button
@@ -127,9 +132,7 @@ function ResetModal() {
<>
<ModalHeader className="flex flex-col gap-1"></ModalHeader>
<ModalBody>
<p>
CS工具箱的偏好设置为默认设置
</p>
<p>CS工具箱的偏好设置为默认设置</p>
</ModalBody>
<ModalFooter>
<Button color="danger" variant="light" onPress={onClose}>

View File

@@ -29,8 +29,8 @@ const SideButton = ({
onClick={() => router.push(route || "/")}
className={cn(
className,
"p-2.5 hover:bg-black/5 rounded-lg transition relative",
path.startsWith(route) && "bg-black/5"
"p-2.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition relative active:scale-90",
path.startsWith(route) && "bg-black/5 dark:bg-white/5"
)}
{...rest}
>