From ae852d0757a0a7515856bf09ffbabf2c9c9a0a6e Mon Sep 17 00:00:00 2001 From: SARDONYX-sard <68905624+SARDONYX-sard@users.noreply.github.com> Date: Fri, 13 Oct 2023 05:35:27 +0900 Subject: [PATCH] feat: implement DAR hinder & OAR remover --- cspell.jsonc | 1 + dar2oar_cli/src/lib.rs | 51 +++++--- dar2oar_core/benches/convert_n_thread.rs | 18 ++- dar2oar_core/src/fs/mod.rs | 123 +++++++++++++++++- .../src/fs/{parallel/mod.rs => parallel.rs} | 66 +++++++--- dar2oar_core/src/fs/path_changer.rs | 45 +++++-- .../fs/{sequential/mod.rs => sequential.rs} | 78 +++++++---- frontend/src/app/layout.tsx | 2 +- .../components/{ => buttons}/convert_btn.tsx | 0 .../components/{ => buttons}/log_file_btn.tsx | 0 .../{ => buttons}/path_selector.tsx | 4 +- .../src/components/buttons/remove_oar_btn.tsx | 49 +++++++ .../components/buttons/restore_dar_btn.tsx | 41 ++++++ frontend/src/components/form.tsx | 115 ++++++++++++---- .../components/{ => lists}/editor_list.tsx | 2 +- .../{ => lists}/select_log_level.tsx | 2 +- .../src/components/{ => lists}/style_list.tsx | 2 +- frontend/src/components/pages/converter.tsx | 11 +- frontend/src/components/pages/settings.tsx | 24 ++-- .../src/components/{ => providers}/theme.tsx | 0 frontend/src/tauri_cmd/index.ts | 50 ++++--- src-tauri/src/cmd.rs | 72 ++++++---- src-tauri/src/runner.rs | 6 +- 23 files changed, 599 insertions(+), 163 deletions(-) rename dar2oar_core/src/fs/{parallel/mod.rs => parallel.rs} (68%) rename dar2oar_core/src/fs/{sequential/mod.rs => sequential.rs} (70%) rename frontend/src/components/{ => buttons}/convert_btn.tsx (100%) rename frontend/src/components/{ => buttons}/log_file_btn.tsx (100%) rename frontend/src/components/{ => buttons}/path_selector.tsx (80%) create mode 100644 frontend/src/components/buttons/remove_oar_btn.tsx create mode 100644 frontend/src/components/buttons/restore_dar_btn.tsx rename frontend/src/components/{ => lists}/editor_list.tsx (94%) rename frontend/src/components/{ => lists}/select_log_level.tsx (96%) rename frontend/src/components/{ => lists}/style_list.tsx (95%) rename frontend/src/components/{ => providers}/theme.tsx (100%) diff --git a/cspell.jsonc b/cspell.jsonc index 5123700..259af7a 100644 --- a/cspell.jsonc +++ b/cspell.jsonc @@ -7,6 +7,7 @@ "Greatsword", "icns", "jwalk", + "mohidden", "multispace", "noconflict", "rfind", diff --git a/dar2oar_cli/src/lib.rs b/dar2oar_cli/src/lib.rs index 28b2c5c..aebe9f0 100644 --- a/dar2oar_cli/src/lib.rs +++ b/dar2oar_cli/src/lib.rs @@ -1,5 +1,9 @@ use clap::{arg, Parser}; -use dar2oar_core::{convert_dar_to_oar, fs::parallel, read_mapping_table}; +use dar2oar_core::{ + convert_dar_to_oar, + fs::{parallel, ConvertOptions}, + read_mapping_table, +}; use std::path::PathBuf; /// dar2oar --src "DAR path" --dist "OAR path" @@ -12,7 +16,7 @@ pub struct Args { /// OAR destination dir path(If not, it is inferred from src) #[clap(long, value_parser)] dist: Option, - /// mod name in config.json & folder name(If not, it is inferred from src) + /// mod name in config.json & directory name(If not, it is inferred from src) #[arg(long)] name: Option, /// mod author in config.json @@ -33,6 +37,9 @@ pub struct Args { /// use multi thread(Probably effective for those with long DAR syntax. Basically single-threaded is faster.) #[arg(long)] run_parallel: bool, + #[arg(long)] + /// After converting to OAR, add mohidden to the DAR directory before conversion to treat it as a hidden directory. (for MO2 users) + hide_dar: bool, } pub fn run_cli(args: Args) -> anyhow::Result<()> { @@ -64,22 +71,26 @@ pub fn run_cli(args: Args) -> anyhow::Result<()> { None => None, }; - match args.run_parallel { - true => parallel::convert_dar_to_oar( - args.src, - dist, - args.name.as_deref(), - args.author.as_deref(), - table, - table_1person, - ), - false => convert_dar_to_oar( - args.src, - dist, - args.name.as_deref(), - args.author.as_deref(), - table, - table_1person, - ), - } + let msg = match args.run_parallel { + true => parallel::convert_dar_to_oar(ConvertOptions { + dar_dir: args.src, + oar_dir: dist, + mod_name: args.name.as_deref(), + author: args.author.as_deref(), + section_table: table, + section_1person_table: table_1person, + hide_dar: args.hide_dar, + }), + false => convert_dar_to_oar(ConvertOptions { + dar_dir: args.src, + oar_dir: dist, + mod_name: args.name.as_deref(), + author: args.author.as_deref(), + section_table: table, + section_1person_table: table_1person, + hide_dar: args.hide_dar, + }), + }?; + log::debug!("{}", msg); + Ok(()) } diff --git a/dar2oar_core/benches/convert_n_thread.rs b/dar2oar_core/benches/convert_n_thread.rs index c57e1b4..ffda5c0 100644 --- a/dar2oar_core/benches/convert_n_thread.rs +++ b/dar2oar_core/benches/convert_n_thread.rs @@ -1,5 +1,9 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use dar2oar_core::{convert_dar_to_oar, fs::parallel, read_mapping_table}; +use dar2oar_core::{ + convert_dar_to_oar, + fs::{parallel, ConvertOptions}, + read_mapping_table, +}; use std::time::Duration; const REMOVE_TARGET: &str = @@ -28,7 +32,11 @@ fn criterion_benchmark(c: &mut Criterion) { let table_content = "../test/settings/mapping_table.txt"; let mapping = read_mapping_table(table_content).unwrap(); - parallel::convert_dar_to_oar(black_box(TARGET), None, None, None, Some(mapping), None) + parallel::convert_dar_to_oar(black_box(ConvertOptions { + dar_dir: TARGET, + section_table: Some(mapping), + ..Default::default() + })) }) }); @@ -40,7 +48,11 @@ fn criterion_benchmark(c: &mut Criterion) { let table_content = "../test/settings/mapping_table.txt"; let mapping = read_mapping_table(table_content).unwrap(); - convert_dar_to_oar(black_box(TARGET), None, None, None, Some(mapping), None) + convert_dar_to_oar(black_box(ConvertOptions { + dar_dir: TARGET, + section_table: Some(mapping), + ..Default::default() + })) }) }); diff --git a/dar2oar_core/src/fs/mod.rs b/dar2oar_core/src/fs/mod.rs index 5a3c2de..1ec9099 100644 --- a/dar2oar_core/src/fs/mod.rs +++ b/dar2oar_core/src/fs/mod.rs @@ -8,9 +8,28 @@ pub use sequential::convert_dar_to_oar; use crate::conditions::{ConditionsConfig, MainConfig}; use anyhow::Context as _; +use std::collections::HashMap; use std::fs; use std::io::{self, Read, Write}; -use std::path::Path; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Default, PartialEq)] +pub struct ConvertOptions<'a, P: AsRef> { + /// DAR source dir path + pub dar_dir: P, + /// OAR destination dir path(If not, it is inferred from src) + pub oar_dir: Option, + /// mod name in config.json & directory name(If not, it is inferred from src) + pub mod_name: Option<&'a str>, + /// mod author in config.json + pub author: Option<&'a str>, + /// path to section name table + pub section_table: Option>, + /// path to section name table(For _1st_person) + pub section_1person_table: Option>, + /// After converting to OAR, add mohidden to the DAR directory before conversion to treat it as a hidden directory. (for MO2 users) + pub hide_dar: bool, +} fn read_file

