From f588892b0583e9e36c2253fed4d23834ac9dd246 Mon Sep 17 00:00:00 2001 From: akanyan Date: Mon, 14 Apr 2025 19:20:08 +0000 Subject: [PATCH] feat: verbose toggle, info tab, many misc fixes --- CHANGELOG.md | 52 ++++++++++++++++++++++ TODO.md | 4 +- bun.lock | 35 ++++++++++++--- package.json | 3 ++ rust/Cargo.lock | 1 + rust/Cargo.toml | 1 + rust/capabilities/default.json | 3 +- rust/src/appdata.rs | 36 ++++++++++++++++ rust/src/cmd.rs | 23 ++++++++-- rust/src/download_handler.rs | 25 ++++++++--- rust/src/lib.rs | 45 +++---------------- rust/src/model/config.rs | 9 +++- rust/src/model/misc.rs | 6 +-- rust/src/model/profile.rs | 30 ++++++------- rust/src/modules/package.rs | 4 +- rust/src/pkg_store.rs | 48 ++------------------- rust/src/profiles/mod.rs | 2 +- rust/src/util.rs | 19 ++++++++ rust/tauri.conf.json | 2 +- src/components/App.vue | 62 ++++++++++++--------------- src/components/InfoPage.vue | 56 ++++++++++++++++++++++++ src/components/ModList.vue | 10 +++-- src/components/ModListEntry.vue | 10 +++-- src/components/ModStore.vue | 44 ++++++++++++++++++- src/components/ProfileList.vue | 1 - src/components/ProfileListEntry.vue | 14 ++++-- src/components/StartButton.vue | 2 +- src/components/options/Segatools.vue | 1 + src/components/options/Startliner.vue | 17 +++++++- src/stores.ts | 31 +++++++++++--- 30 files changed, 410 insertions(+), 186 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 src/components/InfoPage.vue diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..162d2b1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,52 @@ +## 0.7.0 + +- Hopefully fixed issues with the download button +- Added a verbose logging option +- Added an info tab +- Instead of auto-installing segatools & mempatcher at launch, the package store now shows a "install recommended" button + +## 0.6.1 + +- Added support for O.N.G.E.K.I. English Translation +- Disabled the icon buttons as they broke at some point + +## 0.6.0 + +- Chunithm: added support for DLLs (saekawa, mempatcher) +- Chunithm: added a patch interface +- Chunithm: added display settings +- Chunithm: removed split IR +- Both games: added hardware aime reader support +- Added an update progress bar + +## 0.5.0 + +- Added a keyboard configuration UI (for both games) +- Added a prompt after selecting `mu3.exe`/`chusanApp.exe` in the file picker, allowing you to copy much of the data from `segatools.ini`, if it already exists. +- Added an option to not switch the primary monitor. + - This is not recommended to use but it may help when the primary monitor switcher doesn't work correctly. + +## 0.4.0 + +- New error dialog. +- Added tooltips for IO, Aime, Hook. +- Added a welcome message. +- Added a separate auto-update toggle. +- Fixed a display bug with the offline mode toggle. + +## 0.3.0 + +- Added UI scaling, offline mode, and an 'update all' button +- First public release + +## 0.2.0 + +- Added a context menu for the start button with additional launch options +- Added an audio mode button for ongeki +- Fixed the launcher freezing while the game is running +- Probably added auto-updates + +## 0.1.0 + +⚠️ this release is incomplete and potentially cursed +it's more of a preview of a preview diff --git a/TODO.md b/TODO.md index 813c713..81e3092 100644 --- a/TODO.md +++ b/TODO.md @@ -1,11 +1,9 @@ ### Short-term -- CHUNITHM support - https://gitea.tendokyu.moe/TeamTofuShop/segatools/issues/63 ### Long-term -- Auto-updates - Progress bars and other GUI sugar -- IO DLLs and artemis as special packages +- artemis as a special package - Other arcade games (if there is demand) diff --git a/bun.lock b/bun.lock index 5f506ce..4db82d1 100644 --- a/bun.lock +++ b/bun.lock @@ -4,10 +4,11 @@ "": { "name": "startliner", "dependencies": { + "@f3ve/vue-markdown-it": "^0.2.3", "@mdi/font": "7.4.47", "@primevue/forms": "^4.3.3", "@primevue/themes": "^4.3.3", - "@tailwindcss/vite": "^4.1.2", + "@tailwindcss/vite": "^4.1.3", "@tauri-apps/api": "^2.4.1", "@tauri-apps/plugin-cli": "^2.2.0", "@tauri-apps/plugin-deep-link": "~2.2.1", @@ -17,29 +18,31 @@ "@tauri-apps/plugin-shell": "~2.2.1", "@tauri-apps/plugin-updater": "^2.7.0", "@trivago/prettier-plugin-sort-imports": "^5.2.2", - "pinia": "^3.0.1", + "@types/markdown-it": "^14.1.2", + "markdown-it": "^14.1.0", + "pinia": "^3.0.2", "primeicons": "^7.0.0", "primevue": "^4.3.3", "roboto-fontface": "^0.10.0", - "tailwindcss": "^4.1.2", + "tailwindcss": "^4.1.3", "tailwindcss-primeui": "^0.4.0", "vue": "^3.5.13", - "vuetify": "^3.8.0", + "vuetify": "^3.8.1", }, "devDependencies": { "@tauri-apps/cli": "^2.4.1", "@tsconfig/node22": "^22.0.1", - "@types/node": "^22.14.0", + "@types/node": "^22.14.1", "@vitejs/plugin-vue": "^5.2.3", "@vue/eslint-config-typescript": "^14.5.0", "@vue/tsconfig": "^0.5.1", "npm-run-all2": "^7.0.2", "sass": "1.77.8", "sass-embedded": "^1.86.3", - "typescript": "^5.8.2", + "typescript": "^5.8.3", "unplugin-fonts": "^1.3.1", "unplugin-vue-components": "^0.27.5", - "vite": "^6.2.5", + "vite": "^6.2.6", "vite-plugin-vuetify": "^2.1.1", "vue-tsc": "^2.2.8", }, @@ -132,6 +135,8 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.5", "", { "dependencies": { "@eslint/core": "^0.10.0", "levn": "^0.4.1" } }, "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A=="], + "@f3ve/vue-markdown-it": ["@f3ve/vue-markdown-it@0.2.3", "", { "dependencies": { "markdown-it": "^14.1.0" }, "peerDependencies": { "vue": "^3.3.4" } }, "sha512-v0VNd7wb55kwsUUy3n6DLI9+0FYSG0PrCTD3bWuSRo6WS3OHD5wghh/aHzebVdsVkSBXfVpiEUlMA3DrxLs7Lw=="], + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], @@ -292,6 +297,12 @@ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="], + + "@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="], + + "@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="], + "@types/node": ["@types/node@22.14.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.29.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.29.0", "@typescript-eslint/type-utils": "8.29.0", "@typescript-eslint/utils": "8.29.0", "@typescript-eslint/visitor-keys": "8.29.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ=="], @@ -540,6 +551,8 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.29.2", "", { "os": "win32", "cpu": "x64" }, "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA=="], + "linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="], + "local-pkg": ["local-pkg@0.5.1", "", { "dependencies": { "mlly": "^1.7.3", "pkg-types": "^1.2.1" } }, "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], @@ -550,6 +563,10 @@ "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], + "markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="], + + "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="], + "memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], @@ -620,6 +637,8 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "read-package-json-fast": ["read-package-json-fast@4.0.0", "", { "dependencies": { "json-parse-even-better-errors": "^4.0.0", "npm-normalize-package-bin": "^4.0.0" } }, "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg=="], @@ -726,6 +745,8 @@ "typescript-eslint": ["typescript-eslint@8.29.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.29.0", "@typescript-eslint/parser": "8.29.0", "@typescript-eslint/utils": "8.29.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-ep9rVd9B4kQsZ7ZnWCVxUE/xDLUUUsRzE0poAeNu+4CkFErLfuvPt/qtm2EpnSyfvsR0S6QzDFSrPCFBwf64fg=="], + "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], + "ufo": ["ufo@1.5.4", "", {}, "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], diff --git a/package.json b/package.json index b9c6fcc..f3e5e88 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "tauri": "tauri" }, "dependencies": { + "@f3ve/vue-markdown-it": "^0.2.3", "@mdi/font": "7.4.47", "@primevue/forms": "^4.3.3", "@primevue/themes": "^4.3.3", @@ -23,6 +24,8 @@ "@tauri-apps/plugin-shell": "~2.2.1", "@tauri-apps/plugin-updater": "^2.7.0", "@trivago/prettier-plugin-sort-imports": "^5.2.2", + "@types/markdown-it": "^14.1.2", + "markdown-it": "^14.1.0", "pinia": "^3.0.2", "primeicons": "^7.0.0", "primevue": "^4.3.3", diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 08115f5..686f42f 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -4730,6 +4730,7 @@ dependencies = [ "humantime", "junction", "log", + "open", "regex", "reqwest", "rust-ini", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 0f50f33..b6f366b 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -45,6 +45,7 @@ sha256 = "1.6.0" serialport = "4.7.1" fern = { version ="0.7.1", features = ["colored"] } humantime = "2.2.0" +open = "5.3.2" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-cli = "2" diff --git a/rust/capabilities/default.json b/rust/capabilities/default.json index f1ba03e..fe326db 100644 --- a/rust/capabilities/default.json +++ b/rust/capabilities/default.json @@ -23,6 +23,7 @@ "fs:allow-data-read-recursive", "fs:allow-data-write-recursive", "fs:allow-config-read-recursive", - "fs:allow-config-write-recursive" + "fs:allow-config-write-recursive", + "shell:allow-open" ] } diff --git a/rust/src/appdata.rs b/rust/src/appdata.rs index 8d71dd3..2a5f91d 100644 --- a/rust/src/appdata.rs +++ b/rust/src/appdata.rs @@ -1,4 +1,5 @@ use std::hash::{DefaultHasher, Hash, Hasher}; +use std::time::SystemTime; use crate::model::config::GlobalConfig; use crate::model::patch::PatchFileVec; use crate::pkg::{Feature, Status}; @@ -7,6 +8,7 @@ use crate::{model::misc::Game, pkg::PkgKey}; use crate::pkg_store::PackageStore; use crate::util; use anyhow::{anyhow, Result}; +use fern::colors::{Color, ColoredLevelConfig}; use tauri::AppHandle; pub struct GlobalState { @@ -34,6 +36,8 @@ impl AppData { .and_then(|s| Ok(serde_json::from_str::(&s)?)) .unwrap_or_default(); + Self::init_logger(&cfg); + let profile = match cfg.recent_profile { Some((game, ref name)) => Profile::load(game, name.clone()).ok(), None => None @@ -127,4 +131,36 @@ impl AppData { p.fix(&self.pkgs); } } + + fn init_logger(cfg: &GlobalConfig) { + let mut fern_builder; + let colors = ColoredLevelConfig::new() + .debug(Color::Green) + .info(Color::Blue) + .warn(Color::Yellow) + .error(Color::Red); + + fern_builder = fern::Dispatch::new() + .format(move |out, message, record| { + out.finish(format_args!( + "[{} {} {}] {}", + humantime::format_rfc3339_seconds(SystemTime::now()), + colors.color(record.level()), + record.target(), + message + )) + }) + .chain(std::io::stdout()) + .chain(fern::log_file(util::data_dir().join("log.txt")).expect("unable to initialize the logger")); + + if cfg.verbose == true { + fern_builder = fern_builder.level(log::LevelFilter::Debug); + } else { + fern_builder = fern_builder.level(log::LevelFilter::Info); + } + + if let Err(e) = fern_builder.apply() { + panic!("unable to initialize the logger? {:?}", e); + } + } } diff --git a/rust/src/cmd.rs b/rust/src/cmd.rs index 0c09e9b..707495e 100644 --- a/rust/src/cmd.rs +++ b/rust/src/cmd.rs @@ -323,9 +323,10 @@ pub async fn duplicate_profile(profile: ProfileMeta) -> Result<(), String> { pub async fn delete_profile(state: State<'_, Mutex>, profile: ProfileMeta) -> Result<(), String> { log::debug!("invoke: delete_profile({:?})", profile); - std::fs::remove_dir_all(profile.config_dir()) + util::remove_dir_all(profile.config_dir()) + .await .map_err(|e| format!("Unable to delete {:?}: {}", profile.config_dir(), e))?; - if let Err(e) = std::fs::remove_dir_all(profile.data_dir()) { + if let Err(e) = util::remove_dir_all(profile.data_dir()).await { log::warn!("Unable to delete: {:?} {}", profile.data_dir(), e); } @@ -414,7 +415,8 @@ pub async fn get_global_config(state: State<'_, Mutex>, field: GlobalCo let appd = state.lock().await; match field { GlobalConfigField::OfflineMode => Ok(appd.cfg.offline_mode), - GlobalConfigField::EnableAutoupdates => Ok(appd.cfg.enable_autoupdates) + GlobalConfigField::EnableAutoupdates => Ok(appd.cfg.enable_autoupdates), + GlobalConfigField::Verbose => Ok(appd.cfg.verbose) } } @@ -425,7 +427,8 @@ pub async fn set_global_config(state: State<'_, Mutex>, field: GlobalCo let mut appd = state.lock().await; match field { GlobalConfigField::OfflineMode => appd.cfg.offline_mode = value, - GlobalConfigField::EnableAutoupdates => appd.cfg.enable_autoupdates = value + GlobalConfigField::EnableAutoupdates => appd.cfg.enable_autoupdates = value, + GlobalConfigField::Verbose => appd.cfg.verbose = value, }; appd.write().map_err(|e| e.to_string()) } @@ -472,6 +475,18 @@ pub async fn file_exists(path: String) -> Result { Ok(std::fs::exists(path).unwrap_or(false)) } +// Easier than trying to get the barely-documented tauri permissions system to work +#[tauri::command] +pub async fn open_file(path: String) -> Result<(), String> { + open::that(path).map_err(|e| e.to_string())?; + Ok(()) +} + +#[tauri::command] +pub fn get_changelog() -> Result { + Ok(include_str!("../../CHANGELOG.md").to_owned()) +} + #[tauri::command] pub async fn list_com_ports() -> Result, String> { let ports = serialport::available_ports().unwrap_or(Vec::new()); diff --git a/rust/src/download_handler.rs b/rust/src/download_handler.rs index 524fe7e..d404899 100644 --- a/rust/src/download_handler.rs +++ b/rust/src/download_handler.rs @@ -1,4 +1,6 @@ use std::{collections::HashSet, path::PathBuf}; +use futures::Stream; +use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Emitter}; use tokio::fs::File; use anyhow::{anyhow, Result}; @@ -6,14 +8,20 @@ use anyhow::{anyhow, Result}; use crate::pkg::{Package, PkgKey, Remote}; pub struct DownloadHandler { - set: HashSet, + paths: HashSet, app: AppHandle } +#[derive(Serialize, Deserialize, Clone)] +pub struct DownloadTick { + pkg_key: PkgKey, + ratio: f32 +} + impl DownloadHandler { pub fn new(app: AppHandle) -> DownloadHandler { DownloadHandler { - set: HashSet::new(), + paths: HashSet::new(), app } } @@ -22,11 +30,11 @@ impl DownloadHandler { let rmt = pkg.rmt.as_ref() .ok_or_else(|| anyhow!("Attempted to download a package without remote data"))? .clone(); - if self.set.contains(zip_path.to_string_lossy().as_ref()) { - // Todo when there is a clear cache button, it should clear the set - Err(anyhow!("Already downloading")) + if self.paths.contains(zip_path) { + Ok(()) } else { - self.set.insert(zip_path.to_string_lossy().to_string()); + // TODO clear cache button should clear this + self.paths.insert(zip_path.clone()); tauri::async_runtime::spawn(Self::download_zip_proc(self.app.clone(), zip_path.clone(), pkg.key(), rmt)); Ok(()) } @@ -42,10 +50,15 @@ impl DownloadHandler { let mut cache_file_w = File::create(&zip_path_part).await?; let mut byte_stream = reqwest::get(&rmt.download_url).await?.bytes_stream(); + let first_hint = byte_stream.size_hint().0 as f32; log::info!("downloading: {}", rmt.download_url); while let Some(item) = byte_stream.next().await { let i = item?; + app.emit("download-tick", DownloadTick { + pkg_key: pkg_key.clone(), + ratio: 1.0f32 - (byte_stream.size_hint().0 as f32) / first_hint + })?; cache_file_w.write_all(&mut i.as_ref()).await?; } cache_file_w.sync_all().await?; diff --git a/rust/src/lib.rs b/rust/src/lib.rs index c309f2c..0f16002 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -9,11 +9,10 @@ mod modules; mod profiles; mod patcher; -use std::{sync::OnceLock, time::SystemTime}; +use std::sync::OnceLock; use anyhow::anyhow; use closure::closure; use appdata::{AppData, ToggleAction}; -use fern::colors::{Color, ColoredLevelConfig}; use model::misc::Game; use pkg::PkgKey; use pkg_store::Payload; @@ -48,42 +47,7 @@ pub async fn run(_args: Vec) { util::init_dirs(&apph); - let mut fern_builder; - { - let colors = ColoredLevelConfig::new() - .debug(Color::Green) - .info(Color::Blue) - .warn(Color::Yellow) - .error(Color::Red); - - fern_builder = fern::Dispatch::new() - .format(move |out, message, record| { - out.finish(format_args!( - "[{} {} {}] {}", - humantime::format_rfc3339_seconds(SystemTime::now()), - colors.color(record.level()), - record.target(), - message - )) - }) - .chain(std::io::stdout()) - .chain(fern::log_file(util::data_dir().join("log.txt")).expect("unable to initialize the logger")); - } - - #[cfg(debug_assertions)] - { - fern_builder = fern_builder.level(log::LevelFilter::Debug); - } - #[cfg(not(debug_assertions))] - { - if std::env::var("DEBUG_LOG").is_ok() { - fern_builder = fern_builder.level(log::LevelFilter::Debug); - } else { - fern_builder = fern_builder.level(log::LevelFilter::Info); - } - } - - fern_builder.apply()?; + let mut app_data = AppData::new(app.handle().clone()); log::info!( "running from {}", @@ -93,7 +57,6 @@ pub async fn run(_args: Vec) { .unwrap_or_default() ); - let mut app_data = AppData::new(app.handle().clone()); let start_immediately; if let Ok(matches) = app.cli().matches() { @@ -244,6 +207,8 @@ pub async fn run(_args: Vec) { cmd::list_platform_capabilities, cmd::list_directories, cmd::file_exists, + cmd::open_file, + cmd::get_changelog, cmd::list_com_ports, @@ -326,7 +291,7 @@ async fn update(app: tauri::AppHandle) -> tauri_plugin_updater::Result<()> { update.download_and_install( |chunk_length, content_length| { downloaded += chunk_length; - _ = app.emit("update-progress", (chunk_length as f64) / (content_length.unwrap_or(u64::MAX) as f64)); + _ = app.emit("update-progress", (downloaded as f64) / (content_length.unwrap_or(u64::MAX) as f64)); }, || { log::info!("download finished"); diff --git a/rust/src/model/config.rs b/rust/src/model/config.rs index cdca01e..66cf6f7 100644 --- a/rust/src/model/config.rs +++ b/rust/src/model/config.rs @@ -2,10 +2,12 @@ use serde::{Deserialize, Serialize}; use super::misc::Game; #[derive(Serialize, Deserialize, Clone)] +#[serde(default)] pub struct GlobalConfig { pub recent_profile: Option<(Game, String)>, pub offline_mode: bool, pub enable_autoupdates: bool, + pub verbose: bool, } impl Default for GlobalConfig { @@ -13,13 +15,16 @@ impl Default for GlobalConfig { Self { recent_profile: Default::default(), offline_mode: false, - enable_autoupdates: true + enable_autoupdates: true, + verbose: false, } } } #[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "snake_case")] pub enum GlobalConfigField { OfflineMode, - EnableAutoupdates + EnableAutoupdates, + Verbose } \ No newline at end of file diff --git a/rust/src/model/misc.rs b/rust/src/model/misc.rs index cf9f300..92f619c 100644 --- a/rust/src/model/misc.rs +++ b/rust/src/model/misc.rs @@ -5,10 +5,9 @@ use crate::pkg::PkgKey; use super::profile::ProfileModule; #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Copy)] +#[serde(rename_all = "snake_case")] pub enum Game { - #[serde(rename = "ongeki")] Ongeki, - #[serde(rename = "chunithm")] Chunithm, } @@ -89,10 +88,9 @@ pub enum StartCheckError { } #[derive(Default, Serialize, Deserialize, Clone)] +#[serde(default)] pub struct ConfigHook { - #[serde(skip_serializing_if = "Option::is_none")] pub allnet_auth: Option, - #[serde(skip_serializing_if = "Option::is_none")] pub aime: Option, } diff --git a/rust/src/model/profile.rs b/rust/src/model/profile.rs index 5a34aca..14e1ab6 100644 --- a/rust/src/model/profile.rs +++ b/rust/src/model/profile.rs @@ -14,6 +14,7 @@ pub enum Aime { } #[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(default)] pub struct AMNet { pub name: String, pub addr: String, @@ -26,18 +27,17 @@ impl Default for AMNet { } } -#[derive(Deserialize, Serialize, Clone, Debug)] +#[derive(Deserialize, Serialize, Clone, Debug, Default )] +#[serde(default)] pub struct Segatools { pub target: PathBuf, pub hook: Option, pub io: Option, - #[serde(default)] pub aime: Aime, pub amfs: PathBuf, pub option: PathBuf, pub appdata: PathBuf, pub intel: bool, - #[serde(default)] pub amnet: AMNet, pub aime_port: Option, } @@ -69,7 +69,8 @@ pub enum DisplayMode { Fullscreen } -#[derive(Deserialize, Serialize, Clone, Debug)] +#[derive(Deserialize, Serialize, Clone, Debug, Default)] +#[serde(default)] pub struct Display { pub target: String, pub rez: (i32, i32), @@ -77,11 +78,7 @@ pub struct Display { pub rotation: Option, pub frequency: i32, pub borderless_fullscreen: bool, - - #[serde(default)] pub dont_switch_primary: bool, - - #[serde(skip_serializing_if = "Option::is_none")] pub monitor_index_override: Option, } @@ -113,6 +110,7 @@ pub enum NetworkType { } #[derive(Deserialize, Serialize, Clone, Default, Debug)] +#[serde(default)] pub struct Network { pub network_type: NetworkType, @@ -127,11 +125,13 @@ pub struct Network { } #[derive(Deserialize, Serialize, Clone, Default, Debug)] +#[serde(default)] pub struct BepInEx { pub console: bool, } #[derive(Deserialize, Serialize, Clone)] +#[serde(default)] pub struct Wine { pub runtime: PathBuf, pub prefix: PathBuf, @@ -155,20 +155,17 @@ pub enum Mu3Audio { Excl2Ch, } -#[derive(Deserialize, Serialize, Clone, Debug)] +#[derive(Deserialize, Serialize, Clone, Debug, Default)] +#[serde(default)] pub struct Mu3Ini { - #[serde(skip_serializing_if = "Option::is_none")] pub audio: Option, - - #[serde(skip_serializing_if = "Option::is_none")] pub blacklist: Option<(i32, i32)>, } -fn default_true() -> bool { true } - #[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(default)] pub struct OngekiKeyboard { - #[serde(default = "default_true")] pub enabled: bool, + pub enabled: bool, pub use_mouse: bool, pub coin: i32, pub svc: i32, @@ -208,8 +205,9 @@ impl Default for OngekiKeyboard { } #[derive(Deserialize, Serialize, Clone, Debug)] +#[serde(default)] pub struct ChunithmKeyboard { - #[serde(default = "default_true")] pub enabled: bool, + pub enabled: bool, pub coin: i32, pub svc: i32, pub test: i32, diff --git a/rust/src/modules/package.rs b/rust/src/modules/package.rs index 8a66205..0a3c4b1 100644 --- a/rust/src/modules/package.rs +++ b/rust/src/modules/package.rs @@ -14,10 +14,10 @@ pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet, name: &str) -> Result<()> { - let path = path.as_ref().join(name); - if path.exists() { - tokio::fs::remove_dir_all(path) - .await - .map_err(|e| anyhow!("could not delete {}: {}", name, e))?; - } - - Ok(()) - } - - async fn clean_up_file(path: impl AsRef, name: &str, force: bool) -> Result<()> { - let path = path.as_ref().join(name); - if force || path.exists() { - tokio::fs::remove_file(path).await - .map_err(|e| anyhow!("Could not delete /{}: {}", name, e))?; - } - - Ok(()) - } - - async fn clean_up_package(path: impl AsRef) -> Result<()> { - // todo case sensitivity for linux - Self::clean_up_dir(&path, "app").await?; - Self::clean_up_dir(&path, "option").await?; - Self::clean_up_dir(&path, "segatools").await?; - Self::clean_up_file(&path, "icon.png", true).await?; - Self::clean_up_file(&path, "manifest.json", true).await?; - Self::clean_up_file(&path, "README.md", true).await?; - Self::clean_up_file(&path, "post_load.ps1", false).await?; - // todo search for the proper dll - Self::clean_up_file(&path, "saekawa.dll", false).await?; - Self::clean_up_file(&path, "mempatcher32.dll", false).await?; - Self::clean_up_file(&path, "mempatcher64.dll", false).await?; - - tokio::fs::remove_dir(path.as_ref()) - .await - .map_err(|e| anyhow!("Could not delete {}: {}", path.as_ref().to_string_lossy(), e))?; - - Ok(()) - } - fn resolve_deps(&self, rmt: Remote, set: &mut HashSet) -> Result<()> { for d in rmt.dependencies { set.insert(d.clone()); diff --git a/rust/src/profiles/mod.rs b/rust/src/profiles/mod.rs index b9c955d..89028d3 100644 --- a/rust/src/profiles/mod.rs +++ b/rust/src/profiles/mod.rs @@ -94,7 +94,7 @@ impl Profile { } std::fs::write(&path, s) .map_err(|e| anyhow!("error when writing to {:?}: {}", path, e))?; - log::info!("profile written to {:?}", path); + log::info!("profile saved to {:?}", path); Ok(()) } diff --git a/rust/src/util.rs b/rust/src/util.rs index 00147be..c459fb7 100644 --- a/rust/src/util.rs +++ b/rust/src/util.rs @@ -154,4 +154,23 @@ impl PathStr for PathBuf { pub fn bool_to_01(val: bool) -> &'static str { return if val { "1" } else { "0" } +} + +// rm -r with checks +pub async fn remove_dir_all(path: impl AsRef) -> Result<()> { + let canon = path.as_ref().canonicalize()?; + + if canon.to_string_lossy().len() < 10 { + return Err(anyhow!("invalid remove_dir_all target: too short")); + } + + if canon.starts_with(data_dir().canonicalize()?) + || canon.starts_with(config_dir().canonicalize()?) + || canon.starts_with(cache_dir().canonicalize()?) { + tokio::fs::remove_dir_all(path).await + .map_err(|e| anyhow!("invalid remove_dir_all target: {:?}", e))?; + Ok(()) + } else { + Err(anyhow!("invalid remove_dir_all target: not in a data directory")) + } } \ No newline at end of file diff --git a/rust/tauri.conf.json b/rust/tauri.conf.json index 30793e9..3fd5489 100644 --- a/rust/tauri.conf.json +++ b/rust/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "STARTLINER", - "version": "0.6.1", + "version": "0.7.0", "identifier": "zip.patafour.startliner", "build": { "beforeDevCommand": "bun run dev", diff --git a/src/components/App.vue b/src/components/App.vue index a458e51..521377d 100644 --- a/src/components/App.vue +++ b/src/components/App.vue @@ -13,6 +13,7 @@ import TabPanel from 'primevue/tabpanel'; import TabPanels from 'primevue/tabpanels'; import Tabs from 'primevue/tabs'; import { listen } from '@tauri-apps/api/event'; +import InfoPage from './InfoPage.vue'; import ModList from './ModList.vue'; import ModStore from './ModStore.vue'; import OptionList from './OptionList.vue'; @@ -36,7 +37,8 @@ const client = useClientStore(); pkg.setupListeners(); -const currentTab: Ref = ref(3); +const currentTab: Ref<'users' | 'loc' | 'patches' | 'rmt' | 'cfg' | 'info'> = + ref('users'); const pkgSearchTerm = ref(''); const isProfileDisabled = computed(() => prf.current === null); @@ -62,20 +64,11 @@ onMounted(async () => { await Promise.all([prf.reloadList(), prf.reload()]); if (prf.current !== null) { - currentTab.value = 0; + currentTab.value = 'loc'; await pkg.reloadAll(); } - fetch_promise.then(async () => { - await invoke('install_package', { - key: 'segatools-mu3hook', - force: false, - }); - await invoke('install_package', { - key: 'segatools-chusanhook', - force: false, - }); - }); + await fetch_promise; }); const errorVisible = ref(false); @@ -149,42 +142,47 @@ listen<{ message: string; header: string }>('invoke-error', (event) => { :value="currentTab" v-on:update:value=" (value) => { - currentTab = value; + currentTab = value as any; } " class="h-screen" >
-
-
+
-
-
+
('invoke-error', (event) => {
- + - + - + - + -


-
-
- + + +
-
+
+import { ref } from 'vue'; +import Button from 'primevue/button'; +import ScrollPanel from 'primevue/scrollpanel'; +import { invoke } from '../invoke'; +import { VueMarkdownIt } from '@f3ve/vue-markdown-it'; + +const changelog = ref(''); + +invoke('get_changelog').then((s) => (changelog.value = s as string)); + + + + + diff --git a/src/components/ModList.vue b/src/components/ModList.vue index 5fa668b..6debdf2 100644 --- a/src/components/ModList.vue +++ b/src/components/ModList.vue @@ -15,7 +15,8 @@ const props = defineProps({ const pkgs = usePkgStore(); const prf = usePrfStore(); -const empty = ref(true); +const groupCallIndex = ref(0); +const empty = ref(false); const gameSublist: Ref = ref([]); invoke('get_game_packages', { @@ -45,7 +46,10 @@ const group = computed(() => { ({ namespace }) => namespace ) ); - empty.value = Object.keys(res).length === 0; + if (groupCallIndex.value > 0) { + empty.value = Object.keys(res).length === 0; + } + groupCallIndex.value += 1; return res; }); @@ -81,5 +85,5 @@ const missing = computed(() => {
-
+
diff --git a/src/components/ModListEntry.vue b/src/components/ModListEntry.vue index bf0f789..285a9bb 100644 --- a/src/components/ModListEntry.vue +++ b/src/components/ModListEntry.vue @@ -1,10 +1,12 @@ diff --git a/src/components/StartButton.vue b/src/components/StartButton.vue index 112d699..0a74a3e 100644 --- a/src/components/StartButton.vue +++ b/src/components/StartButton.vue @@ -26,7 +26,7 @@ const startline = async (force: boolean, refresh: boolean) => { } else if ('MissingLocalPackage' in o) { return `Package missing: ${o.MissingLocalPackage}`; } else if ('MissingDependency' in o) { - return `Dependency missing: ${o.MissingDependency}`; + return `Dependency missing: ${(o.MissingDependency as string[]).join(' ')}`; } else if ('MissingTool' in o) { return `Tool missing: ${o.MissingTool}`; } else { diff --git a/src/components/options/Segatools.vue b/src/components/options/Segatools.vue index 01a9164..96186d0 100644 --- a/src/components/options/Segatools.vue +++ b/src/components/options/Segatools.vue @@ -119,6 +119,7 @@ const checkSegatoolsIni = async (target: string) => { return { title: pkgKey(p), value: pkgKey(p) }; }) " + placeholder="none" option-label="title" option-value="value" > diff --git a/src/components/options/Startliner.vue b/src/components/options/Startliner.vue index e75af47..a38f5d5 100644 --- a/src/components/options/Startliner.vue +++ b/src/components/options/Startliner.vue @@ -25,6 +25,15 @@ const updatesModel = computed({ await client.setAutoupdates(value); }, }); + +const verboseModel = computed({ + get() { + return client.verbose; + }, + async set(value: boolean) { + await client.setVerbose(value); + }, +}); diff --git a/src/stores.ts b/src/stores.ts index 8af739d..03c26aa 100644 --- a/src/stores.ts +++ b/src/stores.ts @@ -193,8 +193,17 @@ export const usePkgStore = defineStore('pkg', { pkg.js.busy = false; } } + }, - //if (rv === 'Deferred') { /* download progress */ } + async installFromKey(key: string) { + try { + await invoke('install_package', { + key, + force: true, + }); + } catch (err) { + console.error(err); + } }, async updateAll() { @@ -346,6 +355,7 @@ export const useClientStore = defineStore('client', () => { const offlineMode = ref(false); const enableAutoupdates = ref(true); + const verbose = ref(false); const scaleValue = (value: ScaleType) => value === 's' ? 1 : value === 'm' ? 1.25 : value === 'l' ? 1.5 : 2; @@ -401,11 +411,15 @@ export const useClientStore = defineStore('client', () => { } offlineMode.value = await invoke('get_global_config', { - field: 'OfflineMode', + field: 'offline_mode', }); enableAutoupdates.value = await invoke('get_global_config', { - field: 'EnableAutoupdates', + field: 'enable_autoupdates', + }); + + verbose.value = await invoke('get_global_config', { + field: 'verbose', }); }; @@ -438,17 +452,22 @@ export const useClientStore = defineStore('client', () => { const setOfflineMode = async (value: boolean) => { offlineMode.value = value; - await invoke('set_global_config', { field: 'OfflineMode', value }); + await invoke('set_global_config', { field: 'offline_mode', value }); }; const setAutoupdates = async (value: boolean) => { enableAutoupdates.value = value; await invoke('set_global_config', { - field: 'EnableAutoupdates', + field: 'enable_autoupdates', value, }); }; + const setVerbose = async (value: boolean) => { + verbose.value = value; + await invoke('set_global_config', { field: 'verbose', value }); + }; + getCurrentWindow().onResized(async ({ payload }) => { // For whatever reason this is 0 when minimized if (payload.width > 0) { @@ -460,6 +479,7 @@ export const useClientStore = defineStore('client', () => { scaleFactor, offlineMode, enableAutoupdates, + verbose, timeout, scaleModel, load, @@ -467,5 +487,6 @@ export const useClientStore = defineStore('client', () => { queueSave, setOfflineMode, setAutoupdates, + setVerbose, }; });