mirror of
https://github.com/Grasscutters/Cultivation.git
synced 2025-12-12 23:24:35 +01:00
Merge branch 'mod_management'
This commit is contained in:
159
src-tauri/Cargo.lock
generated
159
src-tauri/Cargo.lock
generated
@@ -75,7 +75,7 @@ dependencies = [
|
||||
"asn1-rs-impl",
|
||||
"displaydoc",
|
||||
"nom",
|
||||
"num-traits",
|
||||
"num-traits 0.2.15",
|
||||
"rusticata-macros",
|
||||
"thiserror",
|
||||
"time 0.3.11",
|
||||
@@ -736,6 +736,7 @@ dependencies = [
|
||||
"tokio-rustls",
|
||||
"tokio-tungstenite",
|
||||
"tracing",
|
||||
"unrar",
|
||||
"zip 0.6.2",
|
||||
"zip-extract",
|
||||
]
|
||||
@@ -820,8 +821,8 @@ dependencies = [
|
||||
"asn1-rs",
|
||||
"displaydoc",
|
||||
"nom",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"num-bigint 0.4.3",
|
||||
"num-traits 0.2.15",
|
||||
"rusticata-macros",
|
||||
]
|
||||
|
||||
@@ -948,6 +949,15 @@ dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum_primitive"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be4551092f4d519593039259a9ed8daedf0da12e5109c5280338073eaeb81180"
|
||||
dependencies = [
|
||||
"num-traits 0.1.43",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "error-chain"
|
||||
version = "0.12.4"
|
||||
@@ -1041,6 +1051,12 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-cprng"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
|
||||
|
||||
[[package]]
|
||||
name = "futf"
|
||||
version = "0.1.5"
|
||||
@@ -1699,8 +1715,8 @@ dependencies = [
|
||||
"byteorder",
|
||||
"color_quant",
|
||||
"num-iter",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
"num-rational 0.4.1",
|
||||
"num-traits 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2171,6 +2187,32 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.1.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e"
|
||||
dependencies = [
|
||||
"num-bigint 0.1.44",
|
||||
"num-complex",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-rational 0.1.42",
|
||||
"num-traits 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e63899ad0da84ce718c14936262a41cee2c79c981fc0a0e7c7beb47d5a07e8c1"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits 0.2.15",
|
||||
"rand 0.4.6",
|
||||
"rustc-serialize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.3"
|
||||
@@ -2179,7 +2221,17 @@ checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"num-traits 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.1.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b288631d7878aaf59442cffd36910ea604ecd7745c36054328595114001c9656"
|
||||
dependencies = [
|
||||
"num-traits 0.2.15",
|
||||
"rustc-serialize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2189,7 +2241,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-traits",
|
||||
"num-traits 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2200,7 +2252,19 @@ checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"num-traits 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.1.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e"
|
||||
dependencies = [
|
||||
"num-bigint 0.1.44",
|
||||
"num-integer",
|
||||
"num-traits 0.2.15",
|
||||
"rustc-serialize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2211,7 +2275,16 @@ checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"num-traits 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.1.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31"
|
||||
dependencies = [
|
||||
"num-traits 0.2.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2831,6 +2904,19 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
|
||||
dependencies = [
|
||||
"fuchsia-cprng",
|
||||
"libc",
|
||||
"rand_core 0.3.1",
|
||||
"rdrand",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.7.3"
|
||||
@@ -2876,6 +2962,21 @@ dependencies = [
|
||||
"rand_core 0.6.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
|
||||
dependencies = [
|
||||
"rand_core 0.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.5.1"
|
||||
@@ -2967,6 +3068,15 @@ dependencies = [
|
||||
"yasna",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rdrand"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
|
||||
dependencies = [
|
||||
"rand_core 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.13"
|
||||
@@ -3113,6 +3223,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-serialize"
|
||||
version = "0.3.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.3.3"
|
||||
@@ -4289,6 +4405,31 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04"
|
||||
|
||||
[[package]]
|
||||
name = "unrar"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "433cea4f0b7bec88d47becb380887b8786a3cfb1c82e1ef9d32a682ba6801814"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"enum_primitive",
|
||||
"lazy_static",
|
||||
"num",
|
||||
"regex",
|
||||
"unrar_sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unrar_sys"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0009399408dc0bcc5c8910672544fceceeba18b91f741ff943916e917d982c60"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.7.1"
|
||||
|
||||
@@ -31,6 +31,7 @@ sysinfo = "0.24.6"
|
||||
|
||||
# ZIP-archive library.
|
||||
zip-extract = "0.1.1"
|
||||
unrar = "0.4.4"
|
||||
zip = "0.6.2"
|
||||
|
||||
# For creating a "global" downloads list.
|
||||
|
||||
BIN
src-tauri/icons/icon_resize.png
Normal file
BIN
src-tauri/icons/icon_resize.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
@@ -70,7 +70,11 @@
|
||||
"patch_metadata": "Patch and unpatch your game metadata automatically. Unless playing with old/non-official versions, or you have manually patched your metadata, this should be enabled."
|
||||
},
|
||||
"swag": {
|
||||
"akebi_name": "Akebi",
|
||||
"migoto_name": "Migoto",
|
||||
"reshade_name": "Reshade",
|
||||
"akebi": "Set Akebi Executable",
|
||||
"migoto": "Set 3dMigoto Executable"
|
||||
"migoto": "Set 3DMigoto Executable",
|
||||
"reshade": "Set Reshade Injector"
|
||||
}
|
||||
}
|
||||
|
||||
88
src-tauri/src/gamebanana.rs
Normal file
88
src-tauri/src/gamebanana.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use crate::file_helpers;
|
||||
use crate::web;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::read_dir;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
|
||||
static SITE_URL: &str = "https://gamebanana.com";
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_download_links(mod_id: String) -> String {
|
||||
let res = web::query(format!("{}/apiv9/Mod/{}/DownloadPage", SITE_URL, mod_id).as_str()).await;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_submissions(mode: String) -> String {
|
||||
let res = web::query(
|
||||
format!(
|
||||
"{}/apiv9/Util/Game/Submissions?_idGameRow=8552&_nPage=1&_nPerpage=50&_sMode={}",
|
||||
SITE_URL, mode
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.await;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_mods(path: String) -> HashMap<String, String> {
|
||||
let mut path_buf = PathBuf::from(path);
|
||||
|
||||
// If the path includes a file, remove it
|
||||
if path_buf.file_name().is_some() {
|
||||
path_buf.pop();
|
||||
}
|
||||
|
||||
// Ensure we are in the Mods folder
|
||||
path_buf.push("Mods");
|
||||
|
||||
// Check if dir is empty
|
||||
if file_helpers::dir_is_empty(path_buf.to_str().unwrap()) {
|
||||
return HashMap::new();
|
||||
}
|
||||
|
||||
let mut mod_info_files = vec![];
|
||||
let mut mod_info_strings = HashMap::new();
|
||||
|
||||
for entry in read_dir(path_buf).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
|
||||
// Check each dir for a modinfo.json file
|
||||
if path.is_dir() {
|
||||
let mut mod_info_path = path.clone();
|
||||
mod_info_path.push("modinfo.json");
|
||||
if mod_info_path.exists() {
|
||||
// Push path AND file contents into the hashmap using path as key
|
||||
mod_info_files.push(mod_info_path.to_str().unwrap().to_string());
|
||||
} else {
|
||||
// No modinfo, but we can still push a JSON obj with the folder name
|
||||
mod_info_strings.insert(
|
||||
path.to_str().unwrap().to_string(),
|
||||
format!(
|
||||
"{{ \"name\": \"{}\" }}",
|
||||
path.file_name().unwrap().to_str().unwrap()
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read each modinfo.json file
|
||||
for mod_info_file in mod_info_files {
|
||||
let mut mod_info_string = String::new();
|
||||
|
||||
// It is safe to unwrap here since we *know* that the file exists
|
||||
let mut file = std::fs::File::open(&mod_info_file).unwrap();
|
||||
file.read_to_string(&mut mod_info_string).unwrap();
|
||||
|
||||
// Push into hashmap using path as key
|
||||
mod_info_strings.insert(mod_info_file, mod_info_string);
|
||||
}
|
||||
|
||||
mod_info_strings
|
||||
}
|
||||
@@ -12,6 +12,7 @@ use sysinfo::{System, SystemExt};
|
||||
|
||||
mod downloader;
|
||||
mod file_helpers;
|
||||
mod gamebanana;
|
||||
mod lang;
|
||||
mod metadata_patcher;
|
||||
mod proxy;
|
||||
@@ -59,6 +60,9 @@ fn main() {
|
||||
lang::get_languages,
|
||||
web::valid_url,
|
||||
web::web_get,
|
||||
gamebanana::get_download_links,
|
||||
gamebanana::list_submissions,
|
||||
gamebanana::list_mods,
|
||||
metadata_patcher::patch_metadata
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
use duct::cmd;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn run_program(path: String) {
|
||||
// Open in new thread to prevent blocking.
|
||||
std::thread::spawn(move || {
|
||||
// Without unwrap_or, this can crash when UAC prompt is denied
|
||||
open::that(&path).unwrap_or(());
|
||||
});
|
||||
pub fn run_program(path: String, args: Option<String>) {
|
||||
// Without unwrap_or, this can crash when UAC prompt is denied
|
||||
open::that(format!("{} {}", &path, &args.unwrap_or("".into()))).unwrap_or(());
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn run_program_relative(path: String, args: Option<String>) {
|
||||
// Save the current working directory
|
||||
let cwd = std::env::current_dir().unwrap();
|
||||
|
||||
// Set the new working directory to the path before the executable
|
||||
let mut path_buf = std::path::PathBuf::from(&path);
|
||||
path_buf.pop();
|
||||
|
||||
// Set new working directory
|
||||
std::env::set_current_dir(&path_buf).unwrap();
|
||||
|
||||
println!("Opening {} {}", &path, args.clone().unwrap_or("".into()));
|
||||
|
||||
// Without unwrap_or, this can crash when UAC prompt is denied
|
||||
open::that(format!("{} {}", &path, args.unwrap_or("".into()))).unwrap_or(());
|
||||
|
||||
// Restore the original working directory
|
||||
std::env::set_current_dir(&cwd).unwrap();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -30,7 +48,13 @@ pub fn run_program_relative(path: String) {
|
||||
|
||||
#[tauri::command]
|
||||
pub fn run_command(program: &str, args: Vec<&str>) {
|
||||
cmd(program, args).run().expect("Failed to run command");
|
||||
let prog = program.to_string();
|
||||
let args = args.iter().map(|s| s.to_string()).collect::<Vec<String>>();
|
||||
|
||||
// Commands should not block (this is for the reshade injector mostly)
|
||||
std::thread::spawn(move || {
|
||||
cmd(prog, args).run().unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
use std::fs::File;
|
||||
use std::fs::{read_dir, File};
|
||||
use std::path;
|
||||
use std::thread;
|
||||
use unrar::archive::{Archive, OpenArchive};
|
||||
|
||||
#[tauri::command]
|
||||
pub fn unzip(window: tauri::Window, zipfile: String, destpath: String) {
|
||||
pub fn unzip(
|
||||
window: tauri::Window,
|
||||
zipfile: String,
|
||||
destpath: String,
|
||||
top_level: Option<bool>,
|
||||
folder_if_loose: Option<bool>,
|
||||
) {
|
||||
// Read file TODO: replace test file
|
||||
let f = match File::open(&zipfile) {
|
||||
Ok(f) => f,
|
||||
@@ -15,40 +22,80 @@ pub fn unzip(window: tauri::Window, zipfile: String, destpath: String) {
|
||||
|
||||
let write_path = path::PathBuf::from(&destpath);
|
||||
|
||||
// Get a list of all current directories
|
||||
let mut dirs = vec![];
|
||||
for entry in read_dir(&write_path).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let entry_path = entry.path();
|
||||
if entry_path.is_dir() {
|
||||
dirs.push(entry_path);
|
||||
}
|
||||
}
|
||||
|
||||
// Run extraction in seperate thread
|
||||
thread::spawn(move || {
|
||||
let full_path = write_path;
|
||||
let mut full_path = write_path.clone();
|
||||
|
||||
window.emit("extract_start", &zipfile).unwrap();
|
||||
|
||||
match zip_extract::extract(&f, &full_path, true) {
|
||||
Ok(_) => {
|
||||
println!(
|
||||
"Extracted zip file to: {}",
|
||||
full_path.to_str().unwrap_or("Error")
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to extract zip file: {}", e);
|
||||
let mut res_hash = std::collections::HashMap::new();
|
||||
if folder_if_loose.unwrap_or(false) {
|
||||
// Create a new folder with the same name as the zip file
|
||||
let mut file_name = path::Path::new(&zipfile)
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap();
|
||||
|
||||
res_hash.insert("error".to_string(), e.to_string());
|
||||
// remove ".zip" from the end of the file name
|
||||
file_name = &file_name[..file_name.len() - 4];
|
||||
|
||||
res_hash.insert("path".to_string(), zipfile.to_string());
|
||||
let new_path = full_path.join(file_name);
|
||||
match std::fs::create_dir_all(&new_path) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
println!("Failed to create directory: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
window.emit("download_error", &res_hash).unwrap();
|
||||
}
|
||||
};
|
||||
full_path = new_path.clone();
|
||||
}
|
||||
|
||||
// Get the name of the inenr file in the zip file
|
||||
let mut zip = zip::ZipArchive::new(&f).unwrap();
|
||||
let file = zip.by_index(0).unwrap();
|
||||
let name = file.name();
|
||||
println!("Is rar file? {}", zipfile.ends_with(".rar"));
|
||||
|
||||
let mut name = "".into();
|
||||
|
||||
// If file ends in zip, OR is unknown, extract as zip, otherwise extract as rar
|
||||
if zipfile.ends_with(".rar") {
|
||||
extract_rar(
|
||||
&window,
|
||||
&zipfile,
|
||||
&f,
|
||||
&full_path,
|
||||
top_level.unwrap_or(false),
|
||||
);
|
||||
|
||||
let archive = Archive::new(zipfile.clone());
|
||||
name = archive.list().unwrap().next().unwrap().unwrap().filename;
|
||||
} else {
|
||||
extract_zip(
|
||||
&window,
|
||||
&zipfile,
|
||||
&f,
|
||||
&full_path,
|
||||
top_level.unwrap_or(false),
|
||||
);
|
||||
|
||||
// Get the name of the inenr file in the zip file
|
||||
let mut zip = zip::ZipArchive::new(&f).unwrap();
|
||||
let file = zip.by_index(0).unwrap();
|
||||
name = file.name().to_string().clone();
|
||||
}
|
||||
|
||||
// If the contents is a jar file, emit that we have extracted a new jar file
|
||||
if name.ends_with(".jar") {
|
||||
window
|
||||
.emit("jar_extracted", destpath.to_string() + name)
|
||||
.emit("jar_extracted", destpath.to_string() + name.as_str())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -62,6 +109,80 @@ pub fn unzip(window: tauri::Window, zipfile: String, destpath: String) {
|
||||
}
|
||||
};
|
||||
|
||||
window.emit("extract_end", &zipfile).unwrap();
|
||||
// Get any new directory that could have been created
|
||||
let mut new_dir: String = String::new();
|
||||
for entry in read_dir(&write_path).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let entry_path = entry.path();
|
||||
if entry_path.is_dir() {
|
||||
if !dirs.contains(&entry_path) {
|
||||
new_dir = entry_path.to_str().unwrap().to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut res_hash = std::collections::HashMap::new();
|
||||
res_hash.insert("file", zipfile.to_string());
|
||||
res_hash.insert("new_folder", new_dir.to_string());
|
||||
|
||||
window.emit("extract_end", &res_hash).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
fn extract_rar(
|
||||
window: &tauri::Window,
|
||||
rarfile: &String,
|
||||
f: &File,
|
||||
full_path: &path::PathBuf,
|
||||
top_level: bool,
|
||||
) {
|
||||
let archive = Archive::new(rarfile.clone());
|
||||
|
||||
let mut open_archive = archive
|
||||
.extract_to(full_path.to_str().unwrap().to_string())
|
||||
.unwrap();
|
||||
|
||||
match open_archive.process() {
|
||||
Ok(_) => {
|
||||
println!(
|
||||
"Extracted rar file to: {}",
|
||||
full_path.to_str().unwrap_or("Error")
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to extract rar file: {}", e);
|
||||
let mut res_hash = std::collections::HashMap::new();
|
||||
|
||||
res_hash.insert("error".to_string(), e.to_string());
|
||||
res_hash.insert("path".to_string(), rarfile.to_string());
|
||||
|
||||
window.emit("download_error", &res_hash).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_zip(
|
||||
window: &tauri::Window,
|
||||
zipfile: &String,
|
||||
f: &File,
|
||||
full_path: &path::PathBuf,
|
||||
top_level: bool,
|
||||
) {
|
||||
match zip_extract::extract(f, full_path, top_level) {
|
||||
Ok(_) => {
|
||||
println!(
|
||||
"Extracted zip file to: {}",
|
||||
full_path.to_str().unwrap_or("Error")
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to extract zip file: {}", e);
|
||||
let mut res_hash = std::collections::HashMap::new();
|
||||
|
||||
res_hash.insert("error".to_string(), e.to_string());
|
||||
res_hash.insert("path".to_string(), zipfile.to_string());
|
||||
|
||||
window.emit("download_error", &res_hash).unwrap();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use reqwest::header::USER_AGENT;
|
||||
use reqwest::header::{CONTENT_TYPE, USER_AGENT};
|
||||
|
||||
pub(crate) async fn query(site: &str) -> String {
|
||||
let client = reqwest::Client::new();
|
||||
@@ -6,6 +6,7 @@ pub(crate) async fn query(site: &str) -> String {
|
||||
let response = client
|
||||
.get(site)
|
||||
.header(USER_AGENT, "cultivation")
|
||||
.header(CONTENT_TYPE, "application/json")
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
10
src/resources/icons/back.svg
Normal file
10
src/resources/icons/back.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
|
||||
<desc>Created with Fabric.js 1.7.22</desc>
|
||||
<defs>
|
||||
</defs>
|
||||
<g transform="translate(128 128) scale(0.72 0.72)" style="">
|
||||
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(-175.05 -175.05) scale(3.89 3.89)" >
|
||||
<path d="M 45 0 c 24.813 0 45 20.187 45 45 c 0 24.813 -20.187 45 -45 45 C 20.186 90 0 69.813 0 45 C 0 20.187 20.186 0 45 0 z M 51.263 73.4 l 8.6 -8.6 L 40.064 45 l 19.799 -19.799 l -8.6 -8.6 L 22.864 45 L 51.263 73.4 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1002 B |
11
src/resources/icons/eye.svg
Normal file
11
src/resources/icons/eye.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
|
||||
<desc>Created with Fabric.js 1.7.22</desc>
|
||||
<defs>
|
||||
</defs>
|
||||
<g transform="translate(128 128) scale(0.72 0.72)" style="">
|
||||
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(-175.05 -175.05) scale(3.89 3.89)" >
|
||||
<path d="M 89.307 43.082 C 74.775 25.601 59.868 16.737 45 16.737 c -14.869 0 -29.775 8.864 -44.307 26.345 c -0.924 1.112 -0.924 2.724 0 3.836 C 15.225 64.399 30.131 73.264 45 73.264 c 14.868 0 29.775 -8.864 44.307 -26.346 C 90.231 45.806 90.231 44.194 89.307 43.082 z M 45 62 c -9.374 0 -17 -7.626 -17 -17 s 7.626 -17 17 -17 s 17 7.626 17 17 S 54.374 62 45 62 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||
<circle cx="45" cy="45" r="9" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
11
src/resources/icons/like.svg
Normal file
11
src/resources/icons/like.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
|
||||
<desc>Created with Fabric.js 1.7.22</desc>
|
||||
<defs>
|
||||
</defs>
|
||||
<g transform="translate(128 128) scale(0.72 0.72)" style="">
|
||||
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(-175.05 -175.05) scale(3.89 3.89)" >
|
||||
<path d="M 0 87.201 h 18.478 c 1.44 0 2.607 -1.167 2.607 -2.607 V 35.343 c 0 -1.44 -1.167 -2.607 -2.607 -2.607 H 0 L 0 87.201 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||
<path d="M 83.186 46.32 c 3.763 0 6.814 -3.051 6.814 -6.814 c 0 -3.763 -3.051 -6.814 -6.814 -6.814 H 61.591 c 3.758 -6.768 3.872 -27.328 -5.046 -29.797 c -1.689 -0.468 -3.365 0.823 -3.554 2.565 c -1.568 14.428 -10.395 32.362 -19.37 32.881 h -6.991 v 43.003 h 3.627 c 1.952 0 3.817 0.666 5.444 1.743 c 4.063 2.691 10.906 4.265 17.465 4.101 h 3.172 v 0.012 h 21.788 c 3.763 0 6.814 -3.051 6.814 -6.814 c 0 -3.763 -3.051 -6.814 -6.814 -6.814 h 3.037 c 3.763 0 6.814 -3.051 6.814 -6.814 c 0 -3.763 -3.051 -6.814 -6.814 -6.814 h 2.025 c 3.763 0 6.814 -3.051 6.814 -6.814 S 86.949 46.32 83.186 46.32 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
11
src/resources/icons/plus.svg
Normal file
11
src/resources/icons/plus.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
|
||||
<desc>Created with Fabric.js 1.7.22</desc>
|
||||
<defs>
|
||||
</defs>
|
||||
<g transform="translate(128 128) scale(0.72 0.72)" style="">
|
||||
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(-175.05 -175.05) scale(3.89 3.89)" >
|
||||
<path d="M 58.921 90 H 31.079 c -1.155 0 -2.092 -0.936 -2.092 -2.092 V 2.092 C 28.988 0.936 29.924 0 31.079 0 h 27.841 c 1.155 0 2.092 0.936 2.092 2.092 v 85.817 C 61.012 89.064 60.076 90 58.921 90 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||
<path d="M 90 31.079 v 27.841 c 0 1.155 -0.936 2.092 -2.092 2.092 H 2.092 C 0.936 61.012 0 60.076 0 58.921 V 31.079 c 0 -1.155 0.936 -2.092 2.092 -2.092 h 85.817 C 89.064 28.988 90 29.924 90 31.079 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
10
src/resources/icons/wrench.svg
Normal file
10
src/resources/icons/wrench.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
|
||||
<desc>Created with Fabric.js 1.7.22</desc>
|
||||
<defs>
|
||||
</defs>
|
||||
<g transform="translate(128 128) scale(0.72 0.72)" style="">
|
||||
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(-175.05 -175.05) scale(3.89 3.89)" >
|
||||
<path d="M 87.12 73.212 L 48.774 34.866 c 3.723 -9.153 1.873 -20.042 -5.553 -27.468 c -7.008 -7.008 -17.094 -9.025 -25.9 -6.104 L 28.96 12.934 c 4.387 4.387 4.833 11.594 0.579 16.112 c -4.402 4.674 -11.761 4.757 -16.268 0.25 L 1.295 17.32 c -2.922 8.807 -0.904 18.892 6.104 25.9 c 7.426 7.426 18.315 9.276 27.468 5.553 L 73.212 87.12 c 3.84 3.84 10.067 3.84 13.908 0 l 0 0 C 90.96 83.279 90.96 77.052 87.12 73.212 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -35,12 +35,29 @@ select:focus {
|
||||
.TopButton {
|
||||
height: 60%;
|
||||
margin: 0px 10px;
|
||||
|
||||
color: #c5c5c5;
|
||||
transition: color 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.TopButton span {
|
||||
display: inline-block;
|
||||
line-height: normal;
|
||||
border-bottom: 1px solid #c5c5c5;
|
||||
}
|
||||
|
||||
.TopButton img {
|
||||
filter: invert(95%) sepia(0%) saturate(18%) hue-rotate(153deg) brightness(88%) contrast(81%);
|
||||
|
||||
transition: filter 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.TopButton:hover {
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.TopButton:hover img {
|
||||
filter: invert(100%) sepia(0%) saturate(18%) hue-rotate(153deg) brightness(100%) contrast(100%);
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -97,6 +114,9 @@ select:focus {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ExtrasMenu {
|
||||
}
|
||||
|
||||
@media (max-height: 580px) {
|
||||
.BottomSection {
|
||||
height: 150px;
|
||||
|
||||
178
src/ui/App.tsx
178
src/ui/App.tsx
@@ -1,101 +1,33 @@
|
||||
import React from 'react'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import './App.css'
|
||||
|
||||
import DownloadHandler from '../utils/download'
|
||||
|
||||
// Major Components
|
||||
import TopBar from './components/TopBar'
|
||||
import ServerLaunchSection from './components/ServerLaunchSection'
|
||||
import MainProgressBar from './components/common/MainProgressBar'
|
||||
import Options from './components/menu/Options'
|
||||
import MiniDialog from './components/MiniDialog'
|
||||
import DownloadList from './components/common/DownloadList'
|
||||
import Downloads from './components/menu/Downloads'
|
||||
import NewsSection from './components/news/NewsSection'
|
||||
import Game from './components/menu/Game'
|
||||
|
||||
import RightBar from './components/RightBar'
|
||||
import { getConfigOption, setConfigOption } from '../utils/configuration'
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { dataDir } from '@tauri-apps/api/path'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri'
|
||||
import { getConfigOption } from '../utils/configuration'
|
||||
import { getTheme, loadTheme } from '../utils/themes'
|
||||
import { unpatchGame } from '../utils/metadata'
|
||||
|
||||
interface IProps {
|
||||
[key: string]: never
|
||||
}
|
||||
import { convertFileSrc, invoke } from '@tauri-apps/api/tauri'
|
||||
import { dataDir } from '@tauri-apps/api/path'
|
||||
import { Main } from './Main'
|
||||
import { Mods } from './Mods'
|
||||
|
||||
interface IState {
|
||||
isDownloading: boolean
|
||||
optionsOpen: boolean
|
||||
miniDownloadsOpen: boolean
|
||||
downloadsOpen: boolean
|
||||
gameDownloadsOpen: boolean
|
||||
page: string
|
||||
bgFile: string
|
||||
}
|
||||
|
||||
const downloadHandler = new DownloadHandler()
|
||||
const DEFAULT_BG = 'https://api.grasscutter.io/cultivation/bgfile'
|
||||
|
||||
const downloadHandler = new DownloadHandler()
|
||||
|
||||
class App extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
class App extends React.Component<Readonly<unknown>, IState> {
|
||||
constructor(props: Readonly<unknown>) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isDownloading: false,
|
||||
optionsOpen: false,
|
||||
miniDownloadsOpen: false,
|
||||
downloadsOpen: false,
|
||||
gameDownloadsOpen: false,
|
||||
page: 'main',
|
||||
bgFile: DEFAULT_BG,
|
||||
}
|
||||
|
||||
listen('lang_error', (payload) => {
|
||||
console.log(payload)
|
||||
})
|
||||
|
||||
listen('jar_extracted', ({ payload }: { payload: string }) => {
|
||||
setConfigOption('grasscutter_path', payload)
|
||||
})
|
||||
|
||||
// Emitted for metadata replacing-purposes
|
||||
listen('game_closed', async () => {
|
||||
const wasPatched = await getConfigOption('patch_metadata')
|
||||
|
||||
if (wasPatched) {
|
||||
const unpatched = await unpatchGame()
|
||||
|
||||
console.log(`unpatched game? ${unpatched}`)
|
||||
|
||||
if (!unpatched) {
|
||||
alert(
|
||||
`Could not unpatch game! (You should be able to find your metadata backup in ${await dataDir()}\\cultivation\\)`
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let min = false
|
||||
|
||||
// periodically check if we need to min/max based on whether the game is open
|
||||
setInterval(async () => {
|
||||
const gameOpen = await invoke('is_game_running')
|
||||
|
||||
if (gameOpen && !min) {
|
||||
appWindow.minimize()
|
||||
min = true
|
||||
} else if (!gameOpen && min) {
|
||||
appWindow.unminimize()
|
||||
min = false
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const cert_generated = await getConfigOption('cert_generated')
|
||||
const game_exe = await getConfigOption('game_install_path')
|
||||
const game_path = game_exe?.substring(0, game_exe.replace(/\\/g, '/').lastIndexOf('/')) || ''
|
||||
const root_path = game_path?.substring(0, game_path.replace(/\\/g, '/').lastIndexOf('/')) || ''
|
||||
@@ -155,21 +87,12 @@ class App extends React.Component<IProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
if (!cert_generated) {
|
||||
// Generate the certificate
|
||||
await invoke('generate_ca_files', {
|
||||
path: (await dataDir()) + 'cultivation',
|
||||
})
|
||||
|
||||
await setConfigOption('cert_generated', true)
|
||||
}
|
||||
|
||||
// Period check to only show progress bar when downloading files
|
||||
setInterval(() => {
|
||||
window.addEventListener('changePage', (e) => {
|
||||
this.setState({
|
||||
isDownloading: downloadHandler.getDownloads().filter((d) => d.status !== 'finished')?.length > 0,
|
||||
// @ts-expect-error - TS doesn't like our custom event
|
||||
page: e.detail,
|
||||
})
|
||||
}, 1000)
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -184,69 +107,14 @@ class App extends React.Component<IProps, IState> {
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<TopBar
|
||||
optFunc={() => {
|
||||
this.setState({ optionsOpen: !this.state.optionsOpen })
|
||||
}}
|
||||
downFunc={() => this.setState({ downloadsOpen: !this.state.downloadsOpen })}
|
||||
gameFunc={() => this.setState({ gameDownloadsOpen: !this.state.gameDownloadsOpen })}
|
||||
/>
|
||||
|
||||
<RightBar />
|
||||
|
||||
<NewsSection />
|
||||
|
||||
{
|
||||
// Mini downloads section
|
||||
this.state.miniDownloadsOpen ? (
|
||||
<div className="MiniDownloads" id="miniDownloadContainer">
|
||||
<MiniDialog
|
||||
title="Downloads"
|
||||
closeFn={() => {
|
||||
this.setState({ miniDownloadsOpen: false })
|
||||
}}
|
||||
>
|
||||
<DownloadList downloadManager={downloadHandler} />
|
||||
</MiniDialog>
|
||||
<div className="arrow-down"></div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
{
|
||||
// Download menu
|
||||
this.state.downloadsOpen ? (
|
||||
<Downloads downloadManager={downloadHandler} closeFn={() => this.setState({ downloadsOpen: false })} />
|
||||
) : null
|
||||
}
|
||||
|
||||
{
|
||||
// Options menu
|
||||
this.state.optionsOpen ? (
|
||||
<Options
|
||||
downloadManager={downloadHandler}
|
||||
closeFn={() => this.setState({ optionsOpen: !this.state.optionsOpen })}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
{
|
||||
// Game downloads menu
|
||||
this.state.gameDownloadsOpen ? (
|
||||
<Game downloadManager={downloadHandler} closeFn={() => this.setState({ gameDownloadsOpen: false })} />
|
||||
) : null
|
||||
}
|
||||
|
||||
<div className="BottomSection" id="bottomSectionContainer">
|
||||
<ServerLaunchSection />
|
||||
|
||||
<div
|
||||
id="DownloadProgress"
|
||||
onClick={() => this.setState({ miniDownloadsOpen: !this.state.miniDownloadsOpen })}
|
||||
>
|
||||
{this.state.isDownloading ? <MainProgressBar downloadManager={downloadHandler} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
{(() => {
|
||||
switch (this.state.page) {
|
||||
case 'modding':
|
||||
return <Mods downloadHandler={downloadHandler} />
|
||||
default:
|
||||
return <Main downloadHandler={downloadHandler} />
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class Debug extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="App">
|
||||
<TopBar optFunc={none} downFunc={none} gameFunc={none} />
|
||||
<TopBar />
|
||||
<TextInput readOnly={false} initalValue={'change to set proxy address'} onChange={setProxyAddress} />
|
||||
<button onClick={startProxy}>start proxy</button>
|
||||
<button onClick={stopProxy}>stop proxy</button>
|
||||
|
||||
242
src/ui/Main.tsx
Normal file
242
src/ui/Main.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
import React from 'react'
|
||||
|
||||
// Major Components
|
||||
import TopBar from './components/TopBar'
|
||||
import ServerLaunchSection from './components/ServerLaunchSection'
|
||||
import MainProgressBar from './components/common/MainProgressBar'
|
||||
import Options from './components/menu/Options'
|
||||
import MiniDialog from './components/MiniDialog'
|
||||
import DownloadList from './components/common/DownloadList'
|
||||
import Downloads from './components/menu/Downloads'
|
||||
import NewsSection from './components/news/NewsSection'
|
||||
import Game from './components/menu/Game'
|
||||
import RightBar from './components/RightBar'
|
||||
|
||||
import { getConfigOption, setConfigOption } from '../utils/configuration'
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { dataDir } from '@tauri-apps/api/path'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
import { unpatchGame } from '../utils/metadata'
|
||||
import DownloadHandler from '../utils/download'
|
||||
|
||||
// Graphics
|
||||
import cogBtn from '../resources/icons/cog.svg'
|
||||
import downBtn from '../resources/icons/download.svg'
|
||||
import wrenchBtn from '../resources/icons/wrench.svg'
|
||||
import Menu from './components/menu/Menu'
|
||||
import { ExtrasMenu } from './components/menu/ExtrasMenu'
|
||||
|
||||
interface IProps {
|
||||
downloadHandler: DownloadHandler
|
||||
}
|
||||
|
||||
interface IState {
|
||||
isDownloading: boolean
|
||||
optionsOpen: boolean
|
||||
miniDownloadsOpen: boolean
|
||||
downloadsOpen: boolean
|
||||
gameDownloadsOpen: boolean
|
||||
extrasOpen: boolean
|
||||
migotoSet: boolean
|
||||
playGame: (exe?: string, proc_name?: string) => void
|
||||
}
|
||||
|
||||
export class Main extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isDownloading: false,
|
||||
optionsOpen: false,
|
||||
miniDownloadsOpen: false,
|
||||
downloadsOpen: false,
|
||||
gameDownloadsOpen: false,
|
||||
extrasOpen: false,
|
||||
migotoSet: false,
|
||||
playGame: () => {
|
||||
alert('Error launching game')
|
||||
},
|
||||
}
|
||||
|
||||
listen('lang_error', (payload) => {
|
||||
console.log(payload)
|
||||
})
|
||||
|
||||
listen('jar_extracted', ({ payload }: { payload: string }) => {
|
||||
setConfigOption('grasscutter_path', payload)
|
||||
})
|
||||
|
||||
// Emitted for metadata replacing-purposes
|
||||
listen('game_closed', async () => {
|
||||
const wasPatched = await getConfigOption('patch_metadata')
|
||||
|
||||
if (wasPatched) {
|
||||
const unpatched = await unpatchGame()
|
||||
|
||||
if (!unpatched) {
|
||||
alert(
|
||||
`Could not unpatch game! (You should be able to find your metadata backup in ${await dataDir()}\\cultivation\\)`
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let min = false
|
||||
|
||||
// periodically check if we need to min/max based on whether the game is open
|
||||
setInterval(async () => {
|
||||
const gameOpen = await invoke('is_game_running')
|
||||
|
||||
if (gameOpen && !min) {
|
||||
appWindow.minimize()
|
||||
min = true
|
||||
} else if (!gameOpen && min) {
|
||||
appWindow.unminimize()
|
||||
min = false
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
this.openExtrasMenu = this.openExtrasMenu.bind(this)
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const cert_generated = await getConfigOption('cert_generated')
|
||||
|
||||
this.setState({
|
||||
migotoSet: !!(await getConfigOption('migoto_path')),
|
||||
})
|
||||
|
||||
if (!cert_generated) {
|
||||
// Generate the certificate
|
||||
await invoke('generate_ca_files', {
|
||||
path: (await dataDir()) + 'cultivation',
|
||||
})
|
||||
|
||||
await setConfigOption('cert_generated', true)
|
||||
}
|
||||
|
||||
// Period check to only show progress bar when downloading files
|
||||
setInterval(() => {
|
||||
this.setState({
|
||||
isDownloading: this.props.downloadHandler.getDownloads().filter((d) => d.status !== 'finished')?.length > 0,
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
async openExtrasMenu(playGame: () => void) {
|
||||
this.setState({
|
||||
extrasOpen: true,
|
||||
playGame,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<TopBar>
|
||||
<div
|
||||
id="settingsBtn"
|
||||
onClick={() => this.setState({ optionsOpen: !this.state.optionsOpen })}
|
||||
className="TopButton"
|
||||
>
|
||||
<img src={cogBtn} alt="settings" />
|
||||
</div>
|
||||
<div
|
||||
id="downloadsBtn"
|
||||
className="TopButton"
|
||||
onClick={() => this.setState({ downloadsOpen: !this.state.downloadsOpen })}
|
||||
>
|
||||
<img src={downBtn} alt="downloads" />
|
||||
</div>
|
||||
{this.state.migotoSet && (
|
||||
<div
|
||||
id="modsBtn"
|
||||
onClick={() => {
|
||||
// Create and dispatch a custom "openMods" event
|
||||
const event = new CustomEvent('changePage', { detail: 'modding' })
|
||||
window.dispatchEvent(event)
|
||||
}}
|
||||
className="TopButton"
|
||||
>
|
||||
<img src={wrenchBtn} alt="mods" />
|
||||
</div>
|
||||
)}
|
||||
{/* <div id="gameBtn" className="TopButton" onClick={() => this.setState({ gameDownloadsOpen: !this.state.gameDownloadsOpen })}>
|
||||
<img src={gameBtn} alt="game" />
|
||||
</div> */}
|
||||
</TopBar>
|
||||
|
||||
<RightBar />
|
||||
|
||||
<NewsSection />
|
||||
|
||||
{
|
||||
// Extras section
|
||||
this.state.extrasOpen && (
|
||||
<ExtrasMenu closeFn={() => this.setState({ extrasOpen: false })} playGame={this.state.playGame}>
|
||||
Yo
|
||||
</ExtrasMenu>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
// Mini downloads section
|
||||
this.state.miniDownloadsOpen ? (
|
||||
<div className="MiniDownloads" id="miniDownloadContainer">
|
||||
<MiniDialog
|
||||
title="Downloads"
|
||||
closeFn={() => {
|
||||
this.setState({ miniDownloadsOpen: false })
|
||||
}}
|
||||
>
|
||||
<DownloadList downloadManager={this.props.downloadHandler} />
|
||||
</MiniDialog>
|
||||
<div className="arrow-down"></div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
{
|
||||
// Download menu
|
||||
this.state.downloadsOpen ? (
|
||||
<Downloads
|
||||
downloadManager={this.props.downloadHandler}
|
||||
closeFn={() => this.setState({ downloadsOpen: false })}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
{
|
||||
// Options menu
|
||||
this.state.optionsOpen ? (
|
||||
<Options
|
||||
downloadManager={this.props.downloadHandler}
|
||||
closeFn={() => this.setState({ optionsOpen: !this.state.optionsOpen })}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
{
|
||||
// Game downloads menu
|
||||
this.state.gameDownloadsOpen ? (
|
||||
<Game
|
||||
downloadManager={this.props.downloadHandler}
|
||||
closeFn={() => this.setState({ gameDownloadsOpen: false })}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
<div className="BottomSection" id="bottomSectionContainer">
|
||||
<ServerLaunchSection openExtras={this.openExtrasMenu} />
|
||||
|
||||
<div
|
||||
id="DownloadProgress"
|
||||
onClick={() => this.setState({ miniDownloadsOpen: !this.state.miniDownloadsOpen })}
|
||||
>
|
||||
{this.state.isDownloading ? <MainProgressBar downloadManager={this.props.downloadHandler} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
41
src/ui/Mods.css
Normal file
41
src/ui/Mods.css
Normal file
@@ -0,0 +1,41 @@
|
||||
.Mods {
|
||||
backdrop-filter: blur(10px);
|
||||
height: 90%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Stuff for the top bar progress bar */
|
||||
.TopDownloads {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 35.5%;
|
||||
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.TopDownloads .ProgressBar {
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.ModMenu {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.ModMenu .BigButton {
|
||||
font-size: 16px;
|
||||
padding: 6px 30px;
|
||||
}
|
||||
|
||||
.ModDownloadList {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.ModDownloadItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
margin: 10px;
|
||||
}
|
||||
169
src/ui/Mods.tsx
Normal file
169
src/ui/Mods.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import React from 'react'
|
||||
import DownloadHandler from '../utils/download'
|
||||
import { getModDownload, ModData } from '../utils/gamebanana'
|
||||
import { getModsFolder } from '../utils/mods'
|
||||
import { unzip } from '../utils/zipUtils'
|
||||
import ProgressBar from './components/common/MainProgressBar'
|
||||
import { ModHeader } from './components/mods/ModHeader'
|
||||
import { ModList } from './components/mods/ModList'
|
||||
import TopBar from './components/TopBar'
|
||||
|
||||
import './Mods.css'
|
||||
import Back from '../resources/icons/back.svg'
|
||||
import Menu from './components/menu/Menu'
|
||||
import BigButton from './components/common/BigButton'
|
||||
import Tr from '../utils/language'
|
||||
|
||||
interface IProps {
|
||||
downloadHandler: DownloadHandler
|
||||
}
|
||||
|
||||
interface IState {
|
||||
isDownloading: boolean
|
||||
category: string
|
||||
downloadList: { name: string; url: string; mod: ModData }[] | null
|
||||
}
|
||||
|
||||
const headers = [
|
||||
{
|
||||
name: 'ripe',
|
||||
title: 'Hot',
|
||||
},
|
||||
{
|
||||
name: 'new',
|
||||
title: 'New',
|
||||
},
|
||||
{
|
||||
name: 'installed',
|
||||
title: 'Installed',
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* Mods currently install into folder labelled with their GB ID
|
||||
*
|
||||
* @TODO Categorizaiton/sorting (by likes, views, etc)
|
||||
*/
|
||||
export class Mods extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isDownloading: false,
|
||||
category: '',
|
||||
downloadList: null,
|
||||
}
|
||||
|
||||
this.setCategory = this.setCategory.bind(this)
|
||||
this.addDownload = this.addDownload.bind(this)
|
||||
}
|
||||
|
||||
async addDownload(mod: ModData) {
|
||||
const dlLinks = await getModDownload(String(mod.id))
|
||||
|
||||
// Not gonna bother allowing sorting for now
|
||||
const firstLink = dlLinks[0].downloadUrl
|
||||
const fileExt = firstLink.split('.').pop()
|
||||
|
||||
const modName = `${mod.id}.${fileExt}`
|
||||
|
||||
if (dlLinks.length === 0) return
|
||||
|
||||
// If there is one download we don't care to choose
|
||||
if (dlLinks.length === 1) {
|
||||
this.downloadMod(firstLink, modName, mod)
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
downloadList: dlLinks.map((link) => ({
|
||||
name: link.filename,
|
||||
url: link.downloadUrl,
|
||||
mod: mod,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
async downloadMod(link: string, modName: string, mod: ModData) {
|
||||
const modFolder = await getModsFolder()
|
||||
const path = `${modFolder}/${modName}`
|
||||
|
||||
if (!modFolder) return
|
||||
|
||||
this.props.downloadHandler.addDownload(link, path, async () => {
|
||||
const unzipRes = await unzip(path, modFolder, false, true)
|
||||
|
||||
// Write a modinfo.json file
|
||||
invoke('write_file', {
|
||||
path: `${unzipRes.new_folder}/modinfo.json`,
|
||||
contents: JSON.stringify(mod),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async setCategory(value: string) {
|
||||
this.setState(
|
||||
{
|
||||
category: value,
|
||||
},
|
||||
this.forceUpdate
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="Mods">
|
||||
<TopBar>
|
||||
<div
|
||||
id="backbtn"
|
||||
className="TopButton"
|
||||
onClick={() => {
|
||||
// Create and dispatch a custom "changePage" event
|
||||
const event = new CustomEvent('changePage', { detail: 'main' })
|
||||
window.dispatchEvent(event)
|
||||
}}
|
||||
>
|
||||
<img src={Back} alt="back" />
|
||||
</div>
|
||||
</TopBar>
|
||||
|
||||
{this.state.downloadList && (
|
||||
<Menu className="ModMenu" heading="Links" closeFn={() => this.setState({ downloadList: null })}>
|
||||
<div className="ModDownloadList">
|
||||
{this.state.downloadList.map((o) => {
|
||||
return (
|
||||
<div className="ModDownloadItem" key={o.name}>
|
||||
<div className="ModDownloadName">{o.name}</div>
|
||||
<BigButton
|
||||
id={o.url}
|
||||
onClick={() => {
|
||||
const fileExt = o.url.split('.').pop()
|
||||
const modName = `${o.mod.id}.${fileExt}`
|
||||
|
||||
this.downloadMod(o.url, modName, o.mod)
|
||||
this.setState({
|
||||
downloadList: null,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Tr text="components.download" />
|
||||
</BigButton>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Menu>
|
||||
)}
|
||||
|
||||
<div className="TopDownloads">
|
||||
<ProgressBar downloadManager={this.props.downloadHandler} withStats={false} />
|
||||
</div>
|
||||
|
||||
<ModHeader onChange={this.setCategory} headers={headers} defaultHeader={'ripe'} />
|
||||
|
||||
<ModList key={this.state.category} mode={this.state.category} addDownload={this.addDownload} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,12 @@
|
||||
width: 5%;
|
||||
}
|
||||
|
||||
.AkebiIcon,
|
||||
#ExtrasMenuButton {
|
||||
width: 5%;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.ExtrasIcon,
|
||||
.ServerIcon {
|
||||
height: 20px;
|
||||
filter: invert(28%) sepia(28%) saturate(1141%) hue-rotate(352deg) brightness(96%) contrast(88%);
|
||||
|
||||
@@ -8,13 +8,17 @@ import { translate } from '../../utils/language'
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
|
||||
import Server from '../../resources/icons/server.svg'
|
||||
import Akebi from '../../resources/icons/akebi.svg'
|
||||
import Plus from '../../resources/icons/plus.svg'
|
||||
|
||||
import './ServerLaunchSection.css'
|
||||
import { dataDir } from '@tauri-apps/api/path'
|
||||
import { getGameExecutable } from '../../utils/game'
|
||||
import { patchGame, unpatchGame } from '../../utils/metadata'
|
||||
|
||||
interface IProps {
|
||||
openExtras: (playGame: () => void) => void
|
||||
}
|
||||
|
||||
interface IState {
|
||||
grasscutterEnabled: boolean
|
||||
buttonLabel: string
|
||||
@@ -31,10 +35,12 @@ interface IState {
|
||||
httpsEnabled: boolean
|
||||
|
||||
swag: boolean
|
||||
akebiSet: boolean
|
||||
migotoSet: boolean
|
||||
}
|
||||
|
||||
export default class ServerLaunchSection extends React.Component<{}, IState> {
|
||||
constructor(props: {}) {
|
||||
export default class ServerLaunchSection extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
@@ -49,11 +55,12 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
|
||||
httpsLabel: '',
|
||||
httpsEnabled: false,
|
||||
swag: false,
|
||||
akebiSet: false,
|
||||
migotoSet: false,
|
||||
}
|
||||
|
||||
this.toggleGrasscutter = this.toggleGrasscutter.bind(this)
|
||||
this.playGame = this.playGame.bind(this)
|
||||
this.launchAkebi = this.launchAkebi.bind(this)
|
||||
this.setIp = this.setIp.bind(this)
|
||||
this.setPort = this.setPort.bind(this)
|
||||
this.toggleHttps = this.toggleHttps.bind(this)
|
||||
@@ -74,6 +81,8 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
|
||||
httpsLabel: await translate('main.https_enable'),
|
||||
httpsEnabled: config.https_enabled || false,
|
||||
swag: config.swag_mode || false,
|
||||
akebiSet: config.akebi_path !== '',
|
||||
migotoSet: config.migoto_path !== '',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -182,16 +191,6 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
|
||||
})
|
||||
}
|
||||
|
||||
async launchAkebi() {
|
||||
const config = await getConfig()
|
||||
|
||||
// Get game exe from game path, so we can watch it
|
||||
const pathArr = config.game_install_path.replace(/\\/g, '/').split('/')
|
||||
const gameExec = pathArr[pathArr.length - 1]
|
||||
|
||||
await this.playGame(config.akebi_path, gameExec)
|
||||
}
|
||||
|
||||
setIp(text: string) {
|
||||
this.setState({
|
||||
ip: text,
|
||||
@@ -265,11 +264,9 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
|
||||
{this.state.buttonLabel}
|
||||
</BigButton>
|
||||
{this.state.swag && (
|
||||
<>
|
||||
<BigButton onClick={this.launchAkebi} id="akebiLaunch">
|
||||
<img className="AkebiIcon" id="akebiIcon" src={Akebi} />
|
||||
</BigButton>
|
||||
</>
|
||||
<BigButton onClick={() => this.props.openExtras(this.playGame)} id="ExtrasMenuButton">
|
||||
<img className="ExtrasIcon" id="extrasIcon" src={Plus} />
|
||||
</BigButton>
|
||||
)}
|
||||
<BigButton onClick={this.launchServer} id="serverLaunch">
|
||||
<img className="ServerIcon" id="serverLaunchIcon" src={Server} />
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import React from 'react'
|
||||
import { app } from '@tauri-apps/api'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
import closeIcon from '../../resources/icons/close.svg'
|
||||
import minIcon from '../../resources/icons/min.svg'
|
||||
import cogBtn from '../../resources/icons/cog.svg'
|
||||
import downBtn from '../../resources/icons/download.svg'
|
||||
|
||||
import { getConfig, setConfigOption } from '../../utils/configuration'
|
||||
import Tr from '../../utils/language'
|
||||
|
||||
import './TopBar.css'
|
||||
import { getConfig, setConfigOption } from '../../utils/configuration'
|
||||
import closeIcon from '../../resources/icons/close.svg'
|
||||
import minIcon from '../../resources/icons/min.svg'
|
||||
|
||||
interface IProps {
|
||||
optFunc: () => void
|
||||
downFunc: () => void
|
||||
gameFunc: () => void
|
||||
children?: React.ReactNode | React.ReactNode[]
|
||||
}
|
||||
|
||||
interface IState {
|
||||
@@ -110,15 +105,7 @@ export default class TopBar extends React.Component<IProps, IState> {
|
||||
<div id="minBtn" onClick={this.handleMinimize} className="TopButton">
|
||||
<img src={minIcon} alt="minimize" />
|
||||
</div>
|
||||
<div id="settingsBtn" onClick={this.props.optFunc} className="TopButton">
|
||||
<img src={cogBtn} alt="settings" />
|
||||
</div>
|
||||
<div id="downloadsBtn" className="TopButton" onClick={this.props.downFunc}>
|
||||
<img src={downBtn} alt="downloads" />
|
||||
</div>
|
||||
{/* <div id="gameBtn" className="TopButton" onClick={this.props.gameFunc}>
|
||||
<img src={gameBtn} alt="game" />
|
||||
</div> */}
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ interface IProps {
|
||||
label?: string
|
||||
checked: boolean
|
||||
onChange: () => void
|
||||
id: string
|
||||
id?: string
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
||||
@@ -68,7 +68,6 @@ export default class DirInput extends React.Component<IProps, IState> {
|
||||
directory: true,
|
||||
})
|
||||
} else {
|
||||
console.log(this.props.openFolder)
|
||||
path = await open({
|
||||
filters: [{ name: 'Files', extensions: this.props.extensions || ['*'] }],
|
||||
defaultPath: this.props.openFolder,
|
||||
|
||||
@@ -5,6 +5,7 @@ import './ProgressBar.css'
|
||||
|
||||
interface IProps {
|
||||
downloadManager: DownloadHandler
|
||||
withStats?: boolean
|
||||
}
|
||||
|
||||
interface IState {
|
||||
@@ -70,11 +71,13 @@ export default class ProgressBar extends React.Component<IProps, IState> {
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div className="MainProgressText">
|
||||
<Tr text="main.files_downloading" /> {this.state.files} ({this.state.speed})
|
||||
<br />
|
||||
<Tr text="main.files_extracting" /> {this.state.extracting}
|
||||
</div>
|
||||
{(this.props.withStats === undefined || this.props.withStats) && (
|
||||
<div className="MainProgressText">
|
||||
<Tr text="main.files_downloading" /> {this.state.files} ({this.state.speed})
|
||||
<br />
|
||||
<Tr text="main.files_extracting" /> {this.state.extracting}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -111,8 +111,9 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
|
||||
async downloadGrasscutterStableRepo() {
|
||||
const folder = await this.getGrasscutterFolder()
|
||||
this.props.downloadManager.addDownload(STABLE_REPO_DOWNLOAD, folder + '\\grasscutter_repo.zip', () => {
|
||||
unzip(folder + '\\grasscutter_repo.zip', folder + '\\', this.toggleButtons)
|
||||
this.props.downloadManager.addDownload(STABLE_REPO_DOWNLOAD, folder + '\\grasscutter_repo.zip', async () => {
|
||||
await unzip(folder + '\\grasscutter_repo.zip', folder + '\\', true)
|
||||
this.toggleButtons()
|
||||
})
|
||||
|
||||
this.toggleButtons()
|
||||
@@ -120,8 +121,9 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
|
||||
async downloadGrasscutterDevRepo() {
|
||||
const folder = await this.getGrasscutterFolder()
|
||||
this.props.downloadManager.addDownload(DEV_REPO_DOWNLOAD, folder + '\\grasscutter_repo.zip', () => {
|
||||
unzip(folder + '\\grasscutter_repo.zip', folder + '\\', this.toggleButtons)
|
||||
this.props.downloadManager.addDownload(DEV_REPO_DOWNLOAD, folder + '\\grasscutter_repo.zip', async () => {
|
||||
await unzip(folder + '\\grasscutter_repo.zip', folder + '\\', true)
|
||||
this.toggleButtons()
|
||||
})
|
||||
|
||||
this.toggleButtons()
|
||||
@@ -129,8 +131,9 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
|
||||
async downloadGrasscutterStable() {
|
||||
const folder = await this.getGrasscutterFolder()
|
||||
this.props.downloadManager.addDownload(STABLE_DOWNLOAD, folder + '\\grasscutter.zip', () => {
|
||||
unzip(folder + '\\grasscutter.zip', folder + '\\', this.toggleButtons)
|
||||
this.props.downloadManager.addDownload(STABLE_DOWNLOAD, folder + '\\grasscutter.zip', async () => {
|
||||
await unzip(folder + '\\grasscutter.zip', folder + '\\', true)
|
||||
this.toggleButtons
|
||||
})
|
||||
|
||||
// Also add repo download
|
||||
@@ -141,8 +144,9 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
|
||||
async downloadGrasscutterLatest() {
|
||||
const folder = await this.getGrasscutterFolder()
|
||||
this.props.downloadManager.addDownload(DEV_DOWNLOAD, folder + '\\grasscutter.zip', () => {
|
||||
unzip(folder + '\\grasscutter.zip', folder + '\\', this.toggleButtons)
|
||||
this.props.downloadManager.addDownload(DEV_DOWNLOAD, folder + '\\grasscutter.zip', async () => {
|
||||
await unzip(folder + '\\grasscutter.zip', folder + '\\', true)
|
||||
this.toggleButtons()
|
||||
})
|
||||
|
||||
// Also add repo download
|
||||
@@ -165,15 +169,14 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
})
|
||||
}
|
||||
|
||||
await unzip(folder + '\\resources.zip', folder + '\\', () => {
|
||||
// Rename folder to resources
|
||||
invoke('rename', {
|
||||
path: folder + '\\Resources',
|
||||
newName: 'resources',
|
||||
})
|
||||
|
||||
this.toggleButtons()
|
||||
await unzip(folder + '\\resources.zip', folder + '\\', true)
|
||||
// Rename folder to resources
|
||||
invoke('rename', {
|
||||
path: folder + '\\Resources',
|
||||
newName: 'resources',
|
||||
})
|
||||
|
||||
this.toggleButtons()
|
||||
})
|
||||
|
||||
this.toggleButtons()
|
||||
|
||||
32
src/ui/components/menu/ExtrasMenu.css
Normal file
32
src/ui/components/menu/ExtrasMenu.css
Normal file
@@ -0,0 +1,32 @@
|
||||
.ExtrasMenu {
|
||||
width: 20%;
|
||||
height: 40%;
|
||||
}
|
||||
|
||||
.ExtrasMenu .MenuInner {
|
||||
justify-content: space-between;
|
||||
height: 70%;
|
||||
}
|
||||
|
||||
.ExtrasMenuContent {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ExtraItem {
|
||||
width: 80%;
|
||||
|
||||
padding: 6px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ExtraLaunch .BigButton {
|
||||
padding: 20px 50px;
|
||||
}
|
||||
178
src/ui/components/menu/ExtrasMenu.tsx
Normal file
178
src/ui/components/menu/ExtrasMenu.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import React from 'react'
|
||||
import { getConfig, saveConfig } from '../../../utils/configuration'
|
||||
import Checkbox from '../common/Checkbox'
|
||||
import Menu from './Menu'
|
||||
|
||||
import './ExtrasMenu.css'
|
||||
import BigButton from '../common/BigButton'
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import Tr from '../../../utils/language'
|
||||
import { getGameExecutable } from '../../../utils/game'
|
||||
|
||||
interface IProps {
|
||||
children: React.ReactNode | React.ReactNode[]
|
||||
closeFn: () => void
|
||||
playGame: (exe?: string, proc_name?: string) => void
|
||||
}
|
||||
|
||||
interface IState {
|
||||
migoto?: string
|
||||
akebi?: string
|
||||
reshade?: string
|
||||
launch_migoto: boolean
|
||||
launch_akebi: boolean
|
||||
launch_reshade: boolean
|
||||
}
|
||||
|
||||
export class ExtrasMenu extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
launch_migoto: false,
|
||||
launch_akebi: false,
|
||||
launch_reshade: false,
|
||||
}
|
||||
|
||||
this.launchPreprograms = this.launchPreprograms.bind(this)
|
||||
this.toggleMigoto = this.toggleMigoto.bind(this)
|
||||
this.toggleAkebi = this.toggleAkebi.bind(this)
|
||||
this.toggleReshade = this.toggleReshade.bind(this)
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const config = await getConfig()
|
||||
|
||||
this.setState({
|
||||
migoto: config.migoto_path,
|
||||
akebi: config.akebi_path,
|
||||
reshade: config.reshade_path,
|
||||
launch_akebi: config?.last_extras?.akebi ?? false,
|
||||
launch_migoto: config?.last_extras?.migoto ?? false,
|
||||
launch_reshade: config?.last_extras?.reshade ?? false,
|
||||
})
|
||||
}
|
||||
|
||||
async launchPreprograms() {
|
||||
const config = await getConfig()
|
||||
|
||||
config.last_extras = {
|
||||
migoto: this.state.launch_migoto,
|
||||
akebi: this.state.launch_akebi,
|
||||
reshade: this.state.launch_reshade,
|
||||
}
|
||||
|
||||
await saveConfig(config)
|
||||
|
||||
// Close menu
|
||||
this.props.closeFn()
|
||||
|
||||
// This injects independent of the game
|
||||
if (this.state.launch_migoto) {
|
||||
await this.launchMigoto()
|
||||
}
|
||||
|
||||
// This injects independent of the game
|
||||
if (this.state.launch_reshade) {
|
||||
await this.launchReshade()
|
||||
}
|
||||
|
||||
// This will launch the game
|
||||
if (this.state.launch_akebi) {
|
||||
await this.launchAkebi()
|
||||
|
||||
// This already launches the game
|
||||
return
|
||||
}
|
||||
|
||||
// Launch the game
|
||||
await this.props.playGame()
|
||||
}
|
||||
|
||||
async launchAkebi() {
|
||||
const config = await getConfig()
|
||||
|
||||
// Get game exe from game path, so we can watch it
|
||||
const pathArr = config.game_install_path.replace(/\\/g, '/').split('/')
|
||||
const gameExec = pathArr[pathArr.length - 1]
|
||||
|
||||
await this.props.playGame(config.akebi_path, gameExec)
|
||||
}
|
||||
|
||||
async launchMigoto() {
|
||||
const config = await getConfig()
|
||||
|
||||
if (!config.migoto_path) return alert('Migoto not installed or set!')
|
||||
|
||||
await invoke('run_program_relative', { path: config.migoto_path })
|
||||
}
|
||||
|
||||
async launchReshade() {
|
||||
const config = await getConfig()
|
||||
|
||||
if (!config.reshade_path) return alert('Reshade not installed or set!')
|
||||
|
||||
await invoke('run_command', {
|
||||
program: config.reshade_path,
|
||||
args: [await getGameExecutable()],
|
||||
})
|
||||
}
|
||||
|
||||
toggleMigoto() {
|
||||
this.setState({
|
||||
launch_migoto: !this.state.launch_migoto,
|
||||
})
|
||||
}
|
||||
|
||||
toggleAkebi() {
|
||||
this.setState({
|
||||
launch_akebi: !this.state.launch_akebi,
|
||||
})
|
||||
}
|
||||
|
||||
toggleReshade() {
|
||||
this.setState({
|
||||
launch_reshade: !this.state.launch_reshade,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Menu closeFn={this.props.closeFn} heading="Extras" className="ExtrasMenu">
|
||||
<div className="ExtrasMenuContent">
|
||||
{this.state.migoto && (
|
||||
<div className="ExtraItem">
|
||||
<div className="ExtraItemLabel">
|
||||
<Tr text="swag.migoto_name" />
|
||||
</div>
|
||||
<Checkbox id="MigotoCheckbox" checked={this.state.launch_migoto} onChange={this.toggleMigoto} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{this.state.akebi && (
|
||||
<div className="ExtraItem">
|
||||
<div className="ExtraItemLabel">
|
||||
<Tr text="swag.akebi_name" />
|
||||
</div>
|
||||
<Checkbox id="AkebiCheckbox" checked={this.state.launch_akebi} onChange={this.toggleAkebi} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{this.state.reshade && (
|
||||
<div className="ExtraItem">
|
||||
<div className="ExtraItemLabel">
|
||||
<Tr text="swag.reshade_name" />
|
||||
</div>
|
||||
<Checkbox id="ReshadeCheckbox" checked={this.state.launch_reshade} onChange={this.toggleReshade} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="ExtraLaunch">
|
||||
<BigButton id="ExtraLaunch" onClick={this.launchPreprograms}>
|
||||
<Tr text="main.launch_button" />
|
||||
</BigButton>
|
||||
</div>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -45,11 +45,10 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
|
||||
async downloadGame() {
|
||||
const folder = this.state.gameDownloadFolder
|
||||
this.props.downloadManager.addDownload(GAME_DOWNLOAD, folder + '\\game.zip', () => {
|
||||
unzip(folder + '\\game.zip', folder + '\\', () => {
|
||||
this.setState({
|
||||
gameDownloading: false,
|
||||
})
|
||||
this.props.downloadManager.addDownload(GAME_DOWNLOAD, folder + '\\game.zip', async () => {
|
||||
await unzip(folder + '\\game.zip', folder + '\\', true)
|
||||
this.setState({
|
||||
gameDownloading: false,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ interface IState {
|
||||
|
||||
// Swag stuff
|
||||
akebi_path: string
|
||||
migoto_path: string
|
||||
reshade_path: string
|
||||
}
|
||||
|
||||
export default class Options extends React.Component<IProps, IState> {
|
||||
@@ -61,12 +63,15 @@ export default class Options extends React.Component<IProps, IState> {
|
||||
|
||||
// Swag stuff
|
||||
akebi_path: '',
|
||||
migoto_path: '',
|
||||
reshade_path: '',
|
||||
}
|
||||
|
||||
this.setGameExecutable = this.setGameExecutable.bind(this)
|
||||
this.setGrasscutterJar = this.setGrasscutterJar.bind(this)
|
||||
this.setJavaPath = this.setJavaPath.bind(this)
|
||||
this.setAkebi = this.setAkebi.bind(this)
|
||||
this.setMigoto = this.setMigoto.bind(this)
|
||||
this.toggleGrasscutterWithGame = this.toggleGrasscutterWithGame.bind(this)
|
||||
this.setCustomBackground = this.setCustomBackground.bind(this)
|
||||
this.toggleEncryption = this.toggleEncryption.bind(this)
|
||||
@@ -101,6 +106,8 @@ export default class Options extends React.Component<IProps, IState> {
|
||||
|
||||
// Swag stuff
|
||||
akebi_path: config.akebi_path || '',
|
||||
migoto_path: config.migoto_path || '',
|
||||
reshade_path: config.reshade_path || '',
|
||||
})
|
||||
|
||||
this.forceUpdate()
|
||||
@@ -138,6 +145,22 @@ export default class Options extends React.Component<IProps, IState> {
|
||||
})
|
||||
}
|
||||
|
||||
setMigoto(value: string) {
|
||||
setConfigOption('migoto_path', value)
|
||||
|
||||
this.setState({
|
||||
migoto_path: value,
|
||||
})
|
||||
}
|
||||
|
||||
setReshade(value: string) {
|
||||
setConfigOption('reshade_path', value)
|
||||
|
||||
this.setState({
|
||||
reshade_path: value,
|
||||
})
|
||||
}
|
||||
|
||||
async setLanguage(value: string) {
|
||||
await setConfigOption('language', value)
|
||||
window.location.reload()
|
||||
@@ -204,7 +227,6 @@ export default class Options extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
async restoreMetadata() {
|
||||
console.log(this.props)
|
||||
await meta.restoreMetadata(this.props.downloadManager)
|
||||
}
|
||||
|
||||
@@ -313,10 +335,26 @@ export default class Options extends React.Component<IProps, IState> {
|
||||
<div className="OptionLabel" id="menuOptionsLabelAkebi">
|
||||
<Tr text="swag.akebi" />
|
||||
</div>
|
||||
<div className="OptionValue" id="menuOptionsDirMigoto">
|
||||
<div className="OptionValue" id="menuOptionsDirAkebi">
|
||||
<DirInput onChange={this.setAkebi} value={this.state?.akebi_path} extensions={['exe']} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="OptionSection" id="menuOptionsContainerMigoto">
|
||||
<div className="OptionLabel" id="menuOptionsLabelMigoto">
|
||||
<Tr text="swag.migoto" />
|
||||
</div>
|
||||
<div className="OptionValue" id="menuOptionsDirMigoto">
|
||||
<DirInput onChange={this.setMigoto} value={this.state?.migoto_path} extensions={['exe']} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="OptionSection" id="menuOptionsContainerReshade">
|
||||
<div className="OptionLabel" id="menuOptionsLabelReshade">
|
||||
<Tr text="swag.reshade" />
|
||||
</div>
|
||||
<div className="OptionValue" id="menuOptionsDirReshade">
|
||||
<DirInput onChange={this.setReshade} value={this.state?.reshade_path} extensions={['exe']} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
39
src/ui/components/mods/LoadingCircle.css
Normal file
39
src/ui/components/mods/LoadingCircle.css
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Blatantly yoinked from https://loading.io/css/
|
||||
*/
|
||||
|
||||
.LoadingCircle {
|
||||
display: inline-block;
|
||||
transform: translateZ(1px);
|
||||
|
||||
position: absolute;
|
||||
top: 45%;
|
||||
left: 48.5%;
|
||||
}
|
||||
|
||||
.LoadingCircle > div {
|
||||
display: inline-block;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 8px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
animation: loading 2.4s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
0%,
|
||||
100% {
|
||||
animation-timing-function: cubic-bezier(0.5, 0, 1, 0.5);
|
||||
}
|
||||
0% {
|
||||
transform: rotateY(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotateY(1800deg);
|
||||
animation-timing-function: cubic-bezier(0, 0.5, 0.5, 1);
|
||||
}
|
||||
100% {
|
||||
transform: rotateY(3600deg);
|
||||
}
|
||||
}
|
||||
13
src/ui/components/mods/LoadingCircle.tsx
Normal file
13
src/ui/components/mods/LoadingCircle.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
|
||||
import './LoadingCircle.css'
|
||||
|
||||
export class LoadingCircle extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="LoadingCircle">
|
||||
<div></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
30
src/ui/components/mods/ModHeader.css
Normal file
30
src/ui/components/mods/ModHeader.css
Normal file
@@ -0,0 +1,30 @@
|
||||
.ModHeader {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
|
||||
padding: 10px;
|
||||
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
background: rgba(77, 77, 77, 0.6);
|
||||
}
|
||||
|
||||
.ModHeaderTitle {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
max-width: 20%;
|
||||
}
|
||||
|
||||
.ModHeaderTitle:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ModHeaderTitle.selected {
|
||||
border-bottom: 2px solid #fff;
|
||||
}
|
||||
52
src/ui/components/mods/ModHeader.tsx
Normal file
52
src/ui/components/mods/ModHeader.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react'
|
||||
|
||||
import './ModHeader.css'
|
||||
|
||||
interface IProps {
|
||||
headers: {
|
||||
title: string
|
||||
name: string
|
||||
}[]
|
||||
onChange: (value: string) => void
|
||||
defaultHeader: string
|
||||
}
|
||||
|
||||
interface IState {
|
||||
selected: string
|
||||
}
|
||||
|
||||
export class ModHeader extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
selected: this.props.defaultHeader,
|
||||
}
|
||||
}
|
||||
|
||||
setSelected(value: string) {
|
||||
this.setState({
|
||||
selected: value,
|
||||
})
|
||||
|
||||
this.props.onChange(value)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="ModHeader">
|
||||
{this.props.headers.map((header, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`ModHeaderTitle ${this.state.selected === header.name ? 'selected' : ''}`}
|
||||
onClick={() => this.setSelected(header.name)}
|
||||
>
|
||||
{header.title}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
19
src/ui/components/mods/ModList.css
Normal file
19
src/ui/components/mods/ModList.css
Normal file
@@ -0,0 +1,19 @@
|
||||
.ModList {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-color: rgba(106, 105, 106, 0.6);
|
||||
}
|
||||
|
||||
.ModListInner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
overflow-y: auto;
|
||||
}
|
||||
93
src/ui/components/mods/ModList.tsx
Normal file
93
src/ui/components/mods/ModList.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React from 'react'
|
||||
import { getInstalledMods, getMods, ModData, PartialModData } from '../../../utils/gamebanana'
|
||||
import { LoadingCircle } from './LoadingCircle'
|
||||
|
||||
import './ModList.css'
|
||||
import { ModTile } from './ModTile'
|
||||
|
||||
interface IProps {
|
||||
mode: string
|
||||
addDownload: (mod: ModData) => void
|
||||
}
|
||||
|
||||
interface IState {
|
||||
modList: ModData[] | null
|
||||
installedList:
|
||||
| {
|
||||
path: string
|
||||
info: ModData | PartialModData
|
||||
}[]
|
||||
| null
|
||||
}
|
||||
|
||||
export class ModList extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
modList: null,
|
||||
installedList: null,
|
||||
}
|
||||
|
||||
this.downloadMod = this.downloadMod.bind(this)
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
if (this.props.mode === 'installed') {
|
||||
const installedMods = (await getInstalledMods()).map((mod) => {
|
||||
// Check if it's a partial mod, and if so, fill in some pseudo-data
|
||||
if (!('id' in mod.info)) {
|
||||
const newInfo = mod.info as PartialModData
|
||||
|
||||
newInfo.images = []
|
||||
newInfo.submitter = { name: 'Unknown' }
|
||||
newInfo.likes = 0
|
||||
newInfo.views = 0
|
||||
|
||||
mod.info = newInfo
|
||||
|
||||
return mod
|
||||
}
|
||||
|
||||
return mod
|
||||
})
|
||||
|
||||
this.setState({
|
||||
installedList: installedMods,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const mods = await getMods(this.props.mode)
|
||||
|
||||
this.setState({
|
||||
modList: mods,
|
||||
})
|
||||
}
|
||||
|
||||
async downloadMod(mod: ModData) {
|
||||
this.props.addDownload(mod)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="ModList">
|
||||
{(this.state.modList && this.props.mode !== 'installed') ||
|
||||
(this.state.installedList && this.props.mode === 'installed') ? (
|
||||
<div className="ModListInner">
|
||||
{this.props.mode === 'installed'
|
||||
? this.state.installedList?.map((mod) => (
|
||||
<ModTile path={mod.path} mod={mod.info} key={mod.info.name} onClick={this.downloadMod} />
|
||||
))
|
||||
: this.state.modList?.map((mod: ModData) => (
|
||||
<ModTile mod={mod} key={mod.id} onClick={this.downloadMod} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<LoadingCircle />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
137
src/ui/components/mods/ModTile.css
Normal file
137
src/ui/components/mods/ModTile.css
Normal file
@@ -0,0 +1,137 @@
|
||||
.ModListItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
width: 23%;
|
||||
margin: 10px;
|
||||
|
||||
background: rgb(99, 98, 98, 0.2);
|
||||
border-radius: 10px;
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
transition: background-color 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.ModListItem:hover {
|
||||
cursor: pointer;
|
||||
background: rgb(99, 98, 98, 0.8);
|
||||
}
|
||||
|
||||
.ModAuthor,
|
||||
.ModName {
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
padding: 10px 0 0 10px;
|
||||
margin-left: 10px;
|
||||
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ModAuthor {
|
||||
font-weight: normal;
|
||||
padding: 0 0 0 10px;
|
||||
}
|
||||
|
||||
.ModTileOpen {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ModListItem .Checkbox {
|
||||
filter: invert(100%) sepia(2%) saturate(201%) hue-rotate(47deg) brightness(117%) contrast(100%);
|
||||
}
|
||||
|
||||
.ModImage .ModTileFolder {
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.ModTileFolder:hover {
|
||||
cursor: pointer;
|
||||
filter: invert(1) brightness(0.2);
|
||||
}
|
||||
|
||||
.ModTileOpen,
|
||||
.ModTileDownload {
|
||||
position: absolute;
|
||||
object-fit: contain;
|
||||
|
||||
left: 40%;
|
||||
top: 45%;
|
||||
|
||||
z-index: 999;
|
||||
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.ModTileOpen {
|
||||
left: 37.5%;
|
||||
}
|
||||
|
||||
.ModImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ModImage .ModImageInner {
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
|
||||
margin: 14px;
|
||||
}
|
||||
|
||||
img.blur {
|
||||
filter: blur(6px);
|
||||
}
|
||||
|
||||
img.nsfw {
|
||||
filter: blur(16px);
|
||||
}
|
||||
|
||||
.ModInner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
|
||||
color: #fff;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.ModInner div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
padding: 0px 8px;
|
||||
}
|
||||
|
||||
.ModInner div span {
|
||||
margin: 0px 5px;
|
||||
}
|
||||
|
||||
.ModInner img {
|
||||
object-fit: contain;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
margin: 0px;
|
||||
|
||||
filter: invert(1);
|
||||
}
|
||||
126
src/ui/components/mods/ModTile.tsx
Normal file
126
src/ui/components/mods/ModTile.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import React from 'react'
|
||||
import { ModData, PartialModData } from '../../../utils/gamebanana'
|
||||
|
||||
import './ModTile.css'
|
||||
import Like from '../../../resources/icons/like.svg'
|
||||
import Eye from '../../../resources/icons/eye.svg'
|
||||
import Download from '../../../resources/icons/download.svg'
|
||||
import Folder from '../../../resources/icons/folder.svg'
|
||||
import { shell } from '@tauri-apps/api'
|
||||
import Checkbox from '../common/Checkbox'
|
||||
import { disableMod, enableMod, modIsEnabled } from '../../../utils/mods'
|
||||
|
||||
interface IProps {
|
||||
mod: ModData | PartialModData
|
||||
path?: string
|
||||
onClick: (mod: ModData) => void
|
||||
}
|
||||
|
||||
interface IState {
|
||||
hover: boolean
|
||||
modEnabled: boolean
|
||||
}
|
||||
|
||||
export class ModTile extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
hover: false,
|
||||
modEnabled: false,
|
||||
}
|
||||
|
||||
this.openInExplorer = this.openInExplorer.bind(this)
|
||||
this.toggleMod = this.toggleMod.bind(this)
|
||||
}
|
||||
|
||||
getModFolderName() {
|
||||
if (!('id' in this.props.mod)) {
|
||||
return this.props.mod.name.includes('DISABLED_') ? this.props.mod.name.split('DISABLED_')[1] : this.props.mod.name
|
||||
}
|
||||
|
||||
return String(this.props.mod.id)
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
if (!('id' in this.props.mod)) {
|
||||
// Partial mod
|
||||
this.setState({
|
||||
modEnabled: await modIsEnabled(this.props.mod.name),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
modEnabled: await modIsEnabled(String(this.props.mod.id)),
|
||||
})
|
||||
}
|
||||
|
||||
async openInExplorer() {
|
||||
if (this.props.path) shell.open(this.props.path)
|
||||
}
|
||||
|
||||
toggleMod() {
|
||||
this.setState(
|
||||
{
|
||||
modEnabled: !this.state.modEnabled,
|
||||
},
|
||||
() => {
|
||||
if (this.state.modEnabled) {
|
||||
enableMod(String(this.getModFolderName()))
|
||||
return
|
||||
}
|
||||
|
||||
disableMod(String(this.getModFolderName()))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { mod } = this.props
|
||||
|
||||
return (
|
||||
<div
|
||||
className="ModListItem"
|
||||
onMouseEnter={() => this.setState({ hover: true })}
|
||||
onMouseLeave={() => this.setState({ hover: false })}
|
||||
{...(!this.props.path && {
|
||||
onClick: () => {
|
||||
if (!('id' in mod)) return
|
||||
|
||||
this.props.onClick(mod)
|
||||
},
|
||||
})}
|
||||
>
|
||||
<span className="ModName">{mod.name.includes('DISABLED_') ? mod.name.split('DISABLED_')[1] : mod.name}</span>
|
||||
<span className="ModAuthor">{mod.submitter.name}</span>
|
||||
<div className="ModImage">
|
||||
{this.state.hover &&
|
||||
(!this.props.path ? (
|
||||
<img src={Download} className="ModTileDownload" alt="Download" />
|
||||
) : (
|
||||
<div className="ModTileOpen">
|
||||
<img src={Folder} className="ModTileFolder" alt="Open" onClick={this.openInExplorer} />
|
||||
<Checkbox checked={this.state.modEnabled} id={this.props.mod.name} onChange={this.toggleMod} />
|
||||
</div>
|
||||
))}
|
||||
<img
|
||||
src={mod.images[0]}
|
||||
className={`ModImageInner ${'id' in mod && mod.nsfw ? 'nsfw' : ''} ${this.state.hover ? 'blur' : ''}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="ModInner">
|
||||
<div className="likes">
|
||||
<img src={Like} />
|
||||
<span>{mod.likes.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="views">
|
||||
<img src={Eye} />
|
||||
<span>{mod.views.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,13 @@ export interface Configuration {
|
||||
|
||||
// Swag stuff
|
||||
akebi_path?: string
|
||||
migoto_path?: string
|
||||
reshade_path?: string
|
||||
last_extras?: {
|
||||
migoto: boolean
|
||||
akebi: boolean
|
||||
reshade: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export async function setConfigOption<K extends keyof Configuration>(key: K, value: Configuration[K]): Promise<void> {
|
||||
|
||||
205
src/utils/gamebanana.ts
Normal file
205
src/utils/gamebanana.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { getConfigOption } from './configuration'
|
||||
|
||||
// Generated with https://transform.tools/json-to-typescript I'm lazy cry about it
|
||||
interface GamebananaResponse {
|
||||
_idRow: number
|
||||
_sModelName: string
|
||||
_sSingularTitle: string
|
||||
_sIconClasses: string
|
||||
_sName: string
|
||||
_sProfileUrl: string
|
||||
_aPreviewMedia: PreviewMedia
|
||||
_tsDateAdded: number
|
||||
_bHasFiles: boolean
|
||||
_aSubmitter: Submitter
|
||||
_aRootCategory: RootCategory
|
||||
_bIsNsfw: boolean
|
||||
_sInitialVisibility: string
|
||||
_nLikeCount?: number
|
||||
_bIsOwnedByAccessor: boolean
|
||||
_nViewCount?: number
|
||||
_nPostCount?: number
|
||||
_tsDateUpdated?: number
|
||||
}
|
||||
|
||||
interface PreviewMedia {
|
||||
_aImages?: Image[]
|
||||
_aMetadata?: Metadata
|
||||
}
|
||||
|
||||
interface Image {
|
||||
_sType: string
|
||||
_sBaseUrl: string
|
||||
_sFile: string
|
||||
_sFile530?: string
|
||||
_sFile100: string
|
||||
_sFile220?: string
|
||||
_sCaption?: string
|
||||
}
|
||||
|
||||
interface Metadata {
|
||||
_sState?: string
|
||||
_sSnippet: string
|
||||
_nPostCount?: number
|
||||
_nBounty?: number
|
||||
}
|
||||
|
||||
interface Submitter {
|
||||
_idRow: number
|
||||
_sName: string
|
||||
_bIsOnline: boolean
|
||||
_bHasRipe: boolean
|
||||
_sProfileUrl: string
|
||||
_sAvatarUrl: string
|
||||
_sUpicUrl?: string
|
||||
_aClearanceLevels?: string[]
|
||||
_sHdAvatarUrl?: string
|
||||
}
|
||||
|
||||
interface RootCategory {
|
||||
_sName: string
|
||||
_sProfileUrl: string
|
||||
_sIconUrl: string
|
||||
}
|
||||
|
||||
export interface ModData {
|
||||
id: number
|
||||
name: string
|
||||
images: string[]
|
||||
dateadded: number
|
||||
submitter: {
|
||||
name: string
|
||||
url: string
|
||||
}
|
||||
nsfw: boolean
|
||||
likes: number
|
||||
views: number
|
||||
type: string
|
||||
}
|
||||
|
||||
export interface PartialModData {
|
||||
name: string
|
||||
images: string[]
|
||||
submitter: {
|
||||
name: string
|
||||
}
|
||||
likes: number
|
||||
views: number
|
||||
}
|
||||
|
||||
interface GamebananaDownloads {
|
||||
_bIsTrashed: boolean
|
||||
_bIsWithheld: boolean
|
||||
_aFiles: File[]
|
||||
_sLicense: string
|
||||
}
|
||||
|
||||
interface File {
|
||||
_idRow: number
|
||||
_sFile: string
|
||||
_nFilesize: number
|
||||
_sDescription: string
|
||||
_tsDateAdded: number
|
||||
_nDownloadCount: number
|
||||
_sAnalysisState: string
|
||||
_sDownloadUrl: string
|
||||
_sMd5Checksum: string
|
||||
_sClamAvResult: string
|
||||
_sAnalysisResult: string
|
||||
_bContainsExe: boolean
|
||||
}
|
||||
|
||||
interface ModDownload {
|
||||
filename: string
|
||||
downloadUrl: string
|
||||
filesize: number
|
||||
containsExe: boolean
|
||||
}
|
||||
|
||||
export async function getMods(mode: string) {
|
||||
const resp = JSON.parse(
|
||||
await invoke('list_submissions', {
|
||||
mode,
|
||||
})
|
||||
)
|
||||
|
||||
return formatGamebananaData(resp)
|
||||
}
|
||||
|
||||
export async function formatGamebananaData(obj: GamebananaResponse[]) {
|
||||
if (!obj) return []
|
||||
|
||||
return obj
|
||||
.map((itm) => {
|
||||
const img = itm?._aPreviewMedia?._aImages
|
||||
|
||||
return {
|
||||
id: itm._idRow,
|
||||
name: itm._sName,
|
||||
images: img
|
||||
? img.map((i) => {
|
||||
return i._sBaseUrl + '/' + i._sFile220
|
||||
})
|
||||
: [],
|
||||
dateadded: itm._tsDateAdded,
|
||||
submitter: {
|
||||
name: itm._aSubmitter._sName,
|
||||
url: itm._aSubmitter._sProfileUrl,
|
||||
},
|
||||
nsfw: itm._bIsNsfw,
|
||||
likes: itm?._nLikeCount || 0,
|
||||
views: itm?._nViewCount || 0,
|
||||
type: itm._sSingularTitle,
|
||||
} as ModData
|
||||
})
|
||||
.filter((itm) => itm.type === 'Mod')
|
||||
}
|
||||
|
||||
export async function getInstalledMods() {
|
||||
const migotoPath = await getConfigOption('migoto_path')
|
||||
|
||||
if (!migotoPath) return []
|
||||
|
||||
const mods = (await invoke('list_mods', {
|
||||
path: migotoPath,
|
||||
})) as Record<string, string>
|
||||
|
||||
// These are returned as JSON strings, so we have to parse them
|
||||
return Object.keys(mods).map((path) => {
|
||||
const info = JSON.parse(mods[path]) as ModData | PartialModData
|
||||
|
||||
const modPathArr = path.replace(/\\/g, '/').split('/')
|
||||
|
||||
// If there is a file in this path, remove it from the path
|
||||
if (modPathArr[modPathArr.length - 1].includes('.')) modPathArr.pop()
|
||||
|
||||
return {
|
||||
path: modPathArr.join('/'),
|
||||
info,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function getModDownload(modId: string) {
|
||||
const resp = JSON.parse(
|
||||
await invoke('get_download_links', {
|
||||
modId,
|
||||
})
|
||||
) as GamebananaDownloads
|
||||
|
||||
return formatDownloadsData(resp)
|
||||
}
|
||||
|
||||
export async function formatDownloadsData(obj: GamebananaDownloads) {
|
||||
if (!obj) return []
|
||||
|
||||
return obj._aFiles.map((itm) => {
|
||||
return {
|
||||
filename: itm._sFile,
|
||||
downloadUrl: `https://files.gamebanana.com/mods/${itm._sFile}`,
|
||||
filesize: itm._nFilesize,
|
||||
containsExe: itm._bContainsExe,
|
||||
} as ModDownload
|
||||
})
|
||||
}
|
||||
71
src/utils/mods.ts
Normal file
71
src/utils/mods.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { getConfigOption } from './configuration'
|
||||
|
||||
export async function getModsFolder() {
|
||||
const migotoPath = await getConfigOption('migoto_path')
|
||||
|
||||
if (!migotoPath) return null
|
||||
|
||||
// Remove exe from path
|
||||
const pathArr = migotoPath.replace(/\\/g, '/').split('/')
|
||||
pathArr.pop()
|
||||
|
||||
return pathArr.join('/') + '/Mods/'
|
||||
}
|
||||
|
||||
export async function disableMod(modId: string) {
|
||||
const path = (await getModsFolder()) + modId
|
||||
const pathExists = await invoke('dir_exists', {
|
||||
path,
|
||||
})
|
||||
|
||||
if (!pathExists) return console.log("Path doesn't exist")
|
||||
|
||||
const modName = path.replace(/\\/g, '/').split('/').pop()
|
||||
|
||||
await invoke('rename', {
|
||||
path,
|
||||
newName: `DISABLED_${modName}`,
|
||||
})
|
||||
}
|
||||
|
||||
export async function enableMod(modId: string) {
|
||||
const path = (await getModsFolder()) + `DISABLED_${modId}`
|
||||
const modName = path.replace(/\\/g, '/').split('/').pop()
|
||||
const pathExists = await invoke('dir_exists', {
|
||||
path,
|
||||
})
|
||||
|
||||
if (!pathExists) return console.log("Path doesn't exist")
|
||||
|
||||
if (!modName?.includes('DISABLED_')) return
|
||||
|
||||
const newName = modName.replace('DISABLED_', '')
|
||||
|
||||
await invoke('rename', {
|
||||
path,
|
||||
newName,
|
||||
})
|
||||
}
|
||||
|
||||
export async function getModFolderName(modId: string) {
|
||||
const modsFolder = await getModsFolder()
|
||||
|
||||
if (!modsFolder) return null
|
||||
|
||||
const modEnabled = await invoke('dir_exists', {
|
||||
path: modsFolder + modId,
|
||||
})
|
||||
const modDisabled = await invoke('dir_exists', {
|
||||
path: modsFolder + 'DISABLED_' + modId,
|
||||
})
|
||||
|
||||
if (!modEnabled && !modDisabled) return null
|
||||
|
||||
if (modEnabled) return modId
|
||||
if (modDisabled) return 'DISABLED_' + modId
|
||||
}
|
||||
|
||||
export async function modIsEnabled(modId: string) {
|
||||
return !(await getModFolderName(modId))?.includes('DISABLED_')
|
||||
}
|
||||
@@ -1,15 +1,30 @@
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
|
||||
export function unzip(file: string, dest: string, onFinish?: () => void) {
|
||||
invoke('unzip', {
|
||||
zipfile: file,
|
||||
destpath: dest,
|
||||
})
|
||||
interface UnzipPayload {
|
||||
file: string
|
||||
new_folder: string
|
||||
}
|
||||
|
||||
listen('extract_end', ({ payload }) => {
|
||||
if (payload === file && onFinish) {
|
||||
onFinish()
|
||||
}
|
||||
export function unzip(
|
||||
file: string,
|
||||
dest: string,
|
||||
topLevelStrip?: boolean,
|
||||
folderIfLoose?: boolean
|
||||
): Promise<UnzipPayload> {
|
||||
return new Promise((resolve) => {
|
||||
invoke('unzip', {
|
||||
zipfile: file,
|
||||
destpath: dest,
|
||||
topLevelStrip,
|
||||
folderIfLoose,
|
||||
})
|
||||
|
||||
listen('extract_end', ({ payload }) => {
|
||||
// @ts-expect-error Payload is an object
|
||||
if (payload?.file === file) {
|
||||
resolve(payload as UnzipPayload)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user