[feat] basic steam user parse
todo: steamid32 + avatar
This commit is contained in:
3
.editorconfig
Normal file
3
.editorconfig
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[*.rs]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
31
package.json
31
package.json
@@ -17,34 +17,33 @@
|
|||||||
"fix": "next lint --fix"
|
"fix": "next lint --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@formkit/auto-animate": "^0.8.2",
|
||||||
"@heroui/react": "^2.7.5",
|
"@heroui/react": "^2.7.5",
|
||||||
"@icon-park/react": "^1.4.2",
|
"@icon-park/react": "^1.4.2",
|
||||||
"@reactuses/core": "6.0.1",
|
"@reactuses/core": "6.0.1",
|
||||||
"@supabase/ssr": "0.6.1",
|
"@supabase/ssr": "0.6.1",
|
||||||
"@tauri-apps/api": "2.1.0",
|
"@tauri-apps/api": "2.4.0",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "2.0.0",
|
"@tauri-apps/plugin-clipboard-manager": "2.2.2",
|
||||||
"@tauri-apps/plugin-deep-link": "~2.2.0",
|
"@tauri-apps/plugin-deep-link": "~2.2.0",
|
||||||
"@tauri-apps/plugin-dialog": "~2.2.0",
|
"@tauri-apps/plugin-dialog": "~2.2.0",
|
||||||
"@tauri-apps/plugin-fs": "2.0.0",
|
"@tauri-apps/plugin-fs": "2.2.0",
|
||||||
"@tauri-apps/plugin-global-shortcut": "2.0.0",
|
"@tauri-apps/plugin-global-shortcut": "2.2.0",
|
||||||
"@tauri-apps/plugin-http": "2.0.1",
|
"@tauri-apps/plugin-http": "2.4.2",
|
||||||
"@tauri-apps/plugin-notification": "2.0.0",
|
"@tauri-apps/plugin-notification": "2.2.2",
|
||||||
"@tauri-apps/plugin-os": "2.0.0",
|
"@tauri-apps/plugin-os": "2.2.1",
|
||||||
"@tauri-apps/plugin-process": "2.0.0",
|
"@tauri-apps/plugin-process": "2.2.0",
|
||||||
"@tauri-apps/plugin-shell": "2.0.1",
|
"@tauri-apps/plugin-shell": "2.2.0",
|
||||||
"@tauri-apps/plugin-store": "^2.2.0",
|
"@tauri-apps/plugin-store": "^2.2.0",
|
||||||
"@tauri-store/valtio": "^2.0.0",
|
"@tauri-store/valtio": "2.1.0",
|
||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"ahooks": "^3.8.4",
|
"ahooks": "^3.8.4",
|
||||||
"framer-motion": "^12.5.0",
|
"framer-motion": "^12.5.0",
|
||||||
"next": "15.2.2",
|
"next": "15.2.3",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"swr": "^2.3.3",
|
"swr": "^2.3.3",
|
||||||
"tauri-plugin-system-info-api": "^2.0.10",
|
"tauri-plugin-system-info-api": "^2.0.10"
|
||||||
"throttle-debounce": "^5.0.2",
|
|
||||||
"zustand": "5.0.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.4.0",
|
"@tauri-apps/cli": "^2.4.0",
|
||||||
@@ -62,8 +61,8 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cssnano": "^7.0.6",
|
"cssnano": "^7.0.6",
|
||||||
"eslint": "9.14.0",
|
"eslint": "9.23.0",
|
||||||
"eslint-config-next": "15.0.3",
|
"eslint-config-next": "15.2.3",
|
||||||
"lint-staged": "^15.5.0",
|
"lint-staged": "^15.5.0",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"postcss-import": "^16.1.0",
|
"postcss-import": "^16.1.0",
|
||||||
|
|||||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@@ -7,6 +7,7 @@ name = "CS工具箱"
|
|||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"base64 0.22.1",
|
||||||
"log",
|
"log",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -28,6 +29,7 @@ dependencies = [
|
|||||||
"tauri-plugin-system-info",
|
"tauri-plugin-system-info",
|
||||||
"tauri-plugin-theme",
|
"tauri-plugin-theme",
|
||||||
"tauri-plugin-valtio",
|
"tauri-plugin-valtio",
|
||||||
|
"walkdir",
|
||||||
"window-vibrancy",
|
"window-vibrancy",
|
||||||
"winreg 0.55.0",
|
"winreg 0.55.0",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ tauri-build = { version = "2.1.0", features = [] }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = "0.4.26"
|
log = "0.4.26"
|
||||||
|
base64 = "0.22.1"
|
||||||
|
walkdir = "2.5.0"
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
regex = "1.11.1"
|
regex = "1.11.1"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::steam;
|
use crate::steam;
|
||||||
use crate::tool::*;
|
use crate::tool::*;
|
||||||
|
use crate::vdf::preset;
|
||||||
use crate::wrap_err;
|
use crate::wrap_err;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
@@ -64,11 +65,10 @@ pub fn set_powerplan(plan: i32) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO watch_steam_users
|
#[tauri::command]
|
||||||
// TODO watch_video_settings
|
pub fn get_steam_users(steam_dir: &str) -> Result<Vec<preset::User>, String> {
|
||||||
// TODO watch + cancel
|
wrap_err!(preset::get_users(steam_dir))
|
||||||
// TODO fs_list_dir
|
}
|
||||||
// TODO fs_watch_dir
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn set_auto_login_user(user: &str) -> Result<String, String> {
|
pub fn set_auto_login_user(user: &str) -> Result<String, String> {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ mod tray;
|
|||||||
mod cmds;
|
mod cmds;
|
||||||
mod steam;
|
mod steam;
|
||||||
mod tool;
|
mod tool;
|
||||||
|
mod vdf;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn on_button_clicked() -> String {
|
fn on_button_clicked() -> String {
|
||||||
@@ -96,6 +97,7 @@ fn main() {
|
|||||||
cmds::open_path,
|
cmds::open_path,
|
||||||
cmds::get_powerplan,
|
cmds::get_powerplan,
|
||||||
cmds::set_powerplan,
|
cmds::set_powerplan,
|
||||||
|
cmds::get_steam_users,
|
||||||
cmds::set_auto_login_user,
|
cmds::set_auto_login_user,
|
||||||
cmds::check_path,
|
cmds::check_path,
|
||||||
on_button_clicked
|
on_button_clicked
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
pub mod id;
|
pub mod id;
|
||||||
pub mod path;
|
pub mod path;
|
||||||
pub mod reg;
|
pub mod reg;
|
||||||
|
pub mod user;
|
||||||
|
|
||||||
// common steam utils
|
// common steam utils
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
use crate::vdf::parse::to_json;
|
||||||
|
|
||||||
|
pub fn get_steam_users() -> Result<String, String> {
|
||||||
|
let vdf_data = r#"
|
||||||
|
{
|
||||||
|
"key1"\t\t"value1"
|
||||||
|
"key2"\t\t"value2"
|
||||||
|
"subkey" {
|
||||||
|
"key3"\t\t"value3"
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
let json_data = to_json(vdf_data);
|
||||||
|
Ok(json_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_steam_users() {
|
||||||
|
let result = get_steam_users();
|
||||||
|
assert!(result.is_ok() || result.is_err());
|
||||||
|
println!("{}", result.unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
2
src-tauri/src/vdf/mod.rs
Normal file
2
src-tauri/src/vdf/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod parse;
|
||||||
|
pub mod preset;
|
||||||
84
src-tauri/src/vdf/parse.rs
Normal file
84
src-tauri/src/vdf/parse.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
pub fn to_json(vdf_data: &str) -> String {
|
||||||
|
let linebreak = match std::env::consts::OS {
|
||||||
|
"macos" => "\r",
|
||||||
|
"windows" => "\n",
|
||||||
|
"linux" => "\n",
|
||||||
|
_ => "\n",
|
||||||
|
};
|
||||||
|
|
||||||
|
let startpoint = vdf_data.find('{').unwrap_or(0);
|
||||||
|
let vdf_data = &vdf_data[startpoint..];
|
||||||
|
|
||||||
|
let lines: Vec<&str> = vdf_data.split(linebreak).collect();
|
||||||
|
let mut json_data = String::new();
|
||||||
|
|
||||||
|
for line in lines {
|
||||||
|
let mut line = line.trim_end_matches('\r').trim().to_string();
|
||||||
|
|
||||||
|
if line.contains("\"\t\t\"") {
|
||||||
|
line = line.replace("\"\t\t\"", "\": \"");
|
||||||
|
line.push(',');
|
||||||
|
} else if line.contains("\" \"") {
|
||||||
|
line = line.replace("\" \"", "\": \"");
|
||||||
|
line.push(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
line = line
|
||||||
|
.replace('{', ": {")
|
||||||
|
.replace('\t', "")
|
||||||
|
.replace('}', "},");
|
||||||
|
|
||||||
|
json_data.push_str(&line);
|
||||||
|
}
|
||||||
|
|
||||||
|
json_data = json_data
|
||||||
|
.replace(",}", "}")
|
||||||
|
.trim_start_matches(": ")
|
||||||
|
.trim_end_matches(',')
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
json_data
|
||||||
|
}
|
||||||
|
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_to_json() {
|
||||||
|
let vdf_data = "\"users\"
|
||||||
|
{
|
||||||
|
\"76561198315078806\"
|
||||||
|
{
|
||||||
|
\"AccountName\" \"_jerry_dota2\"
|
||||||
|
\"PersonaName\" \"Rop紫(已黑化)\"
|
||||||
|
\"RememberPassword\" \"1\"
|
||||||
|
\"WantsOfflineMode\" \"0\"
|
||||||
|
\"SkipOfflineModeWarning\" \"0\"
|
||||||
|
\"AllowAutoLogin\" \"1\"
|
||||||
|
\"MostRecent\" \"1\"
|
||||||
|
\"Timestamp\" \"1742706884\"
|
||||||
|
}
|
||||||
|
\"76561198107125441\"
|
||||||
|
{
|
||||||
|
\"AccountName\" \"_im_ai_\"
|
||||||
|
\"PersonaName\" \"Buongiorno\"
|
||||||
|
\"RememberPassword\" \"1\"
|
||||||
|
\"WantsOfflineMode\" \"0\"
|
||||||
|
\"SkipOfflineModeWarning\" \"0\"
|
||||||
|
\"AllowAutoLogin\" \"1\"
|
||||||
|
\"MostRecent\" \"0\"
|
||||||
|
\"Timestamp\" \"1739093763\"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
";
|
||||||
|
|
||||||
|
// let expected_json = r#"{"key1": "value1","key2": "value2","subkey": {"key3": "value3"}}"#;
|
||||||
|
let json_data = to_json(vdf_data);
|
||||||
|
|
||||||
|
// 解析json
|
||||||
|
let json_value: serde_json::Value = serde_json::from_str(&json_data).unwrap();
|
||||||
|
|
||||||
|
println!("{}", json_value)
|
||||||
|
// assert_eq!(to_json(vdf_data), expected_json);
|
||||||
|
}
|
||||||
|
}
|
||||||
259
src-tauri/src/vdf/preset.rs
Normal file
259
src-tauri/src/vdf/preset.rs
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use base64::engine::general_purpose::STANDARD;
|
||||||
|
use base64::Engine;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
use crate::steam;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct User {
|
||||||
|
steam_id64: u64,
|
||||||
|
steam_id32: u32,
|
||||||
|
account_name: String,
|
||||||
|
persona_name: String,
|
||||||
|
recent: i32,
|
||||||
|
avatar: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct LoginUser {
|
||||||
|
steam_id64: u64,
|
||||||
|
steam_id32: u32,
|
||||||
|
account_name: String,
|
||||||
|
persona_name: String,
|
||||||
|
remember_password: String,
|
||||||
|
wants_offline_mode: String,
|
||||||
|
skip_offline_mode_warning: String,
|
||||||
|
allow_auto_login: String,
|
||||||
|
most_recent: String,
|
||||||
|
timestamp: String,
|
||||||
|
avatar: String,
|
||||||
|
avatar_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct LocalUser {
|
||||||
|
steam_id32: u32,
|
||||||
|
persona_name: String,
|
||||||
|
avatar_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_login_users(steam_dir: &str) -> Result<Vec<LoginUser>> {
|
||||||
|
let t_path = Path::new(steam_dir).join("config/loginusers.vdf");
|
||||||
|
if !t_path.exists() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = fs::read_to_string(t_path)?;
|
||||||
|
let json_data = super::parse::to_json(&data);
|
||||||
|
let kv: HashMap<String, Value> = serde_json::from_str(&json_data)?;
|
||||||
|
|
||||||
|
let mut users = Vec::new();
|
||||||
|
for (k, v) in kv {
|
||||||
|
let props = v.as_object().unwrap();
|
||||||
|
|
||||||
|
let avatar = if let Some(img) = read_avatar(&steam_dir, &k) {
|
||||||
|
img
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let id64 = k.parse::<u64>()?;
|
||||||
|
|
||||||
|
let user = LoginUser {
|
||||||
|
steam_id32: steam::id::id64_to_32(id64),
|
||||||
|
steam_id64: id64,
|
||||||
|
account_name: props
|
||||||
|
.get("AccountName")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string(),
|
||||||
|
persona_name: props
|
||||||
|
.get("PersonaName")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string(),
|
||||||
|
remember_password: props
|
||||||
|
.get("RememberPassword")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string(),
|
||||||
|
wants_offline_mode: props
|
||||||
|
.get("WantsOfflineMode")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string(),
|
||||||
|
skip_offline_mode_warning: props
|
||||||
|
.get("SkipOfflineModeWarning")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string(),
|
||||||
|
allow_auto_login: props
|
||||||
|
.get("AllowAutoLogin")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string(),
|
||||||
|
most_recent: props
|
||||||
|
.get("MostRecent")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string(),
|
||||||
|
timestamp: props
|
||||||
|
.get("Timestamp")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string(),
|
||||||
|
avatar,
|
||||||
|
avatar_key: String::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
users.push(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_local_users(steam_dir: &str) -> Result<Vec<LocalUser>> {
|
||||||
|
let root = Path::new(steam_dir).join("userdata");
|
||||||
|
if !root.exists() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut local_users = Vec::new();
|
||||||
|
|
||||||
|
for entry in WalkDir::new(&root) {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
// 跳过根目录
|
||||||
|
if path == root {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只处理目录
|
||||||
|
if entry.file_type().is_dir() {
|
||||||
|
let id = path.file_name().unwrap().to_str().unwrap();
|
||||||
|
|
||||||
|
// 检查 localconfig.vdf 文件是否存在
|
||||||
|
let local_config_path = path.join("config/localconfig.vdf");
|
||||||
|
if !local_config_path.exists() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取并解析 localconfig.vdf 文件
|
||||||
|
let data = fs::read_to_string(local_config_path)?;
|
||||||
|
let json_data = super::parse::to_json(&data);
|
||||||
|
let kv: HashMap<String, Value> = serde_json::from_str(&json_data)?;
|
||||||
|
|
||||||
|
// 获取 friends 节点
|
||||||
|
let friends = kv.get("friends").and_then(|v| v.as_object());
|
||||||
|
if friends.is_none() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let friends = friends.unwrap();
|
||||||
|
|
||||||
|
// 获取 PersonaName
|
||||||
|
let persona_name = friends
|
||||||
|
.get("PersonaName")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// 获取 AvatarKey
|
||||||
|
let avatar_key = friends
|
||||||
|
.get(id)
|
||||||
|
.and_then(|v| v.as_object())
|
||||||
|
.and_then(|props| props.get("avatar"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
// 创建 LocalUser 并加入列表
|
||||||
|
local_users.push(LocalUser {
|
||||||
|
steam_id32: id.parse::<u32>().unwrap(),
|
||||||
|
persona_name,
|
||||||
|
avatar_key,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 跳过子目录
|
||||||
|
WalkDir::new(path).max_depth(1).into_iter().next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(local_users)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_avatar(steam_dir: &str, steam_id64: &str) -> Option<String> {
|
||||||
|
let t_path = Path::new(steam_dir).join(format!("avatarcache/{}.png", steam_id64));
|
||||||
|
if !t_path.exists() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(img) = fs::read(t_path) {
|
||||||
|
Some(STANDARD.encode(img))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_users(steam_dir: &str) -> Result<Vec<User>> {
|
||||||
|
let login_users = parse_login_users(steam_dir)?;
|
||||||
|
let local_users = parse_local_users(steam_dir)?;
|
||||||
|
let users = merge_users(login_users, local_users);
|
||||||
|
|
||||||
|
Ok(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn merge_users(login: Vec<LoginUser>, local: Vec<LocalUser>) -> Vec<User> {
|
||||||
|
let mut users = Vec::new();
|
||||||
|
for i in login {
|
||||||
|
let mut id32: u32 = 0;
|
||||||
|
let mut avatar = i.avatar;
|
||||||
|
let mut avatar_key = String::new();
|
||||||
|
|
||||||
|
let t_usr: Vec<&LocalUser> = local
|
||||||
|
.iter()
|
||||||
|
.filter(|j| i.persona_name == j.persona_name)
|
||||||
|
.collect();
|
||||||
|
if t_usr.len() > 1 {
|
||||||
|
id32 = steam::id::id64_to_32(i.steam_id64);
|
||||||
|
} else if t_usr.len() == 1 {
|
||||||
|
avatar_key = t_usr[0].avatar_key.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
if avatar.is_empty() && !avatar_key.is_empty() {
|
||||||
|
avatar = download_avatar(&avatar_key).unwrap_or_default();
|
||||||
|
}
|
||||||
|
|
||||||
|
users.push(User {
|
||||||
|
steam_id64: i.steam_id64,
|
||||||
|
steam_id32: id32,
|
||||||
|
account_name: i.account_name,
|
||||||
|
persona_name: i.persona_name,
|
||||||
|
recent: i.most_recent.parse().unwrap_or(0),
|
||||||
|
avatar,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 把第一个recent=1的放在头部
|
||||||
|
for (id, usr) in users.iter_mut().enumerate() {
|
||||||
|
if usr.recent == 1 {
|
||||||
|
let tmp = users.remove(id);
|
||||||
|
users.insert(0, tmp);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
users
|
||||||
|
}
|
||||||
|
|
||||||
|
fn download_avatar(avatar_key: &str) -> Result<String> {
|
||||||
|
// Implement avatar download logic here
|
||||||
|
Ok(String::new())
|
||||||
|
}
|
||||||
1199
src-tauri/src/vdf/tests/config.vdf
Normal file
1199
src-tauri/src/vdf/tests/config.vdf
Normal file
File diff suppressed because one or more lines are too long
35
src-tauri/src/vdf/tests/cs2_video.txt
Normal file
35
src-tauri/src/vdf/tests/cs2_video.txt
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
"video.cfg"
|
||||||
|
{
|
||||||
|
"Version" "15"
|
||||||
|
"VendorID" "4318"
|
||||||
|
"DeviceID" "9861"
|
||||||
|
"setting.cpu_level" "3"
|
||||||
|
"setting.gpu_mem_level" "3"
|
||||||
|
"setting.gpu_level" "3"
|
||||||
|
"setting.knowndevice" "0"
|
||||||
|
"setting.defaultres" "2880"
|
||||||
|
"setting.defaultresheight" "2160"
|
||||||
|
"setting.refreshrate_numerator" "0"
|
||||||
|
"setting.refreshrate_denominator" "0"
|
||||||
|
"setting.fullscreen" "1"
|
||||||
|
"setting.coop_fullscreen" "0"
|
||||||
|
"setting.nowindowborder" "1"
|
||||||
|
"setting.mat_vsync" "0"
|
||||||
|
"setting.fullscreen_min_on_focus_loss" "1"
|
||||||
|
"setting.high_dpi" "0"
|
||||||
|
"AutoConfig" "2"
|
||||||
|
"setting.shaderquality" "0"
|
||||||
|
"setting.r_texturefilteringquality" "3"
|
||||||
|
"setting.msaa_samples" "2"
|
||||||
|
"setting.r_csgo_cmaa_enable" "0"
|
||||||
|
"setting.videocfg_shadow_quality" "0"
|
||||||
|
"setting.videocfg_dynamic_shadows" "1"
|
||||||
|
"setting.videocfg_texture_detail" "1"
|
||||||
|
"setting.videocfg_particle_detail" "0"
|
||||||
|
"setting.videocfg_ao_detail" "0"
|
||||||
|
"setting.videocfg_hdr_detail" "3"
|
||||||
|
"setting.videocfg_fsr_detail" "0"
|
||||||
|
"setting.monitor_index" "0"
|
||||||
|
"setting.r_low_latency" "1"
|
||||||
|
"setting.aspectratiomode" "1"
|
||||||
|
}
|
||||||
4406
src-tauri/src/vdf/tests/localconfig.vdf
Normal file
4406
src-tauri/src/vdf/tests/localconfig.vdf
Normal file
File diff suppressed because one or more lines are too long
25
src-tauri/src/vdf/tests/loginusers.vdf
Normal file
25
src-tauri/src/vdf/tests/loginusers.vdf
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"users"
|
||||||
|
{
|
||||||
|
"76561198315078806"
|
||||||
|
{
|
||||||
|
"AccountName" "_jerry_dota2"
|
||||||
|
"PersonaName" "Rop紫(已黑化)"
|
||||||
|
"RememberPassword" "1"
|
||||||
|
"WantsOfflineMode" "0"
|
||||||
|
"SkipOfflineModeWarning" "0"
|
||||||
|
"AllowAutoLogin" "1"
|
||||||
|
"MostRecent" "1"
|
||||||
|
"Timestamp" "1742706884"
|
||||||
|
}
|
||||||
|
"76561198107125441"
|
||||||
|
{
|
||||||
|
"AccountName" "_im_ai_"
|
||||||
|
"PersonaName" "Buongiorno"
|
||||||
|
"RememberPassword" "1"
|
||||||
|
"WantsOfflineMode" "0"
|
||||||
|
"SkipOfflineModeWarning" "0"
|
||||||
|
"AllowAutoLogin" "1"
|
||||||
|
"MostRecent" "0"
|
||||||
|
"Timestamp" "1739093763"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ export default function Page() {
|
|||||||
<p>游戏路径:{steam.state.cs2Dir}</p>
|
<p>游戏路径:{steam.state.cs2Dir}</p>
|
||||||
<p>Steam路径有效:{steam.state.steamDirValid ? "是" : "否"}</p>
|
<p>Steam路径有效:{steam.state.steamDirValid ? "是" : "否"}</p>
|
||||||
<p>游戏路径有效:{steam.state.cs2DirValid ? "是" : "否"}</p>
|
<p>游戏路径有效:{steam.state.cs2DirValid ? "是" : "否"}</p>
|
||||||
<p>Steam账号:{steam.currentUser()?.accountName || " "}</p>
|
<p>Steam账号:{steam.currentUser()?.account_name || " "}</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,12 +15,18 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
const steam = useSteamStore()
|
const steam = useSteamStore()
|
||||||
const debounceSteamDir = useDebounce(steam.state.steamDir, {wait: 500, leading: true, trailing: true, maxWait: 2500})
|
const debounceSteamDir = useDebounce(steam.state.steamDir, {wait: 500, leading: true, trailing: true, maxWait: 2500})
|
||||||
const debounceCs2Dir = useDebounce(steam.state.cs2Dir, {wait: 500, leading: true, trailing: true, maxWait: 2500})
|
const debounceCs2Dir = useDebounce(steam.state.cs2Dir, {wait: 500, leading: true, trailing: true, maxWait: 2500})
|
||||||
|
const debounceSteamDirValid = useDebounce(steam.state.steamDirValid, {wait: 500, leading: true, trailing: true, maxWait: 2500})
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void steam.checkSteamDirValid()
|
void steam.checkSteamDirValid()
|
||||||
}, [debounceSteamDir])
|
}, [debounceSteamDir])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void steam.checkCs2DirValid()
|
void steam.checkCs2DirValid()
|
||||||
}, [debounceCs2Dir])
|
}, [debounceCs2Dir])
|
||||||
|
useEffect(() => {
|
||||||
|
if (debounceSteamDirValid) {
|
||||||
|
void steam.getUsers()
|
||||||
|
}
|
||||||
|
}, [debounceSteamDirValid])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|||||||
@@ -1,10 +1,23 @@
|
|||||||
import { User } from "@icon-park/react"
|
import { Refresh, User } from "@icon-park/react"
|
||||||
import { Card, CardBody, CardHeader, CardIcon } from "../window/Card"
|
import { Card, CardBody, CardHeader, CardIcon, CardTool } from "../window/Card"
|
||||||
import { Button, Chip } from "@heroui/react"
|
import { addToast, Button, Chip } from "@heroui/react"
|
||||||
import { useSteamStore } from "@/store/steam"
|
import { useSteamStore } from "@/store/steam"
|
||||||
|
import { ToolButton } from "../window/ToolButton"
|
||||||
|
import { useAutoAnimate } from "@formkit/auto-animate/react"
|
||||||
|
|
||||||
const SteamUsers = ({ className }: { className?: string }) => {
|
const SteamUsers = ({ className }: { className?: string }) => {
|
||||||
const steam = useSteamStore()
|
const steam = useSteamStore()
|
||||||
|
const [parent /* , enableAnimations */] = useAutoAnimate(/* optional config */)
|
||||||
|
|
||||||
|
const getUsers = async (toast?: boolean) => {
|
||||||
|
if (!steam.state.steamDirValid) {
|
||||||
|
if (toast) addToast({ title: "Steam路径不可用", color: "warning" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await steam.getUsers()
|
||||||
|
|
||||||
|
if (toast) addToast({ title: `已获取Steam用户` })
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card /* className={cn("max-w-96", className)} */>
|
<Card /* className={cn("max-w-96", className)} */>
|
||||||
@@ -12,12 +25,18 @@ const SteamUsers = ({ className }: { className?: string }) => {
|
|||||||
<CardIcon>
|
<CardIcon>
|
||||||
<User /> Steam用户
|
<User /> Steam用户
|
||||||
</CardIcon>
|
</CardIcon>
|
||||||
|
<CardTool>
|
||||||
|
<ToolButton onClick={() => getUsers(true)}>
|
||||||
|
<Refresh />
|
||||||
|
刷新
|
||||||
|
</ToolButton>
|
||||||
|
</CardTool>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<ul>
|
<ul className="flex flex-col gap-3 mt-1" ref={parent}>
|
||||||
{steam.state.users.map((user) => (
|
{steam.state.users.map((user, id) => (
|
||||||
<li
|
<li
|
||||||
key={user.accountName}
|
key={user.account_name}
|
||||||
className="flex gap-2 transition rounded-lg bg-zinc-50 dark:bg-zinc-900"
|
className="flex gap-2 transition rounded-lg bg-zinc-50 dark:bg-zinc-900"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -26,22 +45,33 @@ const SteamUsers = ({ className }: { className?: string }) => {
|
|||||||
className="w-20 h-20 rounded-l-lg"
|
className="w-20 h-20 rounded-l-lg"
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col flex-grow justify-center gap-2 p-0.5">
|
<div className="flex flex-col flex-grow justify-center gap-2 p-0.5">
|
||||||
<h3 className="text-2xl font-semibold">{user.personaName}</h3>
|
<h3 className="text-xl font-semibold">{user.persona_name}</h3>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Chip size="sm" className="bg-zinc-200 dark:bg-zinc-800">
|
<Chip size="sm" className="bg-zinc-200 dark:bg-zinc-800">
|
||||||
{user.accountName}
|
{user.account_name}
|
||||||
</Chip>
|
</Chip>
|
||||||
<Chip size="sm" className="bg-zinc-200 dark:bg-zinc-800">
|
<Chip size="sm" className="bg-zinc-200 dark:bg-zinc-800">
|
||||||
{user.steamID32}
|
{user.steam_id32}
|
||||||
</Chip>
|
</Chip>
|
||||||
<Chip size="sm" className="bg-zinc-200 dark:bg-zinc-800">
|
<Chip size="sm" className="bg-zinc-200 dark:bg-zinc-800">
|
||||||
{user.steamID64}
|
{user.steam_id64?.toString()}
|
||||||
|
</Chip>
|
||||||
|
<Chip size="sm" className="bg-zinc-200 dark:bg-zinc-800">
|
||||||
|
{user.recent}
|
||||||
|
</Chip>
|
||||||
|
|
||||||
|
<Chip size="sm" className="bg-zinc-200 dark:bg-zinc-800">
|
||||||
|
{user.avatar}
|
||||||
</Chip>
|
</Chip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end gap-2 p-2">
|
<div className="flex items-end gap-2 p-2">
|
||||||
<Button size="sm">切换登录</Button>
|
<Button size="sm" onPress={() => steam.switchLoginUser(id)}>
|
||||||
<Button size="sm">选择</Button>
|
切换登录
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" onPress={() => steam.selectUser(id)}>
|
||||||
|
选择
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ const VideoSetting = () => {
|
|||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
{videoSettings.map((vid, index) => (
|
{videoSettings.map((vid, index) => (
|
||||||
<li className="flex flex-col gap-1.5">
|
<li className="flex flex-col gap-1.5" key={index}>
|
||||||
<span className="ml-2">{vid.title}</span>
|
<span className="ml-2">{vid.title}</span>
|
||||||
<Tabs
|
<Tabs
|
||||||
selectedKey={vid.value}
|
selectedKey={vid.value}
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ const Header = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pt-6 select-none pb-9" data-tauri-drag-region>
|
<div className="pt-6 select-none pb-9" data-tauri-drag-region>
|
||||||
<h1 className="mb-0.5 text-xl font-medium tracking-wide w-fit">
|
<h1 className="mb-0.5 text-xl font-semibold tracking-wide w-fit">
|
||||||
{steam.currentUser()?.personaName || 'CS工具箱'}
|
{steam.currentUser()?.persona_name || 'CS工具箱'}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm font-light tracking-wide text-zinc-400 w-fit">
|
<p className="text-sm font-light tracking-wide text-zinc-400 w-fit">
|
||||||
{steam.currentUser()?.accountName || '本周使用CS工具箱 114 小时'}
|
{steam.currentUser()?.account_name || '本周使用CS工具箱 114 小时'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ export const useSteamStore = () => {
|
|||||||
checkSteamDirValid,
|
checkSteamDirValid,
|
||||||
checkCs2DirValid,
|
checkCs2DirValid,
|
||||||
currentUser,
|
currentUser,
|
||||||
|
getUsers,
|
||||||
|
selectUser,
|
||||||
|
switchLoginUser,
|
||||||
resetSteamStore,
|
resetSteamStore,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,29 +64,29 @@ const setCs2DirValid = (valid: boolean) => {
|
|||||||
steamStore.state.cs2DirValid = valid
|
steamStore.state.cs2DirValid = valid
|
||||||
}
|
}
|
||||||
|
|
||||||
const SetSteamDirChecking = (checking: boolean) => {
|
const setSteamDirChecking = (checking: boolean) => {
|
||||||
steamStore.state.steamDirChecking = checking
|
steamStore.state.steamDirChecking = checking
|
||||||
}
|
}
|
||||||
|
|
||||||
const SetCs2DirChecking = (checking: boolean) => {
|
const setCs2DirChecking = (checking: boolean) => {
|
||||||
steamStore.state.cs2DirChecking = checking
|
steamStore.state.cs2DirChecking = checking
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkSteamDirValid = async () => {
|
const checkSteamDirValid = async () => {
|
||||||
SetSteamDirChecking(true)
|
setSteamDirChecking(true)
|
||||||
const pathExist = await invoke<boolean>("check_path", { path: steamStore.state.steamDir })
|
const pathExist = await invoke<boolean>("check_path", { path: steamStore.state.steamDir })
|
||||||
setSteamDirValid(pathExist)
|
setSteamDirValid(pathExist)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
SetSteamDirChecking(false)
|
setSteamDirChecking(false)
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkCs2DirValid = async () => {
|
const checkCs2DirValid = async () => {
|
||||||
SetCs2DirChecking(true)
|
setCs2DirChecking(true)
|
||||||
const pathExist = await invoke<boolean>("check_path", { path: steamStore.state.cs2Dir })
|
const pathExist = await invoke<boolean>("check_path", { path: steamStore.state.cs2Dir })
|
||||||
setCs2DirValid(pathExist)
|
setCs2DirValid(pathExist)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
SetCs2DirChecking(false)
|
setCs2DirChecking(false)
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,12 +94,37 @@ const currentUser = () => {
|
|||||||
return steamStore.state.users.at(0) || undefined
|
return steamStore.state.users.at(0) || undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getUsers = async () => {
|
||||||
|
const users = await invoke<SteamUser[]>("get_steam_users", { steamDir: steamStore.state.steamDir })
|
||||||
|
console.log(users)
|
||||||
|
setUsers(users)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectUser = async (index: number) => {
|
||||||
|
const user = steamStore.state.users.at(index)
|
||||||
|
console.log(index, user)
|
||||||
|
if (user) {
|
||||||
|
setUsers([
|
||||||
|
user,
|
||||||
|
...steamStore.state.users.slice(0, index),
|
||||||
|
...steamStore.state.users.slice(index + 1),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const switchLoginUser = async (index: number) => {
|
||||||
|
const user = steamStore.state.users.at(index)
|
||||||
|
if (user) {
|
||||||
|
await invoke<SteamUser[]>("set_auto_login_user", { user: user.account_name })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const resetSteamStore = () => {
|
const resetSteamStore = () => {
|
||||||
setDir(defaultValue.steamDir)
|
setDir(defaultValue.steamDir)
|
||||||
setCsDir(defaultValue.cs2Dir)
|
setCsDir(defaultValue.cs2Dir)
|
||||||
setUsers(defaultValue.users)
|
setUsers(defaultValue.users)
|
||||||
setSteamDirValid(defaultValue.steamDirValid)
|
setSteamDirValid(defaultValue.steamDirValid)
|
||||||
setCs2DirValid(defaultValue.cs2DirValid)
|
setCs2DirValid(defaultValue.cs2DirValid)
|
||||||
SetSteamDirChecking(defaultValue.steamDirChecking)
|
setSteamDirChecking(defaultValue.steamDirChecking)
|
||||||
SetCs2DirChecking(defaultValue.cs2DirChecking)
|
setCs2DirChecking(defaultValue.cs2DirChecking)
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { AdvancedListItem } from "@/types/common"
|
import type { AdvancedListItem } from "@/types/common"
|
||||||
|
|
||||||
export interface SteamUser extends AdvancedListItem {
|
export interface SteamUser extends AdvancedListItem {
|
||||||
steamID64: string
|
steam_id64: BigInt
|
||||||
steamID32: string
|
steam_id32: number
|
||||||
accountName: string
|
account_name: string
|
||||||
personaName: string
|
persona_name: string
|
||||||
recent: number
|
recent: number
|
||||||
avatar: string
|
avatar: string
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user