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

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