(file_path: P) -> io::Result where @@ -64,3 +83,105 @@ where config_file.write_all(json.as_bytes())?; Ok(()) } + +/// # Returns +/// Report which dirs have been restored +/// +/// # NOTE +/// It is currently used only in GUI, but is implemented in Core as an API. +pub fn restore_dar(dar_dir: impl AsRef) -> anyhow::Result { + let mut restored_dar = None; + let mut restored_1st_dar = None; + for entry in walkdir::WalkDir::new(dar_dir) { + let entry = entry?; + let path = entry.path(); + let (dar_root, _, is_1st_person, _, _, _) = + match path_changer::parse_dar_path(path, Some("DynamicAnimationReplacer.mohidden")) { + Ok(data) => data, + Err(_) => continue, // NOTE: The first search is skipped because it does not yet lead to the DAR file. + }; + + if restored_dar.is_none() && path.is_dir() { + restored_dar = Some(dar_root); + continue; + } + if restored_1st_dar.is_none() && path.is_dir() && is_1st_person { + restored_1st_dar = Some(dar_root); + } + } + + let mut msg = String::new(); + if let Some(dar_root) = restored_dar.as_ref() { + let dist = dar_root + .as_os_str() + .to_string_lossy() + .replace(".mohidden", ""); + fs::rename(dar_root.clone(), dist)?; + msg = format!("{}- Restored 3rd_person", msg); + } + if let Some(dar_root) = restored_1st_dar.as_ref() { + let dist = dar_root + .as_os_str() + .to_string_lossy() + .replace(".mohidden", ""); + fs::rename(dar_root.clone(), dist)?; + msg = format!("{}\n- Restored 1rd_person", msg); + } + + if restored_dar.is_none() && restored_1st_dar.is_none() { + anyhow::bail!("Neither 1st or 3rd person DynamicAnimationReplacer.mohidden found.") + } else { + Ok(msg) + } +} + +/// # NOTE +/// It is currently used only in GUI, but is implemented in Core as an API. +pub fn remove_oar(dar_dir: impl AsRef) -> anyhow::Result<()> { + let mut restored_dar = None; + let mut restored_1st_dar = None; + for entry in walkdir::WalkDir::new(dar_dir) { + let entry = entry?; + let path = entry.path(); + // NOTE: The OAR root obtained by parse fn is calculated and not guaranteed to exist. + let (dar_root, oar_name_space_path, is_1st_person, _, _, _) = + match path_changer::parse_dar_path(path, Some("DynamicAnimationReplacer.mohidden")) { + Ok(data) => data, + Err(_) => { + match path_changer::parse_dar_path(path, Some("DynamicAnimationReplacer")) { + Ok(data) => data, + Err(_) => continue, // NOTE: The first search is skipped because it does not yet lead to the DAR file. + } + } // NOTE: The first search is skipped because it does not yet lead to the DAR file. + }; + + if restored_dar.is_none() && path.is_dir() { + restored_dar = Some((dar_root, oar_name_space_path)); + continue; + } + if restored_1st_dar.is_none() && path.is_dir() && is_1st_person { + restored_1st_dar = Some((dar_root, oar_name_space_path)); + } + } + + if let Some((_, oar_root)) = restored_dar { + dbg!(&oar_root); + if oar_root.exists() { + fs::remove_dir_all(oar_root)?; + } + } + if let Some((_, oar_root)) = restored_1st_dar { + dbg!(&oar_root); + if oar_root.exists() { + fs::remove_dir_all(oar_root)?; + } + } + Ok(()) +} + +#[ignore] +#[test] +fn should_restore_dar() { + let dar_dir = "../test/data/UNDERDOG Animations"; + remove_oar(dar_dir).unwrap(); +} diff --git a/dar2oar_core/src/fs/parallel/mod.rs b/dar2oar_core/src/fs/parallel.rs similarity index 68% rename from dar2oar_core/src/fs/parallel/mod.rs rename to dar2oar_core/src/fs/parallel.rs index 3c8e787..2d9dfa0 100644 --- a/dar2oar_core/src/fs/parallel/mod.rs +++ b/dar2oar_core/src/fs/parallel.rs @@ -1,33 +1,36 @@ use crate::condition_parser::parse_dar2oar; use crate::conditions::ConditionsConfig; use crate::fs::path_changer::parse_dar_path; -use crate::fs::{read_file, write_name_space_config, write_section_config}; -use anyhow::{Context as _, Result, bail}; +use crate::fs::{read_file, write_name_space_config, write_section_config, ConvertOptions}; +use anyhow::{bail, Context as _, Result}; use jwalk::WalkDir; -use std::collections::HashMap; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::Path; /// multi thread converter -pub fn convert_dar_to_oar

