mirror of
https://github.com/Grasscutters/Cultivation.git
synced 2026-02-06 02:06:29 +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",
|
"asn1-rs-impl",
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"nom",
|
"nom",
|
||||||
"num-traits",
|
"num-traits 0.2.15",
|
||||||
"rusticata-macros",
|
"rusticata-macros",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"time 0.3.11",
|
"time 0.3.11",
|
||||||
@@ -736,6 +736,7 @@ dependencies = [
|
|||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tokio-tungstenite",
|
"tokio-tungstenite",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"unrar",
|
||||||
"zip 0.6.2",
|
"zip 0.6.2",
|
||||||
"zip-extract",
|
"zip-extract",
|
||||||
]
|
]
|
||||||
@@ -820,8 +821,8 @@ dependencies = [
|
|||||||
"asn1-rs",
|
"asn1-rs",
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"nom",
|
"nom",
|
||||||
"num-bigint",
|
"num-bigint 0.4.3",
|
||||||
"num-traits",
|
"num-traits 0.2.15",
|
||||||
"rusticata-macros",
|
"rusticata-macros",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -948,6 +949,15 @@ dependencies = [
|
|||||||
"cfg-if 1.0.0",
|
"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]]
|
[[package]]
|
||||||
name = "error-chain"
|
name = "error-chain"
|
||||||
version = "0.12.4"
|
version = "0.12.4"
|
||||||
@@ -1041,6 +1051,12 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fuchsia-cprng"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futf"
|
name = "futf"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -1699,8 +1715,8 @@ dependencies = [
|
|||||||
"byteorder",
|
"byteorder",
|
||||||
"color_quant",
|
"color_quant",
|
||||||
"num-iter",
|
"num-iter",
|
||||||
"num-rational",
|
"num-rational 0.4.1",
|
||||||
"num-traits",
|
"num-traits 0.2.15",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2171,6 +2187,32 @@ dependencies = [
|
|||||||
"winapi",
|
"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]]
|
[[package]]
|
||||||
name = "num-bigint"
|
name = "num-bigint"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@@ -2179,7 +2221,17 @@ checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"num-integer",
|
"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]]
|
[[package]]
|
||||||
@@ -2189,7 +2241,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"num-traits",
|
"num-traits 0.2.15",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2200,7 +2252,19 @@ checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"num-integer",
|
"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]]
|
[[package]]
|
||||||
@@ -2211,7 +2275,16 @@ checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"num-integer",
|
"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]]
|
[[package]]
|
||||||
@@ -2831,6 +2904,19 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"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]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
@@ -2876,6 +2962,21 @@ dependencies = [
|
|||||||
"rand_core 0.6.3",
|
"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]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@@ -2967,6 +3068,15 @@ dependencies = [
|
|||||||
"yasna",
|
"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]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.2.13"
|
version = "0.2.13"
|
||||||
@@ -3113,6 +3223,12 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-serialize"
|
||||||
|
version = "0.3.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
@@ -4289,6 +4405,31 @@ version = "0.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04"
|
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]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ sysinfo = "0.24.6"
|
|||||||
|
|
||||||
# ZIP-archive library.
|
# ZIP-archive library.
|
||||||
zip-extract = "0.1.1"
|
zip-extract = "0.1.1"
|
||||||
|
unrar = "0.4.4"
|
||||||
zip = "0.6.2"
|
zip = "0.6.2"
|
||||||
|
|
||||||
# For creating a "global" downloads list.
|
# 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."
|
"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": {
|
"swag": {
|
||||||
|
"akebi_name": "Akebi",
|
||||||
|
"migoto_name": "Migoto",
|
||||||
|
"reshade_name": "Reshade",
|
||||||
"akebi": "Set Akebi Executable",
|
"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 downloader;
|
||||||
mod file_helpers;
|
mod file_helpers;
|
||||||
|
mod gamebanana;
|
||||||
mod lang;
|
mod lang;
|
||||||
mod metadata_patcher;
|
mod metadata_patcher;
|
||||||
mod proxy;
|
mod proxy;
|
||||||
@@ -59,6 +60,9 @@ fn main() {
|
|||||||
lang::get_languages,
|
lang::get_languages,
|
||||||
web::valid_url,
|
web::valid_url,
|
||||||
web::web_get,
|
web::web_get,
|
||||||
|
gamebanana::get_download_links,
|
||||||
|
gamebanana::list_submissions,
|
||||||
|
gamebanana::list_mods,
|
||||||
metadata_patcher::patch_metadata
|
metadata_patcher::patch_metadata
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
|
|||||||
@@ -1,12 +1,30 @@
|
|||||||
use duct::cmd;
|
use duct::cmd;
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn run_program(path: String) {
|
pub fn run_program(path: String, args: Option<String>) {
|
||||||
// Open in new thread to prevent blocking.
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
// Without unwrap_or, this can crash when UAC prompt is denied
|
// Without unwrap_or, this can crash when UAC prompt is denied
|
||||||
open::that(&path).unwrap_or(());
|
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]
|
#[tauri::command]
|
||||||
@@ -30,7 +48,13 @@ pub fn run_program_relative(path: String) {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn run_command(program: &str, args: Vec<&str>) {
|
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]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
use std::fs::File;
|
use std::fs::{read_dir, File};
|
||||||
use std::path;
|
use std::path;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
use unrar::archive::{Archive, OpenArchive};
|
||||||
|
|
||||||
#[tauri::command]
|
#[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
|
// Read file TODO: replace test file
|
||||||
let f = match File::open(&zipfile) {
|
let f = match File::open(&zipfile) {
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
@@ -15,40 +22,80 @@ pub fn unzip(window: tauri::Window, zipfile: String, destpath: String) {
|
|||||||
|
|
||||||
let write_path = path::PathBuf::from(&destpath);
|
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
|
// Run extraction in seperate thread
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let full_path = write_path;
|
let mut full_path = write_path.clone();
|
||||||
|
|
||||||
window.emit("extract_start", &zipfile).unwrap();
|
window.emit("extract_start", &zipfile).unwrap();
|
||||||
|
|
||||||
match zip_extract::extract(&f, &full_path, true) {
|
if folder_if_loose.unwrap_or(false) {
|
||||||
Ok(_) => {
|
// Create a new folder with the same name as the zip file
|
||||||
println!(
|
let mut file_name = path::Path::new(&zipfile)
|
||||||
"Extracted zip file to: {}",
|
.file_name()
|
||||||
full_path.to_str().unwrap_or("Error")
|
.unwrap()
|
||||||
);
|
.to_str()
|
||||||
}
|
.unwrap();
|
||||||
|
|
||||||
|
// remove ".zip" from the end of the file name
|
||||||
|
file_name = &file_name[..file_name.len() - 4];
|
||||||
|
|
||||||
|
let new_path = full_path.join(file_name);
|
||||||
|
match std::fs::create_dir_all(&new_path) {
|
||||||
|
Ok(_) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Failed to extract zip file: {}", e);
|
println!("Failed to create directory: {}", e);
|
||||||
let mut res_hash = std::collections::HashMap::new();
|
return;
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
full_path = new_path.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// Get the name of the inenr file in the zip file
|
||||||
let mut zip = zip::ZipArchive::new(&f).unwrap();
|
let mut zip = zip::ZipArchive::new(&f).unwrap();
|
||||||
let file = zip.by_index(0).unwrap();
|
let file = zip.by_index(0).unwrap();
|
||||||
let name = file.name();
|
name = file.name().to_string().clone();
|
||||||
|
}
|
||||||
|
|
||||||
// If the contents is a jar file, emit that we have extracted a new jar file
|
// If the contents is a jar file, emit that we have extracted a new jar file
|
||||||
if name.ends_with(".jar") {
|
if name.ends_with(".jar") {
|
||||||
window
|
window
|
||||||
.emit("jar_extracted", destpath.to_string() + name)
|
.emit("jar_extracted", destpath.to_string() + name.as_str())
|
||||||
.unwrap();
|
.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 {
|
pub(crate) async fn query(site: &str) -> String {
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
@@ -6,6 +6,7 @@ pub(crate) async fn query(site: &str) -> String {
|
|||||||
let response = client
|
let response = client
|
||||||
.get(site)
|
.get(site)
|
||||||
.header(USER_AGENT, "cultivation")
|
.header(USER_AGENT, "cultivation")
|
||||||
|
.header(CONTENT_TYPE, "application/json")
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.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 {
|
.TopButton {
|
||||||
height: 60%;
|
height: 60%;
|
||||||
margin: 0px 10px;
|
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%);
|
filter: invert(95%) sepia(0%) saturate(18%) hue-rotate(153deg) brightness(88%) contrast(81%);
|
||||||
|
|
||||||
transition: filter 0.1s ease-in-out;
|
transition: filter 0.1s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.TopButton:hover {
|
.TopButton:hover {
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TopButton:hover img {
|
||||||
filter: invert(100%) sepia(0%) saturate(18%) hue-rotate(153deg) brightness(100%) contrast(100%);
|
filter: invert(100%) sepia(0%) saturate(18%) hue-rotate(153deg) brightness(100%) contrast(100%);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -97,6 +114,9 @@ select:focus {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ExtrasMenu {
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-height: 580px) {
|
@media (max-height: 580px) {
|
||||||
.BottomSection {
|
.BottomSection {
|
||||||
height: 150px;
|
height: 150px;
|
||||||
|
|||||||
176
src/ui/App.tsx
176
src/ui/App.tsx
@@ -1,101 +1,33 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { listen } from '@tauri-apps/api/event'
|
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
|
||||||
import DownloadHandler from '../utils/download'
|
import DownloadHandler from '../utils/download'
|
||||||
|
import { getConfigOption } from '../utils/configuration'
|
||||||
// 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 { getTheme, loadTheme } from '../utils/themes'
|
import { getTheme, loadTheme } from '../utils/themes'
|
||||||
import { unpatchGame } from '../utils/metadata'
|
import { convertFileSrc, invoke } from '@tauri-apps/api/tauri'
|
||||||
|
import { dataDir } from '@tauri-apps/api/path'
|
||||||
interface IProps {
|
import { Main } from './Main'
|
||||||
[key: string]: never
|
import { Mods } from './Mods'
|
||||||
}
|
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
isDownloading: boolean
|
page: string
|
||||||
optionsOpen: boolean
|
|
||||||
miniDownloadsOpen: boolean
|
|
||||||
downloadsOpen: boolean
|
|
||||||
gameDownloadsOpen: boolean
|
|
||||||
bgFile: string
|
bgFile: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const downloadHandler = new DownloadHandler()
|
||||||
const DEFAULT_BG = 'https://api.grasscutter.io/cultivation/bgfile'
|
const DEFAULT_BG = 'https://api.grasscutter.io/cultivation/bgfile'
|
||||||
|
|
||||||
const downloadHandler = new DownloadHandler()
|
class App extends React.Component<Readonly<unknown>, IState> {
|
||||||
|
constructor(props: Readonly<unknown>) {
|
||||||
class App extends React.Component<IProps, IState> {
|
|
||||||
constructor(props: IProps) {
|
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isDownloading: false,
|
page: 'main',
|
||||||
optionsOpen: false,
|
|
||||||
miniDownloadsOpen: false,
|
|
||||||
downloadsOpen: false,
|
|
||||||
gameDownloadsOpen: false,
|
|
||||||
bgFile: DEFAULT_BG,
|
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() {
|
async componentDidMount() {
|
||||||
const cert_generated = await getConfigOption('cert_generated')
|
|
||||||
const game_exe = await getConfigOption('game_install_path')
|
const game_exe = await getConfigOption('game_install_path')
|
||||||
const game_path = game_exe?.substring(0, game_exe.replace(/\\/g, '/').lastIndexOf('/')) || ''
|
const game_path = game_exe?.substring(0, game_exe.replace(/\\/g, '/').lastIndexOf('/')) || ''
|
||||||
const root_path = game_path?.substring(0, game_path.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) {
|
window.addEventListener('changePage', (e) => {
|
||||||
// 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({
|
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() {
|
render() {
|
||||||
@@ -184,69 +107,14 @@ class App extends React.Component<IProps, IState> {
|
|||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TopBar
|
{(() => {
|
||||||
optFunc={() => {
|
switch (this.state.page) {
|
||||||
this.setState({ optionsOpen: !this.state.optionsOpen })
|
case 'modding':
|
||||||
}}
|
return <Mods downloadHandler={downloadHandler} />
|
||||||
downFunc={() => this.setState({ downloadsOpen: !this.state.downloadsOpen })}
|
default:
|
||||||
gameFunc={() => this.setState({ gameDownloadsOpen: !this.state.gameDownloadsOpen })}
|
return <Main downloadHandler={downloadHandler} />
|
||||||
/>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class Debug extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<TopBar optFunc={none} downFunc={none} gameFunc={none} />
|
<TopBar />
|
||||||
<TextInput readOnly={false} initalValue={'change to set proxy address'} onChange={setProxyAddress} />
|
<TextInput readOnly={false} initalValue={'change to set proxy address'} onChange={setProxyAddress} />
|
||||||
<button onClick={startProxy}>start proxy</button>
|
<button onClick={startProxy}>start proxy</button>
|
||||||
<button onClick={stopProxy}>stop 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%;
|
width: 5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.AkebiIcon,
|
#ExtrasMenuButton {
|
||||||
|
width: 5%;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ExtrasIcon,
|
||||||
.ServerIcon {
|
.ServerIcon {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
filter: invert(28%) sepia(28%) saturate(1141%) hue-rotate(352deg) brightness(96%) contrast(88%);
|
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 { invoke } from '@tauri-apps/api/tauri'
|
||||||
|
|
||||||
import Server from '../../resources/icons/server.svg'
|
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 './ServerLaunchSection.css'
|
||||||
import { dataDir } from '@tauri-apps/api/path'
|
import { dataDir } from '@tauri-apps/api/path'
|
||||||
import { getGameExecutable } from '../../utils/game'
|
import { getGameExecutable } from '../../utils/game'
|
||||||
import { patchGame, unpatchGame } from '../../utils/metadata'
|
import { patchGame, unpatchGame } from '../../utils/metadata'
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
openExtras: (playGame: () => void) => void
|
||||||
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
grasscutterEnabled: boolean
|
grasscutterEnabled: boolean
|
||||||
buttonLabel: string
|
buttonLabel: string
|
||||||
@@ -31,10 +35,12 @@ interface IState {
|
|||||||
httpsEnabled: boolean
|
httpsEnabled: boolean
|
||||||
|
|
||||||
swag: boolean
|
swag: boolean
|
||||||
|
akebiSet: boolean
|
||||||
|
migotoSet: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ServerLaunchSection extends React.Component<{}, IState> {
|
export default class ServerLaunchSection extends React.Component<IProps, IState> {
|
||||||
constructor(props: {}) {
|
constructor(props: IProps) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -49,11 +55,12 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
|
|||||||
httpsLabel: '',
|
httpsLabel: '',
|
||||||
httpsEnabled: false,
|
httpsEnabled: false,
|
||||||
swag: false,
|
swag: false,
|
||||||
|
akebiSet: false,
|
||||||
|
migotoSet: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.toggleGrasscutter = this.toggleGrasscutter.bind(this)
|
this.toggleGrasscutter = this.toggleGrasscutter.bind(this)
|
||||||
this.playGame = this.playGame.bind(this)
|
this.playGame = this.playGame.bind(this)
|
||||||
this.launchAkebi = this.launchAkebi.bind(this)
|
|
||||||
this.setIp = this.setIp.bind(this)
|
this.setIp = this.setIp.bind(this)
|
||||||
this.setPort = this.setPort.bind(this)
|
this.setPort = this.setPort.bind(this)
|
||||||
this.toggleHttps = this.toggleHttps.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'),
|
httpsLabel: await translate('main.https_enable'),
|
||||||
httpsEnabled: config.https_enabled || false,
|
httpsEnabled: config.https_enabled || false,
|
||||||
swag: config.swag_mode || 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) {
|
setIp(text: string) {
|
||||||
this.setState({
|
this.setState({
|
||||||
ip: text,
|
ip: text,
|
||||||
@@ -265,11 +264,9 @@ export default class ServerLaunchSection extends React.Component<{}, IState> {
|
|||||||
{this.state.buttonLabel}
|
{this.state.buttonLabel}
|
||||||
</BigButton>
|
</BigButton>
|
||||||
{this.state.swag && (
|
{this.state.swag && (
|
||||||
<>
|
<BigButton onClick={() => this.props.openExtras(this.playGame)} id="ExtrasMenuButton">
|
||||||
<BigButton onClick={this.launchAkebi} id="akebiLaunch">
|
<img className="ExtrasIcon" id="extrasIcon" src={Plus} />
|
||||||
<img className="AkebiIcon" id="akebiIcon" src={Akebi} />
|
|
||||||
</BigButton>
|
</BigButton>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
<BigButton onClick={this.launchServer} id="serverLaunch">
|
<BigButton onClick={this.launchServer} id="serverLaunch">
|
||||||
<img className="ServerIcon" id="serverLaunchIcon" src={Server} />
|
<img className="ServerIcon" id="serverLaunchIcon" src={Server} />
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { app } from '@tauri-apps/api'
|
import { app } from '@tauri-apps/api'
|
||||||
import { appWindow } from '@tauri-apps/api/window'
|
import { appWindow } from '@tauri-apps/api/window'
|
||||||
import closeIcon from '../../resources/icons/close.svg'
|
import { getConfig, setConfigOption } from '../../utils/configuration'
|
||||||
import minIcon from '../../resources/icons/min.svg'
|
|
||||||
import cogBtn from '../../resources/icons/cog.svg'
|
|
||||||
import downBtn from '../../resources/icons/download.svg'
|
|
||||||
|
|
||||||
import Tr from '../../utils/language'
|
import Tr from '../../utils/language'
|
||||||
|
|
||||||
import './TopBar.css'
|
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 {
|
interface IProps {
|
||||||
optFunc: () => void
|
children?: React.ReactNode | React.ReactNode[]
|
||||||
downFunc: () => void
|
|
||||||
gameFunc: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
@@ -110,15 +105,7 @@ export default class TopBar extends React.Component<IProps, IState> {
|
|||||||
<div id="minBtn" onClick={this.handleMinimize} className="TopButton">
|
<div id="minBtn" onClick={this.handleMinimize} className="TopButton">
|
||||||
<img src={minIcon} alt="minimize" />
|
<img src={minIcon} alt="minimize" />
|
||||||
</div>
|
</div>
|
||||||
<div id="settingsBtn" onClick={this.props.optFunc} className="TopButton">
|
{this.props.children}
|
||||||
<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> */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ interface IProps {
|
|||||||
label?: string
|
label?: string
|
||||||
checked: boolean
|
checked: boolean
|
||||||
onChange: () => void
|
onChange: () => void
|
||||||
id: string
|
id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ export default class DirInput extends React.Component<IProps, IState> {
|
|||||||
directory: true,
|
directory: true,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
console.log(this.props.openFolder)
|
|
||||||
path = await open({
|
path = await open({
|
||||||
filters: [{ name: 'Files', extensions: this.props.extensions || ['*'] }],
|
filters: [{ name: 'Files', extensions: this.props.extensions || ['*'] }],
|
||||||
defaultPath: this.props.openFolder,
|
defaultPath: this.props.openFolder,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import './ProgressBar.css'
|
|||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
downloadManager: DownloadHandler
|
downloadManager: DownloadHandler
|
||||||
|
withStats?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
@@ -70,11 +71,13 @@ export default class ProgressBar extends React.Component<IProps, IState> {
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{(this.props.withStats === undefined || this.props.withStats) && (
|
||||||
<div className="MainProgressText">
|
<div className="MainProgressText">
|
||||||
<Tr text="main.files_downloading" /> {this.state.files} ({this.state.speed})
|
<Tr text="main.files_downloading" /> {this.state.files} ({this.state.speed})
|
||||||
<br />
|
<br />
|
||||||
<Tr text="main.files_extracting" /> {this.state.extracting}
|
<Tr text="main.files_extracting" /> {this.state.extracting}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,8 +111,9 @@ export default class Downloads extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
async downloadGrasscutterStableRepo() {
|
async downloadGrasscutterStableRepo() {
|
||||||
const folder = await this.getGrasscutterFolder()
|
const folder = await this.getGrasscutterFolder()
|
||||||
this.props.downloadManager.addDownload(STABLE_REPO_DOWNLOAD, folder + '\\grasscutter_repo.zip', () => {
|
this.props.downloadManager.addDownload(STABLE_REPO_DOWNLOAD, folder + '\\grasscutter_repo.zip', async () => {
|
||||||
unzip(folder + '\\grasscutter_repo.zip', folder + '\\', this.toggleButtons)
|
await unzip(folder + '\\grasscutter_repo.zip', folder + '\\', true)
|
||||||
|
this.toggleButtons()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.toggleButtons()
|
this.toggleButtons()
|
||||||
@@ -120,8 +121,9 @@ export default class Downloads extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
async downloadGrasscutterDevRepo() {
|
async downloadGrasscutterDevRepo() {
|
||||||
const folder = await this.getGrasscutterFolder()
|
const folder = await this.getGrasscutterFolder()
|
||||||
this.props.downloadManager.addDownload(DEV_REPO_DOWNLOAD, folder + '\\grasscutter_repo.zip', () => {
|
this.props.downloadManager.addDownload(DEV_REPO_DOWNLOAD, folder + '\\grasscutter_repo.zip', async () => {
|
||||||
unzip(folder + '\\grasscutter_repo.zip', folder + '\\', this.toggleButtons)
|
await unzip(folder + '\\grasscutter_repo.zip', folder + '\\', true)
|
||||||
|
this.toggleButtons()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.toggleButtons()
|
this.toggleButtons()
|
||||||
@@ -129,8 +131,9 @@ export default class Downloads extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
async downloadGrasscutterStable() {
|
async downloadGrasscutterStable() {
|
||||||
const folder = await this.getGrasscutterFolder()
|
const folder = await this.getGrasscutterFolder()
|
||||||
this.props.downloadManager.addDownload(STABLE_DOWNLOAD, folder + '\\grasscutter.zip', () => {
|
this.props.downloadManager.addDownload(STABLE_DOWNLOAD, folder + '\\grasscutter.zip', async () => {
|
||||||
unzip(folder + '\\grasscutter.zip', folder + '\\', this.toggleButtons)
|
await unzip(folder + '\\grasscutter.zip', folder + '\\', true)
|
||||||
|
this.toggleButtons
|
||||||
})
|
})
|
||||||
|
|
||||||
// Also add repo download
|
// Also add repo download
|
||||||
@@ -141,8 +144,9 @@ export default class Downloads extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
async downloadGrasscutterLatest() {
|
async downloadGrasscutterLatest() {
|
||||||
const folder = await this.getGrasscutterFolder()
|
const folder = await this.getGrasscutterFolder()
|
||||||
this.props.downloadManager.addDownload(DEV_DOWNLOAD, folder + '\\grasscutter.zip', () => {
|
this.props.downloadManager.addDownload(DEV_DOWNLOAD, folder + '\\grasscutter.zip', async () => {
|
||||||
unzip(folder + '\\grasscutter.zip', folder + '\\', this.toggleButtons)
|
await unzip(folder + '\\grasscutter.zip', folder + '\\', true)
|
||||||
|
this.toggleButtons()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Also add repo download
|
// Also add repo download
|
||||||
@@ -165,7 +169,7 @@ export default class Downloads extends React.Component<IProps, IState> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await unzip(folder + '\\resources.zip', folder + '\\', () => {
|
await unzip(folder + '\\resources.zip', folder + '\\', true)
|
||||||
// Rename folder to resources
|
// Rename folder to resources
|
||||||
invoke('rename', {
|
invoke('rename', {
|
||||||
path: folder + '\\Resources',
|
path: folder + '\\Resources',
|
||||||
@@ -174,7 +178,6 @@ export default class Downloads extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
this.toggleButtons()
|
this.toggleButtons()
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
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,13 +45,12 @@ export default class Downloads extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
async downloadGame() {
|
async downloadGame() {
|
||||||
const folder = this.state.gameDownloadFolder
|
const folder = this.state.gameDownloadFolder
|
||||||
this.props.downloadManager.addDownload(GAME_DOWNLOAD, folder + '\\game.zip', () => {
|
this.props.downloadManager.addDownload(GAME_DOWNLOAD, folder + '\\game.zip', async () => {
|
||||||
unzip(folder + '\\game.zip', folder + '\\', () => {
|
await unzip(folder + '\\game.zip', folder + '\\', true)
|
||||||
this.setState({
|
this.setState({
|
||||||
gameDownloading: false,
|
gameDownloading: false,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
gameDownloading: true,
|
gameDownloading: true,
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ interface IState {
|
|||||||
|
|
||||||
// Swag stuff
|
// Swag stuff
|
||||||
akebi_path: string
|
akebi_path: string
|
||||||
|
migoto_path: string
|
||||||
|
reshade_path: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Options extends React.Component<IProps, IState> {
|
export default class Options extends React.Component<IProps, IState> {
|
||||||
@@ -61,12 +63,15 @@ export default class Options extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
// Swag stuff
|
// Swag stuff
|
||||||
akebi_path: '',
|
akebi_path: '',
|
||||||
|
migoto_path: '',
|
||||||
|
reshade_path: '',
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setGameExecutable = this.setGameExecutable.bind(this)
|
this.setGameExecutable = this.setGameExecutable.bind(this)
|
||||||
this.setGrasscutterJar = this.setGrasscutterJar.bind(this)
|
this.setGrasscutterJar = this.setGrasscutterJar.bind(this)
|
||||||
this.setJavaPath = this.setJavaPath.bind(this)
|
this.setJavaPath = this.setJavaPath.bind(this)
|
||||||
this.setAkebi = this.setAkebi.bind(this)
|
this.setAkebi = this.setAkebi.bind(this)
|
||||||
|
this.setMigoto = this.setMigoto.bind(this)
|
||||||
this.toggleGrasscutterWithGame = this.toggleGrasscutterWithGame.bind(this)
|
this.toggleGrasscutterWithGame = this.toggleGrasscutterWithGame.bind(this)
|
||||||
this.setCustomBackground = this.setCustomBackground.bind(this)
|
this.setCustomBackground = this.setCustomBackground.bind(this)
|
||||||
this.toggleEncryption = this.toggleEncryption.bind(this)
|
this.toggleEncryption = this.toggleEncryption.bind(this)
|
||||||
@@ -101,6 +106,8 @@ export default class Options extends React.Component<IProps, IState> {
|
|||||||
|
|
||||||
// Swag stuff
|
// Swag stuff
|
||||||
akebi_path: config.akebi_path || '',
|
akebi_path: config.akebi_path || '',
|
||||||
|
migoto_path: config.migoto_path || '',
|
||||||
|
reshade_path: config.reshade_path || '',
|
||||||
})
|
})
|
||||||
|
|
||||||
this.forceUpdate()
|
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) {
|
async setLanguage(value: string) {
|
||||||
await setConfigOption('language', value)
|
await setConfigOption('language', value)
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
@@ -204,7 +227,6 @@ export default class Options extends React.Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async restoreMetadata() {
|
async restoreMetadata() {
|
||||||
console.log(this.props)
|
|
||||||
await meta.restoreMetadata(this.props.downloadManager)
|
await meta.restoreMetadata(this.props.downloadManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,10 +335,26 @@ export default class Options extends React.Component<IProps, IState> {
|
|||||||
<div className="OptionLabel" id="menuOptionsLabelAkebi">
|
<div className="OptionLabel" id="menuOptionsLabelAkebi">
|
||||||
<Tr text="swag.akebi" />
|
<Tr text="swag.akebi" />
|
||||||
</div>
|
</div>
|
||||||
<div className="OptionValue" id="menuOptionsDirMigoto">
|
<div className="OptionValue" id="menuOptionsDirAkebi">
|
||||||
<DirInput onChange={this.setAkebi} value={this.state?.akebi_path} extensions={['exe']} />
|
<DirInput onChange={this.setAkebi} value={this.state?.akebi_path} extensions={['exe']} />
|
||||||
</div>
|
</div>
|
||||||
</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
|
// Swag stuff
|
||||||
akebi_path?: string
|
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> {
|
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 { invoke } from '@tauri-apps/api'
|
||||||
import { listen } from '@tauri-apps/api/event'
|
import { listen } from '@tauri-apps/api/event'
|
||||||
|
|
||||||
export function unzip(file: string, dest: string, onFinish?: () => void) {
|
interface UnzipPayload {
|
||||||
|
file: string
|
||||||
|
new_folder: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unzip(
|
||||||
|
file: string,
|
||||||
|
dest: string,
|
||||||
|
topLevelStrip?: boolean,
|
||||||
|
folderIfLoose?: boolean
|
||||||
|
): Promise<UnzipPayload> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
invoke('unzip', {
|
invoke('unzip', {
|
||||||
zipfile: file,
|
zipfile: file,
|
||||||
destpath: dest,
|
destpath: dest,
|
||||||
|
topLevelStrip,
|
||||||
|
folderIfLoose,
|
||||||
})
|
})
|
||||||
|
|
||||||
listen('extract_end', ({ payload }) => {
|
listen('extract_end', ({ payload }) => {
|
||||||
if (payload === file && onFinish) {
|
// @ts-expect-error Payload is an object
|
||||||
onFinish()
|
if (payload?.file === file) {
|
||||||
|
resolve(payload as UnzipPayload)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user