diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2f05de5..e6d2867 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,8 @@ jobs: fail-fast: false matrix: platform: - # TODO 其他平台均支持打包,但未经过测试,故暂时屏蔽掉 + # TODO other platforms are supported for packaging, but not tested, so they are temporarily disabled + # 其他平台均支持打包,但未经过测试,故暂时屏蔽掉 # - "macos-latest" # - "ubuntu-latest" - "windows-latest" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a99538a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,34 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- v1.1 Brazilian Portuguese translation. +- v1.1 German Translation +- v1.1 Spanish translation. +- v1.1 Italian translation. +- v1.1 Polish translation. +- v1.1 Ukrainian translation. + +### Changed + +- Use frontmatter title & description in each language version template +- Replace broken OpenGraph image with an appropriately-sized Keep a Changelog + image that will render properly (although in English for all languages) +- Fix OpenGraph title & description for all languages so the title and +description when links are shared are language-appropriate + +### Removed + +- Trademark sign previously shown after the project description in version +0.3.0 + +### Fixed + +- Broken OpenGraph image in all language versions diff --git a/app-icon.png b/app-icon.png new file mode 100644 index 0000000..b1b61c9 Binary files /dev/null and b/app-icon.png differ diff --git a/index.html b/index.html index c184456..db55254 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + Tauri + Vue + Typescript App diff --git a/package.json b/package.json index 72fd95b..9503625 100644 --- a/package.json +++ b/package.json @@ -15,19 +15,20 @@ "dependencies": { "@tauri-apps/api": "^1", "vite-plugin-vuetify": "^2.0.3", - "vue": "^3.3.4", + "vue": "^3.4.27", "vue-i18n": "9", - "vue-router": "4", - "vuetify": "^3.6.8" + "vue-router": "^4.3.3", + "vuetify": "^3.6.9" }, "devDependencies": { "@mdi/font": "^7.4.47", "@tauri-apps/cli": "^1", "@vitejs/plugin-vue": "^5.0.4", "prettier": "3.3.2", - "sass": "^1.77.4", + "sass": "^1.77.5", "typescript": "^5.0.2", - "vite": "^5.0.0", - "vue-tsc": "^1.8.5" - } + "vite": "^5.2.13", + "vue-tsc": "^2.0.21" + }, + "packageManager": "yarn@1.22.19" } diff --git a/public/tauri.svg b/public/tauri.svg deleted file mode 100644 index 31b62c9..0000000 --- a/public/tauri.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 9ec7582..dae5180 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -67,6 +67,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "anyhow" version = "1.0.86" @@ -409,6 +458,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + [[package]] name = "combine" version = "4.6.7" @@ -759,6 +814,29 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -863,6 +941,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futf" version = "0.1.5" @@ -1327,6 +1411,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -1453,6 +1543,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itoa" version = "0.4.8" @@ -1815,6 +1911,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "objc" version = "0.2.7" @@ -2661,6 +2766,17 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simplelog" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" +dependencies = [ + "log", + "termcolor", + "time", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -3099,6 +3215,15 @@ dependencies = [ "utf-8", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thin-slice" version = "0.1.1" @@ -3143,7 +3268,9 @@ checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa 1.0.11", + "libc", "num-conv", + "num_threads", "powerfmt", "serde", "time-core", @@ -3373,6 +3500,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.8.0" @@ -4052,9 +4185,13 @@ dependencies = [ name = "wt-helper-app" version = "0.0.0" dependencies = [ + "env_logger", + "fs_extra", + "log", "serde", "serde_json", "sevenz-rust", + "simplelog", "tauri", "tauri-build", "tempfile", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 64e5a68..096c2e8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -11,7 +11,10 @@ edition = "2021" tauri-build = { version = "1", features = [] } [dependencies] -tauri = { version = "1", features = [ "window-start-dragging", +tauri = { version = "1", features = [ + "window-create", + "window-show", + "window-start-dragging", "fs-all", "path-all", "dialog-open", @@ -23,10 +26,15 @@ sevenz-rust = "0.6.0" zip = "2.1.3" tempfile = "3" walkdir = "2.5.0" +log = "0.4.21" +fs_extra = "1.3.0" +env_logger = "0.11.3" +simplelog = "0.12.2" [target.'cfg(target_os = "windows")'.dependencies] winreg = "0.52.0" + [features] # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! custom-protocol = ["tauri/custom-protocol"] diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png index 6be5e50..9fc0560 100644 Binary files a/src-tauri/icons/128x128.png and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png index e81bece..cd97405 100644 Binary files a/src-tauri/icons/128x128@2x.png and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png index a437dd5..d21e103 100644 Binary files a/src-tauri/icons/32x32.png and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png index 0ca4f27..04818dd 100644 Binary files a/src-tauri/icons/Square107x107Logo.png and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png index b81f820..90c1fc2 100644 Binary files a/src-tauri/icons/Square142x142Logo.png and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png index 624c7bf..606fadc 100644 Binary files a/src-tauri/icons/Square150x150Logo.png and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png index c021d2b..0ce2868 100644 Binary files a/src-tauri/icons/Square284x284Logo.png and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png index 6219700..f196c35 100644 Binary files a/src-tauri/icons/Square30x30Logo.png and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png index f9bc048..f1bb4db 100644 Binary files a/src-tauri/icons/Square310x310Logo.png and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png index d5fbfb2..5529c3e 100644 Binary files a/src-tauri/icons/Square44x44Logo.png and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png index 63440d7..be10db5 100644 Binary files a/src-tauri/icons/Square71x71Logo.png and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png index f3f705a..5a4169b 100644 Binary files a/src-tauri/icons/Square89x89Logo.png and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png index 4556388..b440e7c 100644 Binary files a/src-tauri/icons/StoreLogo.png and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns index 12a5bce..871f064 100644 Binary files a/src-tauri/icons/icon.icns and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico index b3636e4..2226e78 100644 Binary files a/src-tauri/icons/icon.ico and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png index e1cd261..01d4d7f 100644 Binary files a/src-tauri/icons/icon.png and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/src/commands/log.rs b/src-tauri/src/commands/log.rs new file mode 100644 index 0000000..0d7d307 --- /dev/null +++ b/src-tauri/src/commands/log.rs @@ -0,0 +1,10 @@ +use crate::ret_code::RetCode; + +#[tauri::command] +pub fn get_app_log_dir(app: tauri::AppHandle) -> Result { + let app_log_dir = app + .path_resolver() + .app_log_dir() + .ok_or(RetCode::GetAppLogPathFailed)?; + Ok(app_log_dir.to_str().unwrap().to_string()) +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs deleted file mode 100644 index c6ad6c4..0000000 --- a/src-tauri/src/commands/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod manage_wt; -pub mod os; diff --git a/src-tauri/src/commands/os.rs b/src-tauri/src/commands/os.rs index 7eb417a..4c49894 100644 --- a/src-tauri/src/commands/os.rs +++ b/src-tauri/src/commands/os.rs @@ -1,5 +1,3 @@ -use std::fs; -use std::path::Path; use std::process::Command; #[tauri::command] @@ -28,32 +26,5 @@ pub fn show_in_folder(path: String) { #[tauri::command] pub fn delete_folder(path: String) -> Result<(), String> { - let path = Path::new(&path); - - // 检查路径是否存在 - if !path.exists() { - return Err(format!("Path does not exist: {}", path.display())); - } - - // 检查是否是目录 - if !path.is_dir() { - return Err(format!("Path is not a directory: {}", path.display())); - } - - // 尝试删除目录及其内容 - match fs::remove_dir_all(path) { - Ok(_) => Ok(()), - Err(err) => Err(format!("Failed to delete folder: {}", err)), - } -} - -#[tauri::command] -pub fn create_folder(path: String) { - let path = Path::new(&path); - - // 检查路径是否存在 - if !path.exists() { - // 尝试创建目录 - fs::create_dir_all(path).unwrap(); - } + fs_extra::dir::remove(path).map_err(|e| e.to_string()) } diff --git a/src-tauri/src/commands/setting.rs b/src-tauri/src/commands/setting.rs new file mode 100644 index 0000000..39b1903 --- /dev/null +++ b/src-tauri/src/commands/setting.rs @@ -0,0 +1,42 @@ +use log::{debug, error}; + +use crate::{ + config::{self, AppConfig}, + ret_code::RetCode, + WrappedState, +}; + +#[tauri::command] +pub fn get_app_config(state: tauri::State) -> Result { + Ok(state.lock().unwrap().as_ref().unwrap().config.clone()) +} + +#[tauri::command] +pub fn save_app_config( + app: tauri::AppHandle, + state: tauri::State, + config: AppConfig, +) -> Result<(), RetCode> { + let app_config_dir = app + .path_resolver() + .app_config_dir() + .ok_or(RetCode::SaveAppSettingsFailed)?; + debug!("save config to {:?}", app_config_dir); + config::save_config(&app_config_dir, &config).map_err(|e| { + error!("save config error: {}", e); + RetCode::SaveAppSettingsFailed + })?; + debug!("save config to state"); + state.lock().unwrap().as_mut().unwrap().config = config; + + Ok(()) +} + +#[tauri::command] +pub fn get_app_config_dir(app: tauri::AppHandle) -> Result { + let app_config_dir = app + .path_resolver() + .app_config_dir() + .ok_or(RetCode::GetAppSettingsFailed)?; + Ok(app_config_dir.to_str().unwrap().to_string()) +} diff --git a/src-tauri/src/commands/manage_wt.rs b/src-tauri/src/commands/war_thunder.rs similarity index 60% rename from src-tauri/src/commands/manage_wt.rs rename to src-tauri/src/commands/war_thunder.rs index 132c893..5113493 100644 --- a/src-tauri/src/commands/manage_wt.rs +++ b/src-tauri/src/commands/war_thunder.rs @@ -1,12 +1,15 @@ use std::{ - fs, io, + fs, path::{Path, PathBuf}, }; use walkdir::WalkDir; -use zip::ZipArchive; + +use log::warn; + +use crate::{ret_code::RetCode, tools, WrappedState}; #[tauri::command] -pub fn auto_detected_wt_install_path() -> Result { +pub fn auto_detected_wt_root_path() -> Result { let paths_to_scan: Vec; if cfg!(target_os = "windows") { @@ -36,12 +39,11 @@ pub fn auto_detected_wt_install_path() -> Result { return Ok(path.to_str().unwrap().to_string()); } } - return Err("War Thunder install path not found".to_string()); + return Err(RetCode::AutoDetectedWtRootPathFailed); } #[tauri::command] -pub fn auto_detected_wt_setting_path() -> String { - // 写一个rust代码,判断 %USERPROFILE%\Documents\My Games\WarThunder\Saves存不存在 +pub fn auto_detected_wt_setting_path() -> Result { let user_profile = std::env::var("USERPROFILE").unwrap(); let path = std::path::Path::new(&user_profile) .join("Documents") @@ -49,19 +51,32 @@ pub fn auto_detected_wt_setting_path() -> String { .join("WarThunder") .join("Saves"); if path.exists() { - return path.to_str().unwrap().to_string(); + return Ok(path.to_str().unwrap().to_string()); } - return "".to_string(); + return Err(RetCode::AutoDetectedWtSettingPathFailed); } #[tauri::command] -pub fn install_user_skin(skin_path: String, wt_install_path: String) -> Result<(), String> { +pub fn install_user_skin( + skin_path: String, + state: tauri::State, +) -> Result<(), RetCode> { + let wt_root_path = state + .lock() + .unwrap() + .as_ref() + .unwrap() + .config + .clone() + .wt_root_path; + let skin_pb = PathBuf::from(&skin_path); if !skin_pb.exists() { - return Err(format!("Path does not exist: {}", skin_pb.display())); + warn!("Skin path not exists: {:?}", skin_pb); + return Err(RetCode::InstallUserSkinFailed); } // 如果涂装文件夹不存在,则创建 - let skin_base_path = Path::new(&wt_install_path).join("UserSkins"); + let skin_base_path = Path::new(&wt_root_path).join("UserSkins"); if !skin_base_path.exists() { fs::create_dir_all(skin_base_path).unwrap(); } @@ -69,35 +84,34 @@ pub fn install_user_skin(skin_path: String, wt_install_path: String) -> Result<( // 如果是文件夹,则判断文件夹下是否有.blk文件,如果有,则复制这个文件夹到 wt_install_path 下 if skin_pb.is_dir() { if !check_is_folder_contains_blk_file(&skin_pb) { - return Err(format!("Invalid skin folder: {}", skin_pb.display())); + warn!("Skin folder not contains blk file: {:?}", skin_pb); + return Err(RetCode::InstallUserSkinFailed); } - let new_path = Path::new(&wt_install_path) + let new_path = Path::new(&wt_root_path) .join("UserSkins") .join(skin_pb.file_name().unwrap()); if new_path.exists() { - return Err(format!( - "Skin folder already exists: {}", - new_path.display() - )); + warn!("Skin folder already exists: {:?}", new_path); + return Err(RetCode::InstallUserSkinFailed); } - match copy_everything_to(&skin_pb, &new_path) { + match tools::fs::copy_folder(&skin_pb, &new_path) { Ok(_) => Ok(()), - Err(err) => Err(format!("Failed to copy skin folder: {}", err)), + Err(err) => { + warn!("Copy skin folder failed: {:?}", err); + Err(RetCode::InstallUserSkinFailed) + } } } else { // 处理压缩文件 - let temp_dir = - tempfile::tempdir().map_err(|e| format!("Failed to create temp dir: {}", e))?; + let temp_dir = tempfile::tempdir().unwrap(); let temp_path = temp_dir.path(); - decompress_file(skin_pb, temp_path).unwrap(); + tools::fs::decompress_file(skin_pb.as_path(), temp_path).unwrap(); // 查找解压后的文件夹 let mut extracted_folder = None; - for entry in - fs::read_dir(&temp_path).map_err(|e| format!("Failed to read temp dir: {}", e))? - { - let entry = entry.map_err(|e| format!("Failed to read temp dir entry: {}", e))?; + for entry in fs::read_dir(&temp_path).unwrap() { + let entry = entry.unwrap(); if entry.path().is_dir() { extracted_folder = Some(entry.path()); break; @@ -106,72 +120,87 @@ pub fn install_user_skin(skin_path: String, wt_install_path: String) -> Result<( if let Some(extracted_folder) = extracted_folder { if !check_is_folder_contains_blk_file(&extracted_folder) { - return Err(format!( - "Invalid skin folder in archive: {}", - extracted_folder.display() - )); + warn!( + "Extracted folder not contains blk file: {:?}", + extracted_folder + ); + return Err(RetCode::InstallUserSkinFailed); } - let new_path = Path::new(&wt_install_path) + let new_path = Path::new(&wt_root_path) .join("UserSkins") .join(extracted_folder.file_name().unwrap()); if new_path.exists() { - return Err(format!( - "Skin folder already exists: {}", - new_path.display() - )); + warn!("Skin folder already exists: {:?}", new_path); + return Err(RetCode::InstallUserSkinFailed); } - match copy_everything_to(&extracted_folder, &new_path) { + match tools::fs::copy_folder(&extracted_folder, &new_path) { Ok(_) => Ok(()), - Err(err) => Err(format!("Failed to copy extracted skin folder: {}", err)), + Err(err) => { + warn!("Copy skin folder failed: {:?}", err); + Err(RetCode::InstallUserSkinFailed) + } } } else { - Err("No folder found in extracted archive".to_string()) + warn!("Extracted folder not found"); + Err(RetCode::InstallUserSkinFailed) } } } #[tauri::command] -pub fn install_user_sight(sight_path: String, wt_install_path: String) -> Result<(), String> { +pub fn install_user_sight( + sight_path: String, + state: tauri::State, +) -> Result<(), RetCode> { + let wt_root_path = state + .lock() + .unwrap() + .as_ref() + .unwrap() + .config + .clone() + .wt_root_path; + let sight_pb = PathBuf::from(&sight_path); if !sight_pb.exists() { - return Err(format!("Path does not exist: {}", sight_pb.display())); + warn!("Sight path not exists: {:?}", sight_pb); + return Err(RetCode::InstallUserSightFailed); } - let sight_base_path = Path::new(&wt_install_path).join("UserSights"); + let sight_base_path = Path::new(&wt_root_path).join("UserSights"); if !sight_base_path.exists() { fs::create_dir_all(sight_base_path).unwrap(); } if sight_pb.is_dir() { if !check_is_folder_contains_blk_file(&sight_pb) { - return Err(format!("Invalid sight folder: {}", sight_pb.display())); + warn!("Sight folder not contains blk file: {:?}", sight_pb); + return Err(RetCode::InstallUserSightFailed); } - let new_path = Path::new(&wt_install_path) + let new_path = Path::new(&wt_root_path) .join("UserSights") .join(sight_pb.file_name().unwrap()); if new_path.exists() { - return Err(format!( - "Sight folder already exists: {}", - new_path.display() - )); + warn!("Sight folder already exists: {:?}", new_path); + return Err(RetCode::InstallUserSightFailed); } - match copy_everything_to(&sight_pb, &new_path) { + match tools::fs::copy_folder(&sight_pb, &new_path) { Ok(_) => Ok(()), - Err(err) => Err(format!("Failed to copy sight folder: {}", err)), + Err(err) => { + warn!("Copy sight folder failed: {:?}", err); + Err(RetCode::InstallUserSightFailed) + } } } else { - let temp_dir = - tempfile::tempdir().map_err(|e| format!("Failed to create temp dir: {}", e))?; + let temp_dir = tempfile::tempdir().unwrap(); let temp_path = temp_dir.path(); - decompress_file(sight_pb, temp_path).unwrap(); + tools::fs::decompress_file(sight_pb.as_path(), temp_path).unwrap(); // 查找解压后的文件夹 let mut extracted_folder = None; - for entry in - fs::read_dir(&temp_path).map_err(|e| format!("Failed to read temp dir: {}", e))? - { - let entry = entry.map_err(|e| format!("Failed to read temp dir entry: {}", e))?; + for entry in fs::read_dir(&temp_path).unwrap() { + let entry = entry.unwrap(); if entry.path().is_dir() { extracted_folder = Some(entry.path()); break; @@ -180,112 +209,31 @@ pub fn install_user_sight(sight_path: String, wt_install_path: String) -> Result if let Some(extracted_folder) = extracted_folder { if !check_is_folder_contains_blk_file(&extracted_folder) { - return Err(format!( - "Invalid sight folder in archive: {}", - extracted_folder.display() - )); + warn!( + "Extracted folder not contains blk file: {:?}", + extracted_folder + ); + return Err(RetCode::InstallUserSightFailed); } - let new_path = Path::new(&wt_install_path) + let new_path = Path::new(&wt_root_path) .join("UserSights") .join(extracted_folder.file_name().unwrap()); if new_path.exists() { - return Err(format!( - "Sight folder already exists: {}", - new_path.display() - )); + warn!("Sight folder already exists: {:?}", new_path); + return Err(RetCode::InstallUserSightFailed); } - match copy_everything_to(&extracted_folder, &new_path) { + match tools::fs::copy_folder(&extracted_folder, &new_path) { Ok(_) => Ok(()), - Err(err) => Err(format!("Failed to copy extracted sight folder: {}", err)), - } - } else { - Err("No folder found in extracted archive".to_string()) - } - } -} - -fn decompress_file(pb: PathBuf, temp_path: &Path) -> Result<(), String> { - let extension: &str = pb.extension().and_then(|e| e.to_str()).unwrap_or(""); - match extension.to_lowercase().as_str() { - "zip" => { - let file = - fs::File::open(&pb).map_err(|e| format!("Failed to open zip file: {}", e))?; - let mut archive = - ZipArchive::new(file).map_err(|e| format!("Failed to read zip file: {}", e))?; - archive - .extract(&temp_path) - .map_err(|e| format!("Failed to extract zip file: {}", e))?; - } - "rar" => return Err(format!("not support rar file now")), - "7z" => { - sevenz_rust::decompress_file(&pb, &temp_path) - .map_err(|e| format!("Failed to extract 7z file: {}", e))?; - } - _ => return Err(format!("Unsupported file extension: {}", extension)), - } - Ok(()) -} - -#[tauri::command] -pub fn check_is_valid_wt_install_path(path: String) -> bool { - let path = Path::new(&path); - check_is_folder_contains_wt_launcher(path) -} - -// Check if the folder contains the War Thunder launcher -// TODO: Maybe not working on Linux and MacOS -fn check_is_folder_contains_wt_launcher(path: &Path) -> bool { - if !path.exists() { - return false; - } - if !path.is_dir() { - return false; - } - let launcher_names = vec!["launcher.exe"]; - - for name in launcher_names { - let launcher_path = path.join(name); - if launcher_path.exists() { - return true; - } - } - return false; -} - -fn copy_everything_to(old_path: &Path, new_path: &Path) -> io::Result<()> { - if old_path.is_dir() { - fs::create_dir(new_path)?; - for entry in fs::read_dir(old_path)? { - let entry = entry?; - let path = entry.path(); - let new_path = new_path.join(path.file_name().unwrap()); - copy_everything_to(&path, &new_path)?; - } - } else { - fs::copy(old_path, new_path)?; - } - Ok(()) -} - -fn check_is_folder_contains_blk_file(path: &PathBuf) -> bool { - if !path.exists() { - return false; - } - if !path.is_dir() { - return false; - } - for entry in fs::read_dir(path).unwrap() { - let entry = entry.unwrap(); - let path = entry.path(); - if path.is_file() { - if let Some(ext) = path.extension() { - if ext == "blk" { - return true; + Err(_err) => { + warn!("Copy sight folder failed: {:?}", _err); + Err(RetCode::InstallUserSightFailed) } } + } else { + warn!("Extracted folder not found"); + Err(RetCode::InstallUserSightFailed) } } - return false; } #[derive(Debug, serde::Serialize)] @@ -297,23 +245,22 @@ pub struct UserSkinInfo { folder_size: u64, } -fn calculate_directory_size(path: &Path) -> io::Result { - let mut total_size = 0; - for entry in WalkDir::new(path) { - let entry = entry?; - if entry.file_type().is_file() { - total_size += entry.metadata()?.len(); - } - } - Ok(total_size) -} - #[tauri::command] -pub fn get_user_skins(wt_install_path: String) -> Result, String> { - let skin_base_path = Path::new(&wt_install_path).join("UserSkins"); +pub fn get_user_skins(state: tauri::State) -> Result, RetCode> { + let wt_root_path = state + .lock() + .unwrap() + .as_ref() + .unwrap() + .config + .clone() + .wt_root_path; + + let skin_base_path = Path::new(&wt_root_path).join("UserSkins"); if !skin_base_path.exists() { - return Err("UserSkins folder not found".to_string()); + warn!("UserSkins folder not found"); + return Err(RetCode::GetUserSkinsFailed); } let mut infos = Vec::new(); for entry in WalkDir::new(&skin_base_path).min_depth(1).max_depth(10) { @@ -349,7 +296,7 @@ pub fn get_user_skins(wt_install_path: String) -> Result, Stri .to_string_lossy() .to_string(); let full_path = entry.path().to_string_lossy().to_string(); - let folder_size = calculate_directory_size(entry.path()).unwrap(); + let folder_size = fs_extra::dir::get_size(entry.path()).unwrap(); infos.push(UserSkinInfo { vehicle_id, skin_name, @@ -373,11 +320,21 @@ pub struct UserSightInfo { } #[tauri::command] -pub fn get_user_sights(wt_install_path: String) -> Result, String> { - let skin_base_path = Path::new(&wt_install_path).join("UserSights"); +pub fn get_user_sights(state: tauri::State) -> Result, RetCode> { + let wt_root_path = state + .lock() + .unwrap() + .as_ref() + .unwrap() + .config + .clone() + .wt_root_path; + + let skin_base_path = Path::new(&wt_root_path).join("UserSights"); if !skin_base_path.exists() { - return Err("UserSights folder not found".to_string()); + warn!("UserSights folder not found"); + return Err(RetCode::GetUserSightsFailed); } let mut infos = Vec::new(); for entry in WalkDir::new(&skin_base_path).min_depth(1).max_depth(10) { @@ -417,7 +374,7 @@ pub fn get_user_sights(wt_install_path: String) -> Result, St .to_string_lossy() .to_string(); let full_path = entry.path().to_string_lossy().to_string(); - let folder_size = calculate_directory_size(entry.path()).unwrap(); + let folder_size = fs_extra::dir::get_size(entry.path()).unwrap(); infos.push(UserSightInfo { vehicle_id, folder_name, @@ -430,3 +387,44 @@ pub fn get_user_sights(wt_install_path: String) -> Result, St } Ok(infos) } + +// Check if the folder contains the War Thunder launcher +// TODO: Maybe not working on Linux and MacOS +fn check_is_folder_contains_wt_launcher(path: &Path) -> bool { + if !path.exists() { + return false; + } + if !path.is_dir() { + return false; + } + let launcher_names = vec!["launcher.exe"]; + + for name in launcher_names { + let launcher_path = path.join(name); + if launcher_path.exists() { + return true; + } + } + return false; +} + +fn check_is_folder_contains_blk_file(path: &PathBuf) -> bool { + if !path.exists() { + return false; + } + if !path.is_dir() { + return false; + } + for entry in fs::read_dir(path).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.is_file() { + if let Some(ext) = path.extension() { + if ext == "blk" { + return true; + } + } + } + } + return false; +} diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs new file mode 100644 index 0000000..2b9231c --- /dev/null +++ b/src-tauri/src/config.rs @@ -0,0 +1,75 @@ +use std::path::PathBuf; + +use log::debug; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AppConfig { + pub wt_root_path: String, + pub wt_setting_path: String, +} + +const SETTING_FILE: &str = "config.json"; + +/// check config file exists +/// +/// 检查配置文件是否存在 +pub fn check_config_file(base_path: &PathBuf) -> bool { + debug!("check config file at {:?}", base_path); + let config_full_path = base_path.join(SETTING_FILE); + let exists = config_full_path.exists(); + debug!("config file exists: {}", exists); + exists +} + +/// check config file exists, if not, create it +/// +/// 检查配置文件是否存在,如果不存在则创建 +pub fn check_and_create_config_file(base_path: &PathBuf) -> Result<(), Box> { + if check_config_file(base_path) { + return Ok(()); + } + if !base_path.exists() { + std::fs::create_dir_all(&base_path)?; + } + let config_full_path = base_path.join(SETTING_FILE); + if !config_full_path.exists() { + debug!("config file not exists, create it"); + let default_config = json!(AppConfig { + wt_root_path: "".to_string(), + wt_setting_path: "".to_string(), + }); + let default_config_str = serde_json::to_string_pretty(&default_config)?; + std::fs::write(&config_full_path, default_config_str)?; + } + debug!("check config file done"); + Ok(()) +} + +/// get config from file +/// +/// 从文件中获取配置 +pub fn get_config(base_path: &PathBuf) -> Result> { + debug!("get config from file"); + let config_full_path = base_path.join(SETTING_FILE); + let config_str = std::fs::read_to_string(&config_full_path)?; + let config: AppConfig = serde_json::from_str(&config_str)?; + debug!("get config from file done"); + Ok(config) +} + +/// save config to file +/// +/// 保存配置到文件 +pub fn save_config( + base_path: &PathBuf, + config: &AppConfig, +) -> Result<(), Box> { + debug!("save config {:?} to file", config); + let config_full_path = base_path.join(SETTING_FILE); + let config_str = serde_json::to_string_pretty(&config)?; + std::fs::write(&config_full_path, config_str)?; + debug!("save config to file done"); + Ok(()) +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 6632734..42db5e9 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,24 +1,84 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -mod commands; +use std::{fs::File, sync::Mutex}; -// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command +use config::AppConfig; +use log::{info, LevelFilter}; +use serde::{Deserialize, Serialize}; +use simplelog::{ColorChoice, CombinedLogger, Config, TermLogger, TerminalMode, WriteLogger}; +use tauri::Manager; + +mod commands { + pub mod log; + pub mod os; + pub mod setting; + pub mod war_thunder; +} + +mod tools { + pub mod fs; +} + +mod config; +mod ret_code; + +type WrappedState = Mutex>; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct MyState { + config: AppConfig, +} fn main() { tauri::Builder::default() + .setup(|app| { + let app_config_dir = app.path_resolver().app_config_dir().unwrap(); + let app_log_dir = app.path_resolver().app_log_dir().unwrap(); + let log_file = app_log_dir.join("app.log"); + + if !app_log_dir.exists() { + std::fs::create_dir_all(&app_log_dir).unwrap(); + } + + CombinedLogger::init(vec![ + TermLogger::new( + LevelFilter::Debug, + Config::default(), + TerminalMode::Mixed, + ColorChoice::Auto, + ), + WriteLogger::new( + LevelFilter::Info, + Config::default(), + File::create(log_file).unwrap(), + ), + ]) + .unwrap(); + + config::check_and_create_config_file(&app_config_dir).unwrap(); + + app.manage(Mutex::new(Some(MyState { + config: config::get_config(&app_config_dir).unwrap(), + }))); + + Ok(()) + }) .invoke_handler(tauri::generate_handler![ commands::os::show_in_folder, commands::os::delete_folder, - commands::os::create_folder, - commands::manage_wt::auto_detected_wt_install_path, - commands::manage_wt::auto_detected_wt_setting_path, - commands::manage_wt::get_user_skins, - commands::manage_wt::get_user_sights, - commands::manage_wt::install_user_skin, - commands::manage_wt::install_user_sight, - commands::manage_wt::check_is_valid_wt_install_path, + commands::setting::get_app_config, + commands::setting::save_app_config, + commands::setting::get_app_config_dir, + commands::log::get_app_log_dir, + commands::war_thunder::auto_detected_wt_root_path, + commands::war_thunder::auto_detected_wt_setting_path, + commands::war_thunder::install_user_skin, + commands::war_thunder::install_user_sight, + commands::war_thunder::get_user_skins, + commands::war_thunder::get_user_sights, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); + info!("Tauri application started successfully") } diff --git a/src-tauri/src/ret_code.rs b/src-tauri/src/ret_code.rs new file mode 100644 index 0000000..a650018 --- /dev/null +++ b/src-tauri/src/ret_code.rs @@ -0,0 +1,25 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Clone, Copy)] +pub enum RetCode { + Success = 0, + Error = 1, + GetAppSettingsFailed = 10000, + SaveAppSettingsFailed = 10001, + GetAppLogPathFailed = 10002, + AutoDetectedWtRootPathFailed = 20000, + AutoDetectedWtSettingPathFailed = 20001, + InstallUserSkinFailed = 20002, + InstallUserSightFailed = 20003, + GetUserSkinsFailed = 20004, + GetUserSightsFailed = 20005, +} + +impl Serialize for RetCode { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_u32(*self as u32) + } +} diff --git a/src-tauri/src/tools/fs.rs b/src-tauri/src/tools/fs.rs new file mode 100644 index 0000000..b5698dd --- /dev/null +++ b/src-tauri/src/tools/fs.rs @@ -0,0 +1,40 @@ +use std::{fs, path::Path}; + +use fs_extra::dir::{copy, CopyOptions}; +use zip::ZipArchive; + +/// Copy a folder from the source path to the target path. +/// +/// 将源路径的文件夹复制到目标路径。 +pub fn copy_folder(from: &Path, to: &Path) -> Result<(), Box> { + let options = CopyOptions::new().overwrite(true).copy_inside(true); //Initialize default values for CopyOptions + + // copy source/dir1 to target/dir1 + copy(from, to, &options)?; + Ok(()) +} + +/// Decompress a file with extension zip, rar or 7z to the target path. +/// +/// 解压缩扩展名为 zip、rar 或 7z 的文件到目标路径。 +pub fn decompress_file(file_path: &Path, target_path: &Path) -> Result<(), String> { + let extension: &str = file_path.extension().and_then(|e| e.to_str()).unwrap_or(""); + match extension.to_lowercase().as_str() { + "zip" => { + let file = fs::File::open(&file_path) + .map_err(|e| format!("Failed to open zip file: {}", e))?; + let mut archive = + ZipArchive::new(file).map_err(|e| format!("Failed to read zip file: {}", e))?; + archive + .extract(&target_path) + .map_err(|e| format!("Failed to extract zip file: {}", e))?; + } + "rar" => return Err(format!("not support rar file now")), + "7z" => { + sevenz_rust::decompress_file(&file_path, &target_path) + .map_err(|e| format!("Failed to extract 7z file: {}", e))?; + } + _ => return Err(format!("Unsupported file extension: {}", extension)), + } + Ok(()) +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 54d0eca..1fece7c 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -7,7 +7,7 @@ }, "package": { "productName": "wt-helper-app", - "version": "0.0.4" + "version": "0.0.5" }, "tauri": { "allowlist": { @@ -41,7 +41,9 @@ "writeFile": true }, "window": { - "startDragging": true + "startDragging": true, + "show": true, + "create": true } }, "windows": [ @@ -57,6 +59,15 @@ "csp": null }, "bundle": { + "windows": { + "wix": { + "language": ["en-US", "zh-CN"] + }, + "nsis":{ + "displayLanguageSelector": true, + "languages": ["English", "SimpChinese"] + } + }, "active": true, "targets": "all", "identifier": "com.github.axiangcoding.wt-helper-app", diff --git a/src/App.vue b/src/App.vue index 3990057..528128d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,16 +2,48 @@ import { onMounted, ref } from "vue"; import { getVersion } from "@tauri-apps/api/app"; import { open } from "@tauri-apps/api/shell"; -import { getAppSettings } from "./settings"; import BugReportDialog from "./components/dialog/BugReportDialog.vue"; const appVersion = ref(""); const drawer = ref(false); const feedbackDialog = ref(false); +const homeList = [ + { + icon: "mdi-home", + title: "主页", + to: "/", + }, +]; + +const wtTool = [ + { + icon: "mdi-format-paint", + title: "自定义涂装管理", + to: "/wt-skins", + }, + { + icon: "mdi-crosshairs", + title: "自定义瞄具管理", + to: "/wt-sight", + }, +]; + +const appInfo = [ + { + icon: "mdi-cog", + title: "设置", + to: "/setting", + }, + { + icon: "mdi-information", + title: "关于", + to: "/about", + }, +]; + onMounted(async () => { appVersion.value = await getVersion(); - await getAppSettings(); }); async function jumpToBiliBili() { @@ -60,37 +92,26 @@ async function jumpToBiliBili() { activated="wt-skins" color="primary" > - + - 主页 + {{ item.title }} + 战雷小工具 - - - 自定义涂装管理 - - + - 自定义瞄具管理 + {{ item.title }} APP 信息 - - - 设置 - - + - 关于 + {{ item.title }} diff --git a/src/components/card/FunctionCard.vue b/src/components/card/FunctionCard.vue new file mode 100644 index 0000000..36ecf31 --- /dev/null +++ b/src/components/card/FunctionCard.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/src/components/card/LinkCard.vue b/src/components/card/LinkCard.vue new file mode 100644 index 0000000..98fdd14 --- /dev/null +++ b/src/components/card/LinkCard.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/src/components/dialog/BugReportDialog.vue b/src/components/dialog/BugReportDialog.vue index d9a25d8..da821da 100644 --- a/src/components/dialog/BugReportDialog.vue +++ b/src/components/dialog/BugReportDialog.vue @@ -7,13 +7,13 @@ const show = defineModel();