( - dar_dir: P, - oar_dir: Option, - mod_name: Option<&str>, - author: Option<&str>, - section_table: Option>, - section_1person_table: Option>, -) -> Result<()> -where - P: AsRef, -{ +/// # Return +/// Complete info +pub fn convert_dar_to_oar(options: ConvertOptions>) -> Result { + let ConvertOptions { + dar_dir, + oar_dir, + mod_name, + author, + section_table, + section_1person_table, + hide_dar, + } = options; let mut is_converted_once = false; + let mut dar_namespace = None; // To need rename to hidden + let mut dar_1st_namespace = None; // To need rename to hidden(For _1stperson) for entry in WalkDir::new(dar_dir) { let entry = entry?; let path = entry.path(); // Separate this for binding let path = path.as_path(); - let (oar_name_space_path, is_1st_person, parsed_mod_name, priority, remain) = - match parse_dar_path(path) { + + let (dar_root, oar_name_space_path, is_1st_person, parsed_mod_name, priority, remain) = + match parse_dar_path(path, None) { Ok(data) => data, Err(_) => continue, // NOTE: The first search is skipped because it does not yet lead to the DAR file. }; @@ -88,6 +91,13 @@ where Err(err) => log::error!("Error reading file {path:?}: {err}"), } + if is_1st_person { + if dar_1st_namespace.is_none() { + dar_1st_namespace = Some(dar_root); + } + } else if dar_namespace.is_none() { + dar_namespace = Some(dar_root); + } if !is_converted_once { is_converted_once = true; write_name_space_config(&oar_name_space_path, &parsed_mod_name, author) @@ -99,7 +109,7 @@ where })?; } } else { - // maybe motion files(.hkx) + // maybe motion files(.kkx) if let Some(remain) = remain { let non_leaf_dir = section_root.join(remain); fs::create_dir_all(&non_leaf_dir)?; @@ -112,7 +122,25 @@ where } match is_converted_once { - true => Ok(()), + true => { + let mut msg = "Conversion Completed.".to_string(); + if hide_dar { + if let Some(dar_namespace) = dar_namespace { + let mut dist = dar_namespace.clone(); + dist.as_mut_os_string().push(".mohidden"); + fs::rename(dar_namespace, dist)?; + msg = format!("{}\n- 3rdPerson DAR dir was renamed", msg); + }; + + if let Some(dar_1st_namespace) = dar_1st_namespace { + let mut dist = dar_1st_namespace.clone(); + dist.as_mut_os_string().push(".mohidden"); + fs::rename(dar_1st_namespace, dist)?; + msg = format!("{}\n- 1stPerson DAR dir was renamed", msg); + }; + } + Ok(msg) + } false => bail!("DynamicAnimationReplacer dir was never found"), } } diff --git a/dar2oar_core/src/fs/path_changer.rs b/dar2oar_core/src/fs/path_changer.rs index 8c5e020..718b57d 100644 --- a/dar2oar_core/src/fs/path_changer.rs +++ b/dar2oar_core/src/fs/path_changer.rs @@ -5,6 +5,7 @@ use std::path::{Path, PathBuf}; /// - Ok: oar root path, is_1st_person_dir, mod name, priority, non leaf remain(Path excluding file from priority to the end) /// - Err: anyhow Error type DarPathsResult = Result<( + PathBuf, PathBuf, bool, Option, @@ -29,20 +30,23 @@ type DarPathsResult = Result<( /// /// # DAR: /// - "\/\/_1stperson/character/animations/DynamicAnimationReplacer/_CustomConditions/\/_conditions.txt" -pub fn parse_dar_path(path: impl AsRef) -> DarPathsResult { +pub fn parse_dar_path(path: impl AsRef, dar_dirname: Option<&str>) -> DarPathsResult { let path = path.as_ref(); let paths: Vec<&OsStr> = path.iter().collect(); let is_1st_person = path.iter().any(|os_str| os_str == OsStr::new("_1stperson")); - let oar_root = path + let (dar_root, oar_root) = path .iter() - .position(|os_str| os_str == OsStr::new("DynamicAnimationReplacer")) + .position(|os_str| os_str == OsStr::new(dar_dirname.unwrap_or("DynamicAnimationReplacer"))) .and_then(|idx| { paths.get(0..idx).map(|path| { - let mut string = path.join(OsStr::new("/")); - string.push("/OpenAnimationReplacer"); - Path::new(&string).to_path_buf() + let mut dar = path.join(OsStr::new("/")); + let mut oar = dar.clone(); + dar.push("/"); + dar.push(dar_dirname.unwrap_or("DynamicAnimationReplacer")); + oar.push("/OpenAnimationReplacer"); + (Path::new(&dar).to_path_buf(), Path::new(&oar).to_path_buf()) }) }) .with_context(|| { @@ -93,7 +97,14 @@ pub fn parse_dar_path(path: impl AsRef) -> DarPathsResult { }) }); - Ok((oar_root, is_1st_person, mod_name, priority, non_leaf_remain)) + Ok(( + dar_root, + oar_root, + is_1st_person, + mod_name, + priority, + non_leaf_remain, + )) } #[cfg(test)] @@ -105,11 +116,15 @@ mod test { #[test] fn test_parse_dar_path_1st_person() { let path = Path::new("../ModName/meshes/actors/character/_1stperson/animations/DynamicAnimationReplacer/_CustomConditions/8107000/_conditions.txt"); - let result = parse_dar_path(path); + let result = parse_dar_path(path, None); assert!(result.is_ok()); - let (oar_root, is_1st_person, mod_name, priority, remain) = result.unwrap(); + let (dar_root, oar_root, is_1st_person, mod_name, priority, remain) = result.unwrap(); + assert_eq!( + dar_root, + PathBuf::from("../ModName/meshes/actors/character/_1stperson/animations/DynamicAnimationReplacer") + ); assert_eq!( oar_root, PathBuf::from( @@ -124,12 +139,16 @@ mod test { #[test] fn test_parse_dar_path_3rd_person() { - let path = Path::new("../ModName/meshes/actors/character/animations/DynamicAnimationReplacer/_CustomConditions/8107000/InnerDir/_conditions.txt"); - let result = parse_dar_path(path); + let path = Path::new("../ModName/meshes/actors/character/animations/DynamicAnimationReplacer.mohidden/_CustomConditions/8107000/InnerDir/_conditions.txt"); + let result = parse_dar_path(path, Some("DynamicAnimationReplacer.mohidden")); assert!(result.is_ok()); - let (oar_root, is_1st_person, mod_name, priority, remain) = result.unwrap(); + let (dar_root, oar_root, is_1st_person, mod_name, priority, remain) = result.unwrap(); + assert_eq!( + dar_root, + PathBuf::from("../ModName/meshes/actors/character/animations/DynamicAnimationReplacer.mohidden") + ); assert_eq!( oar_root, PathBuf::from("../ModName/meshes/actors/character/animations/OpenAnimationReplacer") @@ -145,7 +164,7 @@ mod test { // Create a path with invalid UTF-8 characters let invalid_path = OsStr::new("invalid_path").to_os_string(); let path = Path::new(&invalid_path); - let result = parse_dar_path(path); + let result = parse_dar_path(path, None); assert!(result.is_err()); } } diff --git a/dar2oar_core/src/fs/sequential/mod.rs b/dar2oar_core/src/fs/sequential.rs similarity index 70% rename from dar2oar_core/src/fs/sequential/mod.rs rename to dar2oar_core/src/fs/sequential.rs index 456a220..b9acd12 100644 --- a/dar2oar_core/src/fs/sequential/mod.rs +++ b/dar2oar_core/src/fs/sequential.rs @@ -1,32 +1,36 @@ use crate::condition_parser::parse_dar2oar; use crate::conditions::ConditionsConfig; use crate::fs::path_changer::parse_dar_path; -use crate::fs::{read_file, write_name_space_config, write_section_config}; +use crate::fs::{read_file, write_name_space_config, write_section_config, ConvertOptions}; use anyhow::{bail, Context as _, Result}; -use std::collections::HashMap; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::Path; use walkdir::WalkDir; /// Single thread converter -pub fn convert_dar_to_oar

( - dar_dir: P, - oar_dir: Option, - mod_name: Option<&str>, - author: Option<&str>, - section_table: Option>, - section_1person_table: Option>, -) -> Result<()> -where - P: AsRef, -{ +/// +/// # Return +/// Complete info +pub fn convert_dar_to_oar(options: ConvertOptions>) -> Result { + let ConvertOptions { + dar_dir, + oar_dir, + mod_name, + author, + section_table, + section_1person_table, + hide_dar, + } = options; let mut is_converted_once = false; + let mut dar_namespace = None; // To need rename to hidden + let mut dar_1st_namespace = None; // To need rename to hidden(For _1stperson) for entry in WalkDir::new(dar_dir) { let entry = entry?; let path = entry.path(); - let (oar_name_space_path, is_1st_person, parsed_mod_name, priority, remain) = - match parse_dar_path(path) { + + let (dar_root, oar_name_space_path, is_1st_person, parsed_mod_name, priority, remain) = + match parse_dar_path(path, None) { Ok(data) => data, Err(_) => continue, // NOTE: The first search is skipped because it does not yet lead to the DAR file. }; @@ -87,6 +91,13 @@ where Err(err) => log::error!("Error reading file {path:?}: {err}"), } + if is_1st_person { + if dar_1st_namespace.is_none() { + dar_1st_namespace = Some(dar_root); + } + } else if dar_namespace.is_none() { + dar_namespace = Some(dar_root); + } if !is_converted_once { is_converted_once = true; write_name_space_config(&oar_name_space_path, &parsed_mod_name, author) @@ -111,7 +122,25 @@ where } match is_converted_once { - true => Ok(()), + true => { + let mut msg = "Conversion Completed.".to_string(); + if hide_dar { + if let Some(dar_namespace) = dar_namespace { + let mut dist = dar_namespace.clone(); + dist.as_mut_os_string().push(".mohidden"); + fs::rename(dar_namespace, dist)?; + msg = format!("{}\n- 3rdPerson DAR dir was renamed", msg); + }; + + if let Some(dar_1st_namespace) = dar_1st_namespace { + let mut dist = dar_1st_namespace.clone(); + dist.as_mut_os_string().push(".mohidden"); + fs::rename(dar_1st_namespace, dist)?; + msg = format!("{}\n- 1stPerson DAR dir was renamed", msg); + }; + } + Ok(msg) + } false => bail!("DynamicAnimationReplacer dir was never found"), } } @@ -137,13 +166,12 @@ mod test { let table = crate::read_mapping_table("../test/settings/UnderDog Animations_mapping_table.txt") .unwrap(); - convert_dar_to_oar( - "../test/data/UNDERDOG Animations", - None, - None, - None, - Some(table), - None, - ) + convert_dar_to_oar(ConvertOptions { + dar_dir: "../test/data/UNDERDOG Animations", + section_table: Some(table), + hide_dar: true, + ..Default::default() + })?; + Ok(()) } } diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index bd42e93..6427105 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -11,7 +11,7 @@ const Menu = dynamic(() => import("../components/navigation"), { loading: () => , ssr: false, }); -const ThemeProvider = dynamic(() => import("../components/theme"), { +const ThemeProvider = dynamic(() => import("../components/providers/theme"), { loading: () => , ssr: false, diff --git a/frontend/src/components/convert_btn.tsx b/frontend/src/components/buttons/convert_btn.tsx similarity index 100% rename from frontend/src/components/convert_btn.tsx rename to frontend/src/components/buttons/convert_btn.tsx diff --git a/frontend/src/components/log_file_btn.tsx b/frontend/src/components/buttons/log_file_btn.tsx similarity index 100% rename from frontend/src/components/log_file_btn.tsx rename to frontend/src/components/buttons/log_file_btn.tsx diff --git a/frontend/src/components/path_selector.tsx b/frontend/src/components/buttons/path_selector.tsx similarity index 80% rename from frontend/src/components/path_selector.tsx rename to frontend/src/components/buttons/path_selector.tsx index d679281..00748c7 100644 --- a/frontend/src/components/path_selector.tsx +++ b/frontend/src/components/buttons/path_selector.tsx @@ -1,6 +1,6 @@ import { Button } from "@mui/material"; import toast from "react-hot-toast"; -import { openPath } from "../tauri_cmd"; +import { openPath } from "../../tauri_cmd"; type Props = { path: string; @@ -8,7 +8,7 @@ type Props = { setValue: (value: string) => void; }; -export function PathSelector({ path, isDir = false, setValue }: Props) { +export function SelectPathButton({ path, isDir = false, setValue }: Props) { const handleClick = async () => { openPath(path, setValue, isDir).catch((e) => toast.error(`${e}`)); }; diff --git a/frontend/src/components/buttons/remove_oar_btn.tsx b/frontend/src/components/buttons/remove_oar_btn.tsx new file mode 100644 index 0000000..9dca8ce --- /dev/null +++ b/frontend/src/components/buttons/remove_oar_btn.tsx @@ -0,0 +1,49 @@ +import { removeOarDir } from "@/tauri_cmd"; +import toast from "react-hot-toast"; +import DeleteIcon from "@mui/icons-material/Delete"; +import Button from "@mui/material/Button"; +import Tooltip from "@mui/material/Tooltip"; + +type Props = { + darPath: string; + oarPath: string; +}; + +export const RemoveOarBtn = ({ darPath, oarPath }: Props) => { + return ( + + Find and delete OAR dir from "DAR(src) Directory*" or + "OAR(dist) Directory". +

+ } + > + + + ); +}; diff --git a/frontend/src/components/buttons/restore_dar_btn.tsx b/frontend/src/components/buttons/restore_dar_btn.tsx new file mode 100644 index 0000000..f8f36ea --- /dev/null +++ b/frontend/src/components/buttons/restore_dar_btn.tsx @@ -0,0 +1,41 @@ +import { Tooltip } from "@mui/material"; +import Button from "@mui/material/Button"; +import { restoreDarDir } from "@/tauri_cmd"; +import toast from "react-hot-toast"; +import RestoreIcon from "@mui/icons-material/Restore"; + +type Props = { + path: string; +}; + +export const RestoreDarBtn = ({ path }: Props) => { + return ( + + Restore the directory hidden by "Hide DAR".(For MO2 user) +

+ } + > + +
+ ); +}; diff --git a/frontend/src/components/form.tsx b/frontend/src/components/form.tsx index 612a1ef..1c9af1e 100644 --- a/frontend/src/components/form.tsx +++ b/frontend/src/components/form.tsx @@ -12,9 +12,11 @@ import type { SubmitHandler } from "react-hook-form"; import { Controller, useForm } from "react-hook-form"; import toast from "react-hot-toast"; import { convertDar2oar } from "../tauri_cmd"; -import ConvertButton from "./convert_btn"; -import { PathSelector } from "./path_selector"; -import { LogFileButton } from "./log_file_btn"; +import ConvertButton from "./buttons/convert_btn"; +import { LogFileButton } from "./buttons/log_file_btn"; +import { SelectPathButton } from "./buttons/path_selector"; +import { RemoveOarBtn } from "./buttons/remove_oar_btn"; +import { RestoreDarBtn } from "./buttons/restore_dar_btn"; type FormProps = { src: string; @@ -25,10 +27,11 @@ type FormProps = { mapping1personPath: string; loading: boolean; runParallel: boolean; + hideDar: boolean; }; export function ConvertForm() { - const { handleSubmit, control, setValue } = useForm({ + const { handleSubmit, control, setValue, getValues } = useForm({ mode: "onBlur", criteriaMode: "all", shouldFocusError: false, @@ -40,6 +43,7 @@ export function ConvertForm() { mappingPath: localStorage.getItem("mappingPath") ?? "", mapping1personPath: localStorage.getItem("mapping1personPath") ?? "", runParallel: localStorage.getItem("runParallel") === "true", + hideDar: localStorage.getItem("hideDar") === "true", loading: false as boolean, } satisfies FormProps, }); @@ -72,22 +76,27 @@ export function ConvertForm() { mappingPath, mapping1personPath, runParallel, + hideDar, }) => { setLoading(true); - await convertDar2oar({ - src, - dist, - modName, - modAuthor, - mappingPath, - mapping1personPath, - runParallel, - }).catch((e) => { - toast.error(`${e}`); + try { + const completeInfo = await convertDar2oar({ + src, + dist, + modName, + modAuthor, + mappingPath, + mapping1personPath, + runParallel, + hideDar, + }); + toast.success(completeInfo); + } catch (err) { + toast.error(`${err}`); + } finally { setLoading(false); - }); - setLoading(false); + } }; return ( @@ -148,7 +157,11 @@ export function ConvertForm() { - + )} @@ -182,7 +195,7 @@ export function ConvertForm() { /> - - @@ -257,7 +270,7 @@ export function ConvertForm() { - @@ -322,13 +335,26 @@ export function ConvertForm() { + - + + ( - + + Use multi-threading. +
+ In most cases, it slows down by tens of ms, but may be + effective when there is more weight on CPU processing with + fewer files to copy and more logic parsing of + "_condition.txt" +

+ } + >
+ + + ( + + After conversion, append ".mohidden" to the DAR + dirname in "DAR(src) Directory*" to make it a + hidden directory(For MO2 users) +
+
+ NOTE: Failure to cross the drive or No permission. +

+ } + > + { + localStorage.setItem("hideDar", `${!value}`); + setValue("hideDar", !value); + }} + checked={value} + aria-label="Hide DAR" + /> + } + label="Hide DAR" + /> +
+ )} + /> +
+ + + + + + +
void; diff --git a/frontend/src/components/pages/converter.tsx b/frontend/src/components/pages/converter.tsx index 64b65d3..cb027a0 100644 --- a/frontend/src/components/pages/converter.tsx +++ b/frontend/src/components/pages/converter.tsx @@ -24,7 +24,16 @@ export default function Converter() { > - + ); } diff --git a/frontend/src/components/pages/settings.tsx b/frontend/src/components/pages/settings.tsx index cbd71e9..54c30dc 100644 --- a/frontend/src/components/pages/settings.tsx +++ b/frontend/src/components/pages/settings.tsx @@ -4,8 +4,8 @@ import AceEditor from "react-ace"; import { Box } from "@mui/material"; import InputLabel from "@mui/material/InputLabel"; import type { EditorMode } from "@/utils/editor"; -import { SelectEditorMode } from "@/components/editor_list"; -import { SelectStyleList } from "@/components/style_list"; +import { SelectEditorMode } from "@/components/lists/editor_list"; +import { SelectStyleList } from "@/components/lists/style_list"; import { Toaster } from "react-hot-toast"; import { selectEditorMode } from "@/utils/editor"; import { useDynStyle, useInjectScript, useStorageState } from "@/hooks"; @@ -74,19 +74,19 @@ export default function Settings() { display: "flex", justifyContent: "space-between", width: "40%", - marginTop: "30px" + marginTop: "30px", }} > - + - + diff --git a/frontend/src/components/theme.tsx b/frontend/src/components/providers/theme.tsx similarity index 100% rename from frontend/src/components/theme.tsx rename to frontend/src/components/providers/theme.tsx diff --git a/frontend/src/tauri_cmd/index.ts b/frontend/src/tauri_cmd/index.ts index 9147399..8c7d1c9 100644 --- a/frontend/src/tauri_cmd/index.ts +++ b/frontend/src/tauri_cmd/index.ts @@ -14,36 +14,54 @@ type ConverterArgs = { mapping1personPath?: string; logLevel?: LogLevel; runParallel?: boolean; + hideDar?: boolean; }; /** * Convert DAR to OAR * # Throw Error */ -export async function convertDar2oar(props: ConverterArgs): Promise { - const src = props.src === "" ? undefined : props.src; - const dist = props.dist === "" ? undefined : props.dist; +export async function convertDar2oar(props: ConverterArgs): Promise { + const darDir = props.src === "" ? undefined : props.src; + const oarDir = props.dist === "" ? undefined : props.dist; const modName = props.modName === "" ? undefined : props.modName; const modAuthor = props.modAuthor === "" ? undefined : props.modAuthor; const mapping1personPath = props.mapping1personPath === "" ? undefined : props.mapping1personPath; const mappingPath = props.mappingPath === "" ? undefined : props.mappingPath; const runParallel = props.runParallel ?? false; + const hideDar = props.hideDar ?? false; - try { - await invoke("convert_dar2oar", { - darModFolder: src, - oarModFolder: dist, - modName, - modAuthor, - mappingPath, - mapping1personPath, - logLevel: props.logLevel, - runParallel, - }); - } catch (e) { - throw new Error(`${e}`); + return invoke("convert_dar2oar", { + darDir, + oarDir, + modName, + modAuthor, + mappingPath, + mapping1personPath, + logLevel: props.logLevel, + runParallel, + hideDar, + }); +} + +/** + * @param darPath + * # Throw Error + */ +export async function restoreDarDir(darDir: string) { + if (darDir === "") { + throw new Error("DAR dir must be specified."); } + return invoke("restore_dar_dir", { darDir }); +} + +/** + * @param darPath + * # Throw Error + */ +export async function removeOarDir(path: string) { + await invoke("remove_oar_dir", { path }); } /** diff --git a/src-tauri/src/cmd.rs b/src-tauri/src/cmd.rs index af65042..5d80b74 100644 --- a/src-tauri/src/cmd.rs +++ b/src-tauri/src/cmd.rs @@ -1,4 +1,8 @@ -use dar2oar_core::{convert_dar_to_oar, fs::parallel, read_mapping_table}; +use dar2oar_core::{ + convert_dar_to_oar, + fs::{parallel, remove_oar, restore_dar, ConvertOptions}, + read_mapping_table, +}; use std::path::Path; /// early return with Err() and write log error. @@ -26,17 +30,18 @@ macro_rules! try_get_mapping_table { #[allow(clippy::too_many_arguments)] #[tauri::command] -pub fn convert_dar2oar( - dar_mod_folder: &str, - oar_mod_folder: Option<&str>, +pub(crate) fn convert_dar2oar( + dar_dir: &str, + oar_dir: Option<&str>, mod_name: Option<&str>, mod_author: Option<&str>, mapping_path: Option, mapping_1person_path: Option, log_level: Option, run_parallel: Option, -) -> Result<(), String> { - let dist = oar_mod_folder.and_then(|dist| match dist.is_empty() { + hide_dar: Option, +) -> Result { + let oar_dir = oar_dir.and_then(|dist| match dist.is_empty() { true => None, false => Some(Path::new(dist).to_path_buf()), }); @@ -55,37 +60,56 @@ pub fn convert_dar2oar( }) .unwrap_or("error"); - log::debug!("src: {}", dar_mod_folder); - log::debug!("dist: {:?}", dist); + log::debug!("src: {}", dar_dir); + log::debug!("dist: {:?}", oar_dir); log::debug!("mod_name: {:?}", mod_name); log::debug!("mod_author: {:?}", mod_author); log::debug!("table path: {:?}", mapping_path.as_ref()); log::debug!("1st person table path: {:?}", mapping_1person_path.as_ref()); log::debug!("log level: {:?}", log_level); log::debug!("run parallel: {:?}", run_parallel); + log::debug!("to hidden dar: {:?}", hide_dar); match run_parallel { - Some(true) => match parallel::convert_dar_to_oar( - dar_mod_folder, - dist, + Some(true) => match parallel::convert_dar_to_oar(ConvertOptions { + dar_dir, + oar_dir, mod_name, - mod_author, - table, - table_1person, - ) { - Ok(_) => Ok(()), + author: mod_author, + section_table: table, + section_1person_table: table_1person, + hide_dar: hide_dar.unwrap_or(false), + }) { + Ok(complete_msg) => Ok(complete_msg), Err(err) => bail!(err), }, - Some(false) | None => match convert_dar_to_oar( - dar_mod_folder, - dist, + Some(false) | None => match convert_dar_to_oar(ConvertOptions { + dar_dir, + oar_dir, mod_name, - mod_author, - table, - table_1person, - ) { - Ok(_) => Ok(()), + author: mod_author, + section_table: table, + section_1person_table: table_1person, + hide_dar: hide_dar.unwrap_or(false), + }) { + Ok(complete_msg) => Ok(complete_msg), Err(err) => bail!(err), }, } } + +#[tauri::command] +pub(crate) fn restore_dar_dir(dar_dir: &str) -> Result { + match restore_dar(dar_dir) { + Ok(res) => Ok(res), + Err(err) => Err(err.to_string()), + } +} + +#[tauri::command] +pub(crate) fn remove_oar_dir(path: &str) -> Result<(), String> { + match remove_oar(path) { + Ok(()) => Ok(()), + Err(err) => Err(err.to_string()), + } +} diff --git a/src-tauri/src/runner.rs b/src-tauri/src/runner.rs index d069801..114fbe9 100644 --- a/src-tauri/src/runner.rs +++ b/src-tauri/src/runner.rs @@ -8,7 +8,11 @@ pub fn run_tauri() -> anyhow::Result<()> { .targets([LogTarget::LogDir, LogTarget::Stdout, LogTarget::Webview]) .build(), ) - .invoke_handler(tauri::generate_handler![crate::cmd::convert_dar2oar]) + .invoke_handler(tauri::generate_handler![ + crate::cmd::convert_dar2oar, + crate::cmd::remove_oar_dir, + crate::cmd::restore_dar_dir, + ]) .run(tauri::generate_context!()) .context("Failed to execute tauri") }