feat: verbose toggle, info tab, many misc fixes

This commit is contained in:
2025-04-14 19:20:08 +00:00
parent 37df371006
commit f588892b05
30 changed files with 410 additions and 186 deletions

52
CHANGELOG.md Normal file
View File

@ -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

View File

@ -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)

View File

@ -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=="],

View File

@ -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",

1
rust/Cargo.lock generated
View File

@ -4730,6 +4730,7 @@ dependencies = [
"humantime",
"junction",
"log",
"open",
"regex",
"reqwest",
"rust-ini",

View File

@ -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"

View File

@ -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"
]
}

View File

@ -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::<GlobalConfig>(&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);
}
}
}

View File

@ -323,9 +323,10 @@ pub async fn duplicate_profile(profile: ProfileMeta) -> Result<(), String> {
pub async fn delete_profile(state: State<'_, Mutex<AppData>>, 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<AppData>>, 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<AppData>>, 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<bool, ()> {
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<String, ()> {
Ok(include_str!("../../CHANGELOG.md").to_owned())
}
#[tauri::command]
pub async fn list_com_ports() -> Result<BTreeMap<String, i32>, String> {
let ports = serialport::available_ports().unwrap_or(Vec::new());

View File

@ -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<String>,
paths: HashSet<PathBuf>,
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?;

View File

@ -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<String>) {
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<String>) {
.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<String>) {
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");

View File

@ -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
}

View File

@ -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<ConfigHookAuth>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aime: Option<ConfigHookAime>,
}

View File

@ -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<PkgKey>,
pub io: Option<PkgKey>,
#[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<i32>,
}
@ -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<i32>,
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<i32>,
}
@ -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<Mu3Audio>,
#[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,

View File

@ -14,10 +14,10 @@ pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgK
if redo_bepinex {
if pfx_dir.join("BepInEx").exists() {
tokio::fs::remove_dir_all(pfx_dir.join("BepInEx")).await?;
util::remove_dir_all(pfx_dir.join("BepInEx")).await?;
}
if pfx_dir.join("lang").exists() {
tokio::fs::remove_dir_all(pfx_dir.join("lang")).await?;
util::remove_dir_all(pfx_dir.join("lang")).await?;
}
}

View File

@ -1,5 +1,4 @@
use std::collections::{HashMap, HashSet};
use std::path::Path;
use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter};
@ -207,8 +206,9 @@ impl PackageStore {
"{}-{}-{}.zip",
pkg.namespace, pkg.name, rmt.version
));
let part_path = zip_path.join(".part");
if !zip_path.exists() {
if !zip_path.exists() && !part_path.exists() {
self.dlh.download_zip(&zip_path, &pkg)?;
log::debug!("deferring {}", key);
return Ok(InstallResult::Deferred);
@ -243,7 +243,7 @@ impl PackageStore {
if path.exists() && path.join("manifest.json").exists() {
pkg.loc = None;
let rv = Self::clean_up_package(&path).await;
let rv = util::remove_dir_all(&path).await;
if rv.is_ok() {
self.app.emit("install-end-prelude", Payload {
@ -269,48 +269,6 @@ impl PackageStore {
self.store.insert(key, new);
}
async fn clean_up_dir(path: impl AsRef<Path>, 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<Path>, 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<Path>) -> 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<PkgKey>) -> Result<()> {
for d in rmt.dependencies {
set.insert(d.clone());

View File

@ -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(())
}

View File

@ -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<Path>) -> 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"))
}
}

View File

@ -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",

View File

@ -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<string | number> = 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"
>
<div class="fixed w-full flex z-100">
<TabList class="grow" :show-navigators="false">
<Tab :value="3"><div class="pi pi-users"></div></Tab>
<Tab :disabled="isProfileDisabled" :value="0"
<Tab value="users"><div class="pi pi-users"></div></Tab>
<Tab :disabled="isProfileDisabled" value="loc"
><div class="pi pi-box"></div
></Tab>
<Tab v-if="prf.current?.meta.game === 'chunithm'" :value="4"
<Tab
v-if="prf.current?.meta.game === 'chunithm'"
value="patches"
><div class="pi pi-ticket"></div
></Tab>
<Tab
v-if="pkg.networkStatus === 'online'"
:disabled="isProfileDisabled"
:value="1"
value="rmt"
><div class="pi pi-download"></div
></Tab>
<Tab :disabled="isProfileDisabled" :value="2"
<Tab :disabled="isProfileDisabled" value="cfg"
><div class="pi pi-cog"></div
></Tab>
<Tab value="info"
><div class="pi pi-info-circle"></div
></Tab>
<div class="grow"></div>
<div class="flex gap-4">
<div
class="flex"
v-if="[0, 1, 2].includes(currentTab as number)"
v-if="['loc', 'rmt', 'cfg'].includes(currentTab)"
>
<InputIcon class="self-center mr-2">
<i class="pi pi-search" />
</InputIcon>
<InputText
v-if="currentTab === 2"
v-if="currentTab === 'cfg'"
style="min-width: 0; width: 25dvw"
class="self-center"
size="small"
@ -234,28 +232,19 @@ listen<{ message: string; header: string }>('invoke-error', (event) => {
</TabList>
</div>
<TabPanels class="w-full grow mt-[3rem]">
<TabPanel :value="0">
<TabPanel value="loc">
<ModList :search="pkgSearchTerm" />
</TabPanel>
<TabPanel :value="1">
<TabPanel value="rmt">
<ModStore :search="pkgSearchTerm" />
</TabPanel>
<TabPanel :value="2">
<TabPanel value="cfg">
<OptionList />
</TabPanel>
<TabPanel :value="3">
<TabPanel value="users">
<ProfileList />
<br /><br /><br />
<footer>
<Button
icon="pi pi-discord"
as="a"
target="_blank"
href="https://discord.gg/jxvzHjjEmc"
/>
</footer>
</TabPanel>
<TabPanel :value="4">
<TabPanel value="patches">
<PatchList
v-if="
pkg.hasLocal('mempatcher-mempatcher') &&
@ -268,8 +257,11 @@ listen<{ message: string; header: string }>('invoke-error', (event) => {
and enabled.
</div>
</TabPanel>
<TabPanel value="info">
<InfoPage />
</TabPanel>
</TabPanels>
<div v-if="currentTab === 5 || currentTab === 3">
<div v-if="currentTab === 'users' || currentTab === 'info'">
<img
v-if="prf.current?.meta.game === 'ongeki'"
src="/sticker-ongeki.svg"

View File

@ -0,0 +1,56 @@
<script setup lang="ts">
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));
</script>
<template>
<h1>About</h1>
STARTLINER is a simple launcher, configuration tool and mod manager for
O.N.G.E.K.I. and CHUNITHM.
<h1>Changelog</h1>
<ScrollPanel style="height: 200px">
<div class="changelog">
<vue-markdown-it
:source="changelog"
:options="{ typographer: true, breaks: true }"
/>
</div>
</ScrollPanel>
<footer class="mt-10 flex gap-3">
<Button
icon="pi pi-discord"
as="a"
target="_blank"
href="https://discord.gg/jxvzHjjEmc"
/>
<Button
icon="pi pi-github"
as="a"
target="_blank"
href="https://gitea.tendokyu.moe/akanyan/STARTLINER"
/>
</footer>
</template>
<style lang="css">
h1 {
font-size: 1.7rem;
}
.changelog h2 {
font-size: 1.4rem;
}
.changelog ul {
list-style-type: circle;
}
.changelog li {
margin-left: 40px;
}
</style>

View File

@ -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<string[]> = 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(() => {
<Fieldset v-for="(namespace, key) in group" :legend="key.toString()">
<ModListEntry v-for="p in namespace" :pkg="p" />
</Fieldset>
<div v-if="empty" class="text-3xl"></div>
<div v-if="empty === true" class="text-3xl fadein"></div>
</template>

View File

@ -1,10 +1,12 @@
<script setup lang="ts">
import { computed } from 'vue';
import Button from 'primevue/button';
import ToggleSwitch from 'primevue/toggleswitch';
import InstallButton from './InstallButton.vue';
import LinkButton from './LinkButton.vue';
import ModTitlecard from './ModTitlecard.vue';
import UpdateButton from './UpdateButton.vue';
import { invoke } from '../invoke';
import { usePkgStore, usePrfStore } from '../stores';
import { Feature, Package } from '../types';
import { hasFeature } from '../util';
@ -38,7 +40,7 @@ const model = computed({
v-model="model"
/>
<InstallButton :pkg="pkg" />
<!-- <Button
<Button
rounded
icon="pi pi-folder"
severity="help"
@ -46,8 +48,10 @@ const model = computed({
size="small"
class="ml-2 shrink-0"
style="width: 2rem; height: 2rem"
v-on:click="pkg?.loc && open(pkg.loc.path ?? '')"
/> -->
v-on:click="
pkg?.loc?.path && invoke('open_file', { path: pkg.loc.path })
"
/>
<LinkButton v-if="pkgs.networkStatus === 'online'" :pkg="pkg" />
</div>
</template>

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { Ref, ref } from 'vue';
import { Ref, computed, ref } from 'vue';
import Button from 'primevue/button';
import Divider from 'primevue/divider';
import MultiSelect from 'primevue/multiselect';
import ToggleSwitch from 'primevue/toggleswitch';
@ -40,6 +41,39 @@ const list = () => {
empty.value = res.length === 0;
return res;
};
const shouldShowRecommended = computed(() => {
if (prf.current!.meta.game === 'ongeki') {
return !pkgs.allLocal.some((p) => pkgKey(p) === 'segatools-mu3hook');
}
if (prf.current!.meta.game === 'chunithm') {
return (
!pkgs.allLocal.some((p) => pkgKey(p) === 'segatools-chusanhook') ||
!pkgs.allLocal.some((p) => pkgKey(p) === 'mempatcher-mempatcher')
);
}
return false;
});
const getRecommendedTooltip = () => {
if (prf.current!.meta.game === 'ongeki') {
return 'segatools-mu3hook';
}
if (prf.current!.meta.game === 'chunithm') {
return 'segatools-chusanhook and mempatcher';
}
return '';
};
const installRecommended = () => {
if (prf.current!.meta.game === 'ongeki') {
pkgs.installFromKey('segatools-mu3hook');
}
if (prf.current!.meta.game === 'chunithm') {
pkgs.installFromKey('segatools-chusanhook');
pkgs.installFromKey('mempatcher-mempatcher');
}
};
</script>
<template>
@ -78,6 +112,14 @@ const list = () => {
</div>
</div>
<Divider />
<Button
v-if="shouldShowRecommended"
label="Install recommended packages"
v-tooltip="getRecommendedTooltip"
icon="pi pi-plus"
class="mb-3"
@click="installRecommended"
/>
<div v-for="p in list()" class="flex flex-row">
<ModStoreEntry :pkg="p" />
</div>

View File

@ -22,7 +22,6 @@ const prf = usePrfStore();
icon="pi pi-plus"
class="chunithm-button profile-button"
@click="() => prf.create('chunithm')"
v-tooltip="'!!! Experimental !!!'"
/>
</div>
<div class="mt-12 flex flex-col flex-wrap align-middle gap-4">

View File

@ -2,10 +2,12 @@
import { ref } from 'vue';
import Button from 'primevue/button';
import InputText from 'primevue/inputtext';
import * as path from '@tauri-apps/api/path';
import { invoke } from '../invoke';
import { usePrfStore } from '../stores';
import { useGeneralStore, usePrfStore } from '../stores';
import { ProfileMeta } from '../types';
const general = useGeneralStore();
const prf = usePrfStore();
const isEditing = ref(false);
@ -110,7 +112,7 @@ const deleteProfile = async () => {
style="width: 2rem; height: 2rem"
@click="isEditing = true"
/>
<!-- <Button
<Button
rounded
icon="pi pi-folder"
severity="help"
@ -121,9 +123,13 @@ const deleteProfile = async () => {
@click="
path
.join(general.dataDir, `profile-${p!.game}-${p!.name}`)
.then(open)
.then(async (path) => {
if (await invoke('file_exists', { path })) {
await invoke('open_file', { path });
}
})
"
/> -->
/>
</div>
</template>

View File

@ -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 {

View File

@ -119,6 +119,7 @@ const checkSegatoolsIni = async (target: string) => {
return { title: pkgKey(p), value: pkgKey(p) };
})
"
placeholder="none"
option-label="title"
option-value="value"
></Select>

View File

@ -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);
},
});
</script>
<template>
@ -45,12 +54,18 @@ const updatesModel = computed({
</OptionRow>
<OptionRow
title="Offline mode"
tooltip="Disables the package store. Requires a restart."
tooltip="Disables the package store. Applies after a restart."
>
<ToggleSwitch v-model="offlineModel" />
</OptionRow>
<OptionRow title="Enable automatic updates">
<ToggleSwitch v-model="updatesModel" />
</OptionRow>
<OptionRow
title="Enable detailed logs"
tooltip="Applies after a restart."
>
<ToggleSwitch v-model="verboseModel" />
</OptionRow>
</OptionCategory>
</template>

View File

@ -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,
};
});