mirror of
https://github.com/Grasscutters/Cultivation.git
synced 2025-12-14 08:04:52 +01:00
Compare commits
39 Commits
custom-opt
...
resource_c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0c545d032 | ||
|
|
88dd7b854f | ||
|
|
e159bc2cdb | ||
|
|
cc4600ec77 | ||
|
|
f37e44a88c | ||
|
|
4f806efc93 | ||
|
|
083de896b3 | ||
|
|
9c64c1f282 | ||
|
|
aa10a908ad | ||
|
|
bae193050f | ||
|
|
fc5ffae1e2 | ||
|
|
ab0e05ffe1 | ||
|
|
88a1740b91 | ||
|
|
f24f3af377 | ||
|
|
abafc94379 | ||
|
|
b2453e7c4d | ||
|
|
4fc90ee333 | ||
|
|
0ec8782f48 | ||
|
|
33c67eef06 | ||
|
|
b3585927ca | ||
|
|
4c9dad49c4 | ||
|
|
960fcae647 | ||
|
|
4172ee9106 | ||
|
|
8566e7f35e | ||
|
|
0b2296f918 | ||
|
|
6f997a38d3 | ||
|
|
cd128741b4 | ||
|
|
174a7b5163 | ||
|
|
e75474fde7 | ||
|
|
c3119ce7a7 | ||
|
|
6c4b546de2 | ||
|
|
30388eb6a9 | ||
|
|
3008f50e1f | ||
|
|
99293ad7cf | ||
|
|
afdbc5ad80 | ||
|
|
1c0edd2bcd | ||
|
|
8ac4d063a4 | ||
|
|
bb9a044e05 | ||
|
|
4f63e55a28 |
@@ -33,7 +33,6 @@
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"no-explicit-any": "off"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ Consider Cultivation to be the bleeding-edge version of GrassClipper.
|
||||
During this open-beta testing period, **helpful issues are appreciated**, while unhelpful ones will be closed.
|
||||
|
||||
## Fair Warning
|
||||
Cultivation is **VERY MUCH IN BETA** and a majority of features do not work.\
|
||||
There are **no official releases of Cultivation**. You are **required** to build the application from **scratch**.\
|
||||
Please do **NOT install, download, or use pre-compiled versions of Cultivation**. Only use releases from this GitHub repository.
|
||||
Cultivation is **VERY MUCH IN BETA**.
|
||||
There are **no official releases of Cultivation**. You are **required** to build the application from **scratch** unless you want to deal with the alpha state of the current builds.
|
||||
Please do **NOT install, download, or use pre-compiled versions of Cultivation found elsewhere**. Only use releases from this GitHub repository.
|
||||
|
||||
# Cultivation
|
||||
A game launcher designed to easily proxy traffic from anime game to private servers.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cultivation",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.0.0-rc.5",
|
||||
|
||||
@@ -12,9 +12,7 @@
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<title>Cultivation</title>
|
||||
<script src="%PUBLIC_URL%/theme-engine.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Passes a message through to the React backend.
|
||||
* @param type The message type.
|
||||
* @param data The message data.
|
||||
*/
|
||||
function passthrough(type, data) {
|
||||
document.dispatchEvent(new CustomEvent('domMessage', {
|
||||
type, msg: data
|
||||
}))
|
||||
}
|
||||
|
||||
function setConfigValue(key, value) {
|
||||
passthrough('updateConfig', {setting: key, value})
|
||||
}
|
||||
1279
src-tauri/Cargo.lock
generated
1279
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "cultivation"
|
||||
version = "1.0.1"
|
||||
version = "0.1.0"
|
||||
description = "A custom launcher for anime game."
|
||||
authors = ["KingRainbow44", "SpikeHD"]
|
||||
license = "Apache-2.0"
|
||||
license = ""
|
||||
repository = "https://github.com/Grasscutters/Cultivation.git"
|
||||
default-run = "cultivation"
|
||||
edition = "2021"
|
||||
@@ -14,22 +14,26 @@ rust-version = "1.57"
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.0.0-rc.8", features = [] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
is_elevated = "0.1.2"
|
||||
registry = "1.2.1"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
sudo = "0.6.0"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.0.0-rc.9", features = ["api-all", "updater"] }
|
||||
tauri = { version = "1.0.0-rc.9", features = ["api-all"] }
|
||||
|
||||
# Access system process info.
|
||||
sysinfo = "0.23.12"
|
||||
sysinfo = "0.24.6"
|
||||
|
||||
# ZIP-archive library.
|
||||
zip-extract = "0.1.1"
|
||||
zip = "0.6.2"
|
||||
|
||||
# For creating a "global" downloads list.
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
# Access to the Windows Registry.
|
||||
registry = "1.2.1"
|
||||
once_cell = "1.13.0"
|
||||
|
||||
# Program opener.
|
||||
open = "2.1.2"
|
||||
@@ -38,10 +42,6 @@ duct = "0.13.5"
|
||||
# Serialization.
|
||||
serde_json = "1"
|
||||
|
||||
# System process elevation.
|
||||
is_elevated = "0.1.2"
|
||||
runas = "0.2.1"
|
||||
|
||||
# Dependencies for the HTTP(S) proxy.
|
||||
http = "0.2"
|
||||
hudsucker = "0.17.2"
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"game_exec": "Set Game Executable",
|
||||
"game_version": "Set Game Version",
|
||||
"emergency_metadata": "Emergency Metadata Restore",
|
||||
"grasscutter_jar": "Set Grasscutter JAR",
|
||||
"toggle_encryption": "Toggle Encryption",
|
||||
"java_path": "Set Custom Java Path",
|
||||
@@ -57,5 +59,8 @@
|
||||
"gc_stable_data": "Download the current stable Grasscutter data files, which does not come with a jar file. This is useful for updating.",
|
||||
"gc_dev_data": "Download the latest development Grasscutter data files, which does not come with a jar file. This is useful for updating.",
|
||||
"resources": "These are also required to run a Grasscutter server. This button will be grey if you have an existing resources folder with contents inside"
|
||||
},
|
||||
"swag": {
|
||||
"akebi": "Set Akebi Executable"
|
||||
}
|
||||
}
|
||||
61
src-tauri/lang/vi.json
Normal file
61
src-tauri/lang/vi.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"lang_name": "Tiếng Việt",
|
||||
"main": {
|
||||
"title": "Cultivation",
|
||||
"launch_button": "Khởi Chạy",
|
||||
"gc_enable": "Kết nối đến Grasscutter",
|
||||
"https_enable": "Sử dụng HTTPS",
|
||||
"ip_placeholder": "Địa chỉ máy chủ...",
|
||||
"port_placeholder": "Cổng...",
|
||||
"files_downloading": "Đang tải file: ",
|
||||
"files_extracting": "Đang giải nén tệp tin: "
|
||||
},
|
||||
"options": {
|
||||
"enabled": "Bật",
|
||||
"disabled": "Tắt",
|
||||
"game_exec": "Đường dẫn đến GenshinImpact.exe",
|
||||
"grasscutter_jar": "Đường dẫn đến Grasscutter.jar",
|
||||
"toggle_encryption": "Bật/Tắt mã hoá",
|
||||
"java_path": "Đường dẫn Java tuỳ chỉnh",
|
||||
"grasscutter_with_game": "Tự động khởi chạy Grasscutter cùng game",
|
||||
"language": "Chọn ngôn ngữ",
|
||||
"background": "Ảnh nền tuỳ chỉnh (đường dẫn hoặc tệp tin ảnh)",
|
||||
"theme": "Chọn giao diện"
|
||||
},
|
||||
"downloads": {
|
||||
"grasscutter_stable_data": "Tải xuống dữ liệu Grasscutter bản ổn định",
|
||||
"grasscutter_latest_data": "Tải xuống dữ liệu Grasscutter bản mới nhất",
|
||||
"grasscutter_stable_data_update": "Cập nhật dữ liệu Grasscutter bản ổn định",
|
||||
"grasscutter_latest_data_update": "Cập nhật dữ liệu Grasscutter bản mới nhất",
|
||||
"grasscutter_stable": "Tải xuống Grasscutter phiên bản ổn định",
|
||||
"grasscutter_latest": "Tải xuống Grasscutter phiển bản mới nhất",
|
||||
"grasscutter_stable_update": "Cập nhật Grasscutter ổn định",
|
||||
"grasscutter_latest_update": "Cập nhật Grasscutter mới nhất",
|
||||
"resources": "Tải xuống tài nguyên cho Grasscutter"
|
||||
},
|
||||
"download_status": {
|
||||
"downloading": "Đang tải",
|
||||
"extracting": "Đang giải nén",
|
||||
"error": "Lỗi",
|
||||
"finished": "Đã xong",
|
||||
"stopped": "Đã dừng"
|
||||
},
|
||||
"components": {
|
||||
"select_file": "Chọn tệp tin hoặc thư mục...",
|
||||
"select_folder": "Chọn thư mục...",
|
||||
"download": "Tải xuống"
|
||||
},
|
||||
"news": {
|
||||
"latest_commits": "Cập nhật gần đây",
|
||||
"latest_version": "Phiên bản mới nhất"
|
||||
},
|
||||
"help": {
|
||||
"port_help_text": "Đảm bảo đây là cổng của server Dispatch, không phải cổng của server Game. Thường là '443'.",
|
||||
"game_help_text": "Bạn không cần phải sử dụng một bản sao riêng để chơi với Grasscutter. Việc này chỉ xảy ra nếu bạn hạ phiên bản xuống 2.6 hoặc chưa cài đặt trò chơi.",
|
||||
"gc_stable_jar": "Tải xuống phiên bản ổn định của Grasscutter, bảo gồm file jar và các file dữ liệu.",
|
||||
"gc_dev_jar": "Tải xuống phiên bản phát triển mới nhất của Grasscutter, bảo gồm file jar và các file dữ liệu.",
|
||||
"gc_stable_data": "Tải xuống bản ổn định các tệp dữ liệu của Grasscutter, không bao gồm file jar. Phù hợp khi cập nhật.",
|
||||
"gc_dev_data": "Tải xuống bản phát triển mới nhất các tệp dữ liệu của Grasscutter, không bao gồm file jar. Phù hợp khi cập nhật.",
|
||||
"resources": "Chúng được yêu cầu để chạy máy chủ Grasscutter. Nút này sẽ có màu xám nếu bạn có một thư mục tài nguyên có nội dung bên trong"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use lazy_static::lazy_static;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use std::sync::Mutex;
|
||||
use std::cmp::min;
|
||||
@@ -8,12 +8,7 @@ use std::io::Write;
|
||||
use futures_util::StreamExt;
|
||||
|
||||
// This will create a downloads list that will be used to check if we should continue downloading the file
|
||||
lazy_static! {
|
||||
static ref DOWNLOADS: Mutex<Vec<String>> = {
|
||||
let m = Vec::new();
|
||||
Mutex::new(m)
|
||||
};
|
||||
}
|
||||
static DOWNLOADS: Lazy<Mutex<Vec<String>>> = Lazy::new(|| Mutex::new(Vec::new()));
|
||||
|
||||
// Lots of help from: https://gist.github.com/giuliano-oliveira/4d11d6b3bb003dba3a1b53f43d81b30d
|
||||
// and docs ofc
|
||||
@@ -59,17 +54,17 @@ pub async fn download_file(window: tauri::Window, url: &str, path: &str) -> Resu
|
||||
let chunk = match item {
|
||||
Ok(itm) => itm,
|
||||
Err(e) => {
|
||||
emit_download_err(window, format!("Error while downloading file"), path);
|
||||
emit_download_err(window, "Error while downloading file".to_string(), path);
|
||||
return Err(format!("Error while downloading file: {}", e));
|
||||
}
|
||||
};
|
||||
let vect = &chunk.to_vec()[..];
|
||||
|
||||
// Write bytes
|
||||
match file.write_all(&vect) {
|
||||
match file.write_all(vect) {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
emit_download_err(window, format!("Error while writing file"), path);
|
||||
emit_download_err(window, "Error while writing file".to_string(), path);
|
||||
return Err(format!("Error while writing file: {}", e));
|
||||
}
|
||||
}
|
||||
@@ -78,7 +73,7 @@ pub async fn download_file(window: tauri::Window, url: &str, path: &str) -> Resu
|
||||
let new = min(downloaded + (chunk.len() as u64), total_size);
|
||||
downloaded = new;
|
||||
|
||||
total_downloaded = total_downloaded + chunk.len() as u64;
|
||||
total_downloaded += chunk.len() as u64;
|
||||
|
||||
let mut res_hash = std::collections::HashMap::new();
|
||||
|
||||
@@ -110,15 +105,15 @@ pub async fn download_file(window: tauri::Window, url: &str, path: &str) -> Resu
|
||||
window.emit("download_finished", &path).unwrap();
|
||||
|
||||
// We are done
|
||||
return Ok(());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn emit_download_err(window: tauri::Window, msg: std::string::String, path: &str) {
|
||||
pub fn emit_download_err(window: tauri::Window, msg: String, path: &str) {
|
||||
let mut res_hash = std::collections::HashMap::new();
|
||||
|
||||
res_hash.insert(
|
||||
"error".to_string(),
|
||||
msg.to_string(),
|
||||
msg,
|
||||
);
|
||||
|
||||
res_hash.insert(
|
||||
|
||||
@@ -5,31 +5,51 @@ pub fn rename(path: String, new_name: String) {
|
||||
let mut new_path = path.clone();
|
||||
|
||||
// Check if file/folder to replace exists
|
||||
if !fs::metadata(&path).is_ok() {
|
||||
if fs::metadata(&path).is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if path uses forward or back slashes
|
||||
if new_path.contains("\\") {
|
||||
new_path = path.replace("\\", "/");
|
||||
if new_path.contains('\\') {
|
||||
new_path = path.replace('\\', "/");
|
||||
}
|
||||
|
||||
let path_replaced = &path.replace(&new_path.split("/").last().unwrap(), &new_name);
|
||||
let path_replaced = &path.replace(&new_path.split('/').last().unwrap(), &new_name);
|
||||
|
||||
fs::rename(path, &path_replaced).unwrap();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn dir_exists(path: &str) -> bool {
|
||||
return fs::metadata(&path).is_ok();
|
||||
fs::metadata(&path).is_ok()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn dir_is_empty(path: &str) -> bool {
|
||||
return fs::read_dir(&path).unwrap().count() == 0;
|
||||
fs::read_dir(&path).unwrap().count() == 0
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn dir_delete(path: &str) {
|
||||
fs::remove_dir_all(path).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn copy_file(path: String, new_path: String) -> bool {
|
||||
let filename = &path.split("/").last().unwrap();
|
||||
let mut new_path_buf = std::path::PathBuf::from(&new_path);
|
||||
|
||||
// If the new path doesn't exist, create it.
|
||||
if !dir_exists(new_path_buf.pop().to_string().as_str()) {
|
||||
std::fs::create_dir_all(&new_path).unwrap();
|
||||
}
|
||||
|
||||
// Copy old to new
|
||||
match std::fs::copy(&path, format!("{}/{}", new_path, filename)) {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
println!("Failed to copy file: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,13 @@ pub async fn get_lang(window: tauri::Window, lang: String) -> String {
|
||||
|
||||
// Send contents of language file back
|
||||
let lang_path: PathBuf = [&install_location(), "lang", &format!("{}.json", lang)].iter().collect();
|
||||
let contents = match std::fs::read_to_string(&lang_path) {
|
||||
match std::fs::read_to_string(&lang_path) {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
emit_lang_err(window, format!("Failed to read language file: {}", e));
|
||||
return "".to_string();
|
||||
"".to_string()
|
||||
}
|
||||
};
|
||||
|
||||
return contents;
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -23,9 +21,9 @@ pub async fn get_languages() -> std::collections::HashMap<String, String> {
|
||||
// for each lang file, set the key as the filename and the value as the lang_name contained in the file
|
||||
let mut languages = std::collections::HashMap::new();
|
||||
|
||||
let mut lang_files = std::fs::read_dir(Path::new(&install_location()).join("lang")).unwrap();
|
||||
let lang_files = std::fs::read_dir(Path::new(&install_location()).join("lang")).unwrap();
|
||||
|
||||
while let Some(entry) = lang_files.next() {
|
||||
for entry in lang_files {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
let filename = path.file_name().unwrap().to_str().unwrap();
|
||||
@@ -41,15 +39,15 @@ pub async fn get_languages() -> std::collections::HashMap<String, String> {
|
||||
languages.insert(filename.to_string(), content);
|
||||
}
|
||||
|
||||
return languages;
|
||||
languages
|
||||
}
|
||||
|
||||
pub fn emit_lang_err(window: tauri::Window, msg: std::string::String) {
|
||||
pub fn emit_lang_err(window: tauri::Window, msg: String) {
|
||||
let mut res_hash = std::collections::HashMap::new();
|
||||
|
||||
res_hash.insert(
|
||||
"error".to_string(),
|
||||
msg.to_string(),
|
||||
msg,
|
||||
);
|
||||
|
||||
window.emit("lang_error", &res_hash).unwrap();
|
||||
|
||||
@@ -3,9 +3,8 @@ all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{sync::Mutex, collections::HashMap};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use std::thread;
|
||||
use sysinfo::{System, SystemExt};
|
||||
@@ -20,21 +19,9 @@ mod lang;
|
||||
mod proxy;
|
||||
mod web;
|
||||
|
||||
lazy_static! {
|
||||
static ref WATCH_GAME_PROCESS: Mutex<String> = {
|
||||
let m = "".to_string();
|
||||
Mutex::new(m)
|
||||
};
|
||||
}
|
||||
static WATCH_GAME_PROCESS: Lazy<Mutex<String>> = Lazy::new(|| Mutex::new(String::new()));
|
||||
|
||||
fn main() {
|
||||
// Start the game process watcher.
|
||||
process_watcher();
|
||||
|
||||
// Make BG folder if it doesn't exist.
|
||||
let bg_folder: PathBuf = [&system_helpers::install_location(), "bg"].iter().collect();
|
||||
std::fs::create_dir_all(&bg_folder).unwrap();
|
||||
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
enable_process_watcher,
|
||||
@@ -48,7 +35,6 @@ fn main() {
|
||||
system_helpers::run_program,
|
||||
system_helpers::run_jar,
|
||||
system_helpers::open_in_browser,
|
||||
system_helpers::copy_file,
|
||||
system_helpers::install_location,
|
||||
system_helpers::is_elevated,
|
||||
proxy::set_proxy_addr,
|
||||
@@ -58,63 +44,64 @@ fn main() {
|
||||
file_helpers::dir_exists,
|
||||
file_helpers::dir_is_empty,
|
||||
file_helpers::dir_delete,
|
||||
file_helpers::copy_file,
|
||||
downloader::download_file,
|
||||
downloader::stop_download,
|
||||
lang::get_lang,
|
||||
lang::get_languages,
|
||||
web::valid_url
|
||||
web::valid_url,
|
||||
web::web_get
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
fn process_watcher() {
|
||||
// Every 5 seconds, see if the game process is still running.
|
||||
// If it is not, then we assume the game has closed and disable the proxy
|
||||
// to prevent any requests from being sent to the game.
|
||||
|
||||
// Start a thread so as to not block the main thread.
|
||||
thread::spawn(|| {
|
||||
let mut system = System::new_all();
|
||||
|
||||
loop {
|
||||
// Refresh system info
|
||||
system.refresh_all();
|
||||
|
||||
// Grab the game process name
|
||||
let proc = WATCH_GAME_PROCESS.lock().unwrap().to_string();
|
||||
|
||||
if !&proc.is_empty() {
|
||||
let proc_with_name = system.processes_by_exact_name(&proc);
|
||||
let mut exists = false;
|
||||
|
||||
for _p in proc_with_name {
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// If the game process closes, disable the proxy.
|
||||
if !exists {
|
||||
*WATCH_GAME_PROCESS.lock().unwrap() = "".to_string();
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
thread::sleep(std::time::Duration::from_secs(5));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn is_game_running() -> bool {
|
||||
// Grab the game process name
|
||||
let proc = WATCH_GAME_PROCESS.lock().unwrap().to_string();
|
||||
|
||||
return !proc.is_empty();
|
||||
!proc.is_empty()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn enable_process_watcher(process: String) {
|
||||
fn enable_process_watcher(window: tauri::Window,process: String) {
|
||||
*WATCH_GAME_PROCESS.lock().unwrap() = process;
|
||||
|
||||
window.listen("disable_process_watcher", |_e| {
|
||||
*WATCH_GAME_PROCESS.lock().unwrap() = "".to_string();
|
||||
});
|
||||
|
||||
println!("Starting process watcher...");
|
||||
|
||||
thread::spawn(move || {
|
||||
let mut system = System::new_all();
|
||||
|
||||
loop {
|
||||
thread::sleep(std::time::Duration::from_secs(5));
|
||||
|
||||
// Refresh system info
|
||||
system.refresh_all();
|
||||
|
||||
// Grab the game process name
|
||||
let proc = WATCH_GAME_PROCESS.lock().unwrap().to_string();
|
||||
|
||||
if !proc.is_empty() {
|
||||
let mut proc_with_name = system.processes_by_exact_name(&proc);
|
||||
let exists = proc_with_name.next().is_some();
|
||||
|
||||
// If the game process closes, disable the proxy.
|
||||
if !exists {
|
||||
println!("Game closed");
|
||||
|
||||
*WATCH_GAME_PROCESS.lock().unwrap() = "".to_string();
|
||||
disconnect();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -140,11 +127,8 @@ fn disconnect() {
|
||||
|
||||
#[tauri::command]
|
||||
async fn req_get(url: String) -> String {
|
||||
// Send a GET request to the specified URL.
|
||||
let response = web::query(&url.to_string()).await;
|
||||
|
||||
// Send the response body back to the client.
|
||||
return response;
|
||||
// Send a GET request to the specified URL and send the response body back to the client.
|
||||
web::query(&url.to_string()).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -180,7 +164,7 @@ async fn get_theme_list(data_dir: String) -> Vec<HashMap<String, String>> {
|
||||
}
|
||||
}
|
||||
|
||||
return themes;
|
||||
themes
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -204,7 +188,7 @@ async fn get_bg_file(bg_path: String, appdata: String) -> String {
|
||||
}
|
||||
|
||||
// Now we check if the bg folder, which is one directory above the game_path, exists.
|
||||
let bg_img_path = format!("{}\\{}", bg_path.clone().to_string(), file_name.as_str());
|
||||
let bg_img_path = format!("{}\\{}", &bg_path, &file_name);
|
||||
|
||||
// If it doesn't, then we do not have backgrounds to grab.
|
||||
if !file_helpers::dir_exists(&bg_path) {
|
||||
@@ -220,15 +204,15 @@ async fn get_bg_file(bg_path: String, appdata: String) -> String {
|
||||
// The image exists, lets copy it to our local '\bg' folder.
|
||||
let bg_img_path_local = format!("{}\\bg\\{}", copy_loc, file_name.as_str());
|
||||
|
||||
return match std::fs::copy(bg_img_path, bg_img_path_local) {
|
||||
match std::fs::copy(bg_img_path, bg_img_path_local) {
|
||||
Ok(_) => {
|
||||
// Copy was successful, lets return true.
|
||||
format!("{}\\{}", copy_loc, response_data.bg_file.as_str())
|
||||
format!("{}\\{}", copy_loc, response_data.bg_file)
|
||||
}
|
||||
Err(e) => {
|
||||
// Copy failed, lets return false
|
||||
println!("Failed to copy background image: {}", e);
|
||||
"".to_string()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* https://github.com/omjadas/hudsucker/blob/main/examples/log.rs
|
||||
*/
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{sync::Mutex, str::FromStr};
|
||||
|
||||
use rcgen::*;
|
||||
@@ -17,11 +17,12 @@ use hudsucker::{
|
||||
use std::fs;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
use registry::{Hive, Data, Security};
|
||||
|
||||
use rustls_pemfile as pemfile;
|
||||
use tauri::http::Uri;
|
||||
use crate::system_helpers::run_command;
|
||||
|
||||
#[cfg(windows)]
|
||||
use registry::{Hive, Data, Security};
|
||||
|
||||
async fn shutdown_signal() {
|
||||
tokio::signal::ctrl_c().await
|
||||
@@ -29,12 +30,7 @@ async fn shutdown_signal() {
|
||||
}
|
||||
|
||||
// Global ver for getting server address.
|
||||
lazy_static! {
|
||||
static ref SERVER: Mutex<String> = {
|
||||
let m = "http://localhost:443".to_string();
|
||||
Mutex::new(m)
|
||||
};
|
||||
}
|
||||
static SERVER: Lazy<Mutex<String>> = Lazy::new(|| Mutex::new("http://localhost:443".to_string()));
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ProxyHandler;
|
||||
@@ -109,37 +105,43 @@ pub async fn create_proxy(proxy_port: u16, certificate_path: String) {
|
||||
/**
|
||||
* Connects to the local HTTP(S) proxy server.
|
||||
*/
|
||||
#[cfg(windows)]
|
||||
pub fn connect_to_proxy(proxy_port: u16) {
|
||||
if cfg!(target_os = "windows") {
|
||||
// Create 'ProxyServer' string.
|
||||
let server_string: String = format!("http=127.0.0.1:{};https=127.0.0.1:{}", proxy_port, proxy_port);
|
||||
// Create 'ProxyServer' string.
|
||||
let server_string: String = format!("http=127.0.0.1:{};https=127.0.0.1:{}", proxy_port, proxy_port);
|
||||
|
||||
// Fetch the 'Internet Settings' registry key.
|
||||
let settings = Hive::CurrentUser.open(r"Software\Microsoft\Windows\CurrentVersion\Internet Settings", Security::Write).unwrap();
|
||||
// Fetch the 'Internet Settings' registry key.
|
||||
let settings = Hive::CurrentUser.open(r"Software\Microsoft\Windows\CurrentVersion\Internet Settings", Security::Write).unwrap();
|
||||
|
||||
// Set registry values.
|
||||
settings.set_value("ProxyServer", &Data::String(server_string.parse().unwrap())).unwrap();
|
||||
settings.set_value("ProxyEnable", &Data::U32(1)).unwrap();
|
||||
}
|
||||
// Set registry values.
|
||||
settings.set_value("ProxyServer", &Data::String(server_string.parse().unwrap())).unwrap();
|
||||
settings.set_value("ProxyEnable", &Data::U32(1)).unwrap();
|
||||
|
||||
println!("Connected to the proxy.");
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn connect_to_proxy(_proxy_port: u16) {
|
||||
println!("Connecting to the proxy is not implemented on this platform.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects from the local HTTP(S) proxy server.
|
||||
*/
|
||||
#[cfg(windows)]
|
||||
pub fn disconnect_from_proxy() {
|
||||
if cfg!(target_os = "windows") {
|
||||
// Fetch the 'Internet Settings' registry key.
|
||||
let settings = Hive::CurrentUser.open(r"Software\Microsoft\Windows\CurrentVersion\Internet Settings", Security::Write).unwrap();
|
||||
// Fetch the 'Internet Settings' registry key.
|
||||
let settings = Hive::CurrentUser.open(r"Software\Microsoft\Windows\CurrentVersion\Internet Settings", Security::Write).unwrap();
|
||||
|
||||
// Set registry values.
|
||||
settings.set_value("ProxyEnable", &Data::U32(0)).unwrap();
|
||||
}
|
||||
// Set registry values.
|
||||
settings.set_value("ProxyEnable", &Data::U32(0)).unwrap();
|
||||
|
||||
println!("Disconnected from proxy.");
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn disconnect_from_proxy() {}
|
||||
|
||||
/*
|
||||
* Generates a private key and certificate used by the certificate authority.
|
||||
* Additionally installs the certificate and private key in the Root CA store.
|
||||
@@ -201,12 +203,19 @@ pub fn generate_ca_files(path: &Path) {
|
||||
/*
|
||||
* Attempts to install the certificate authority's certificate into the Root CA store.
|
||||
*/
|
||||
#[cfg(windows)]
|
||||
pub fn install_ca_files(cert_path: &Path) {
|
||||
if cfg!(target_os = "windows") {
|
||||
run_command("certutil", vec!["-user", "-addstore", "Root", cert_path.to_str().unwrap()]);
|
||||
} else {
|
||||
run_command("security", vec!["add-trusted-cert", "-d", "-r", "trustRoot", "-k", "/Library/Keychains/System.keychain", cert_path.to_str().unwrap()]);
|
||||
}
|
||||
|
||||
crate::system_helpers::run_command("certutil", vec!["-user", "-addstore", "Root", cert_path.to_str().unwrap()]);
|
||||
println!("Installed certificate.");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn install_ca_files(cert_path: &Path) {
|
||||
crate::system_helpers::run_command("security", vec!["add-trusted-cert", "-d", "-r", "trustRoot", "-k", "/Library/Keychains/System.keychain", cert_path.to_str().unwrap()]);
|
||||
println!("Installed certificate.");
|
||||
}
|
||||
|
||||
#[cfg(not(any(windows, target_os = "macos")))]
|
||||
pub fn install_ca_files(_cert_path: &Path) {
|
||||
println!("Certificate installation is not supported on this platform.");
|
||||
}
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
|
||||
use std::thread;
|
||||
use tauri;
|
||||
use open;
|
||||
use duct::cmd;
|
||||
|
||||
use crate::file_helpers;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn run_program(path: String) {
|
||||
// Open the program from the specified path.
|
||||
|
||||
// Open in new thread to prevent blocking.
|
||||
thread::spawn(move || {
|
||||
open::that(&path).unwrap();
|
||||
std::thread::spawn(move || {
|
||||
// Without unwrap_or, this can crash when UAC prompt is denied
|
||||
open::that(&path).unwrap_or(());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,7 +24,7 @@ pub fn run_jar(path: String, execute_in: String, java_path: String) {
|
||||
};
|
||||
|
||||
// Open the program from the specified path.
|
||||
match open::with(format!("/k cd /D \"{}\" & {}", &execute_in, &command).to_string(), "C:\\Windows\\System32\\cmd.exe") {
|
||||
match open::with(format!("/k cd /D \"{}\" & {}", &execute_in, &command), "C:\\Windows\\System32\\cmd.exe") {
|
||||
Ok(_) => (),
|
||||
Err(e) => println!("Failed to open jar ({} from {}): {}", &path, &execute_in, e),
|
||||
};
|
||||
@@ -46,25 +39,6 @@ pub fn open_in_browser(url: String) {
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn copy_file(path: String, new_path: String) -> bool {
|
||||
let filename = &path.split("/").last().unwrap();
|
||||
let mut new_path_buf = std::path::PathBuf::from(&new_path);
|
||||
|
||||
// If the new path doesn't exist, create it.
|
||||
if !file_helpers::dir_exists(new_path_buf.pop().to_string().as_str()) {
|
||||
std::fs::create_dir_all(&new_path).unwrap();
|
||||
}
|
||||
|
||||
// Copy old to new
|
||||
match std::fs::copy(&path, format!("{}/{}", new_path, filename)) {
|
||||
Ok(_) => true,
|
||||
Err(e) => {
|
||||
println!("Failed to copy file: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn install_location() -> String {
|
||||
@@ -76,7 +50,14 @@ pub fn install_location() -> String {
|
||||
return exe_path.to_str().unwrap().to_string();
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[tauri::command]
|
||||
pub fn is_elevated() -> bool {
|
||||
return is_elevated::is_elevated();
|
||||
}
|
||||
is_elevated::is_elevated()
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tauri::command]
|
||||
pub fn is_elevated() -> bool {
|
||||
sudo::check() == sudo::RunningAs::Root
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use zip_extract;
|
||||
use zip;
|
||||
use std::fs::File;
|
||||
use std::path;
|
||||
use std::thread;
|
||||
|
||||
@@ -4,7 +4,7 @@ pub(crate) async fn query(site: &str) -> String {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let response = client.get(site).header(USER_AGENT, "cultivation").send().await.unwrap();
|
||||
return response.text().await.unwrap();
|
||||
response.text().await.unwrap()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -14,5 +14,11 @@ pub(crate) async fn valid_url(url: String) -> bool {
|
||||
|
||||
let response = client.get(url).header(USER_AGENT, "cultivation").send().await.unwrap();
|
||||
|
||||
return response.status().as_str() == "200";
|
||||
response.status().as_str() == "200"
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn web_get(url: String) -> String {
|
||||
// Send a GET request to the specified URL and send the response body back to the client.
|
||||
query(&url).await
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "Cultivation",
|
||||
"version": "1.0.1"
|
||||
"version": "1.0.2"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
@@ -72,7 +72,7 @@
|
||||
"csp": "default-src 'self' https://asset.localhost; img-src 'self'; img-src https://* asset: https://asset.localhost"
|
||||
},
|
||||
"updater": {
|
||||
"active": true,
|
||||
"active": false,
|
||||
"dialog": true,
|
||||
"endpoints": [
|
||||
"https://api.grasscutter.io/cultivation/updater?version={{current_version}}",
|
||||
|
||||
@@ -17,7 +17,6 @@ let isDebug = false;
|
||||
isDebug = await getConfigOption('debug_enabled')
|
||||
})
|
||||
|
||||
// Render the app.
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
{
|
||||
@@ -26,10 +25,5 @@ root.render(
|
||||
</React.StrictMode>
|
||||
)
|
||||
|
||||
// Enable web vitals if needed.
|
||||
import reportWebVitals from './utils/reportWebVitals'
|
||||
isDebug && reportWebVitals(console.log)
|
||||
|
||||
// Setup DOM message passing.
|
||||
import { parseMessageFromDOM } from './utils/dom'
|
||||
document.addEventListener<string>('domMessage', parseMessageFromDOM)
|
||||
isDebug && reportWebVitals(console.log)
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "Example Theme",
|
||||
"version": "420.69",
|
||||
"description": "Show off some of the abilities of the Cultivation theme system",
|
||||
|
||||
"includes": {
|
||||
"_README": "You can include any amount of CSS and JS files here. Paths are relative to the theme directory.",
|
||||
|
||||
"css": ["/index.css"],
|
||||
"js": ["/index.js"]
|
||||
},
|
||||
|
||||
"settings": [
|
||||
{
|
||||
"label": "Example Setting",
|
||||
"type": "input",
|
||||
"className": "Input",
|
||||
|
||||
"data": {
|
||||
"placeholder": "Enter a value",
|
||||
"initialValue": "Change this value"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Example Setting",
|
||||
"type": "checkbox",
|
||||
"className": "Checkbox"
|
||||
}
|
||||
],
|
||||
|
||||
"_README": "These are optional. Including neither will result in the launcher simply using the default background choice.",
|
||||
"customBackgroundPath": "/background/bg.png",
|
||||
"customBackgroundURL": ""
|
||||
}
|
||||
67
src/resources/icons/akebi.svg
Normal file
67
src/resources/icons/akebi.svg
Normal file
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="1024.000000pt" height="1024.000000pt" viewBox="0 0 1024.000000 1024.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,1024.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M7045 10204 c-301 -50 -596 -277 -731 -564 -96 -204 -111 -463 -40
|
||||
-692 16 -52 14 -58 -26 -58 -62 0 -220 -85 -331 -178 -24 -20 -105 -91 -178
|
||||
-157 -74 -66 -177 -156 -229 -200 -208 -175 -629 -573 -1001 -944 -152 -152
|
||||
-286 -284 -297 -294 -18 -17 -27 -17 -75 -8 -96 21 -249 29 -361 20 -321 -26
|
||||
-613 -162 -836 -389 -341 -346 -457 -846 -303 -1301 l36 -106 -66 -100 c-102
|
||||
-155 -212 -343 -303 -518 -353 -683 -556 -1455 -531 -2025 6 -136 29 -341 47
|
||||
-415 36 -156 58 -234 92 -335 47 -137 52 -147 134 -315 90 -182 113 -220 223
|
||||
-375 376 -528 945 -931 1571 -1113 261 -76 439 -94 503 -52 27 17 30 17 105
|
||||
-4 125 -35 209 -50 345 -62 107 -9 152 -8 279 6 118 13 180 15 277 8 214 -14
|
||||
475 -9 566 11 144 31 312 80 395 114 14 6 68 27 120 48 91 36 302 135 365 172
|
||||
180 104 373 265 507 422 346 407 518 977 520 1725 0 287 -10 448 -48 755 -43
|
||||
357 -95 631 -208 1100 -24 102 -47 199 -51 217 -5 26 -1 41 25 78 48 70 108
|
||||
204 132 297 32 118 31 366 0 482 -75 272 -258 503 -507 639 l-77 42 -68 202
|
||||
c-37 112 -70 213 -73 225 -3 13 -14 57 -26 98 -12 41 -33 134 -47 205 -23 115
|
||||
-26 154 -27 345 0 223 1 232 62 580 44 245 54 349 47 490 -10 234 -52 334
|
||||
-206 495 -98 103 -128 150 -167 261 -24 70 -27 94 -28 209 0 135 10 185 58
|
||||
280 49 99 182 235 282 288 179 96 373 112 464 39 62 -49 101 -117 101 -174 0
|
||||
-81 -49 -152 -117 -172 -13 -3 -32 -8 -42 -11 -13 -4 -34 7 -65 34 -26 22 -63
|
||||
45 -84 52 -107 32 -219 -73 -198 -185 7 -37 89 -141 129 -163 137 -76 279 -77
|
||||
428 -3 102 52 190 149 236 263 36 89 42 239 14 326 -36 108 -112 215 -206 286
|
||||
-80 62 -158 92 -265 105 -105 11 -142 11 -245 -6z m-574 -1640 c138 -41 189
|
||||
-186 160 -459 -13 -115 -45 -328 -62 -405 -19 -86 -49 -386 -49 -493 0 -163
|
||||
17 -299 71 -552 7 -36 35 -132 102 -350 l24 -80 -31 -2 c-114 -9 -233 -28
|
||||
-305 -50 -193 -55 -383 -185 -509 -347 -12 -16 -24 -27 -27 -24 -6 5 -22 125
|
||||
-30 223 -3 39 -10 113 -15 165 -10 105 -26 365 -35 595 -5 120 -11 158 -26
|
||||
192 -34 70 -110 104 -187 84 -44 -12 -77 -47 -217 -230 -38 -51 -90 -118 -115
|
||||
-150 -25 -32 -70 -91 -100 -131 -30 -40 -64 -84 -76 -98 l-21 -25 -37 67 c-89
|
||||
159 -229 312 -386 419 l-81 55 213 214 c371 372 775 751 1063 997 94 79 193
|
||||
166 220 192 78 75 185 157 239 184 60 30 136 33 217 9z m-2317 -1783 c351 -93
|
||||
611 -365 682 -712 22 -109 15 -291 -16 -403 -107 -396 -472 -688 -900 -722
|
||||
l-85 -6 70 50 c180 127 282 307 293 513 16 326 -203 611 -533 695 -90 22 -256
|
||||
23 -347 1 -136 -33 -284 -119 -366 -212 -22 -25 -42 -45 -44 -45 -9 0 11 102
|
||||
33 175 104 343 411 610 782 681 104 20 329 12 431 -15z m1310 -406 c21 -268
|
||||
29 -351 51 -545 31 -262 72 -520 120 -765 26 -130 66 -320 75 -360 5 -22 28
|
||||
-128 50 -235 23 -107 45 -213 50 -235 41 -178 113 -614 145 -880 48 -401 57
|
||||
-943 20 -1215 -55 -407 -186 -801 -368 -1102 -107 -178 -168 -262 -258 -355
|
||||
-146 -150 -290 -212 -494 -213 -252 0 -520 107 -800 319 -274 207 -539 609
|
||||
-674 1021 -112 339 -154 603 -154 960 0 278 18 425 82 671 92 348 301 725 615
|
||||
1106 l50 61 87 16 c472 86 871 430 1017 876 42 128 55 211 58 383 l4 168 120
|
||||
157 c169 220 194 252 196 249 1 -1 5 -38 8 -82z m1043 -511 c-144 -112 -216
|
||||
-335 -168 -519 27 -101 69 -171 148 -245 74 -69 141 -104 246 -127 67 -14 90
|
||||
-15 159 -5 145 21 277 102 353 218 60 90 86 178 86 287 0 51 -3 102 -6 112
|
||||
-13 40 26 -17 61 -89 95 -196 84 -447 -28 -633 -183 -303 -569 -423 -917 -284
|
||||
-171 69 -330 226 -398 394 -152 373 46 791 432 913 73 23 82 17 32 -22z
|
||||
m-3393 -1044 c104 -70 239 -133 356 -166 52 -15 97 -28 98 -30 4 -2 -15 -29
|
||||
-83 -124 -60 -83 -171 -254 -217 -335 -185 -323 -305 -691 -349 -1070 -19
|
||||
-158 -16 -519 5 -690 34 -284 98 -554 186 -790 44 -120 179 -400 231 -480 171
|
||||
-266 299 -420 509 -610 50 -45 61 -59 47 -63 -47 -10 -352 117 -535 224 -700
|
||||
409 -1152 1060 -1253 1804 -19 136 -16 466 5 625 78 591 314 1222 678 1813
|
||||
l70 113 87 -84 c48 -46 122 -107 165 -137z m3105 -469 c175 -88 288 -116 481
|
||||
-116 190 0 316 30 470 111 30 16 58 30 61 32 15 7 131 -523 183 -833 64 -382
|
||||
88 -661 88 -1015 -1 -346 -24 -553 -94 -820 -112 -430 -342 -768 -675 -993
|
||||
-139 -94 -386 -208 -643 -297 -67 -24 -174 -50 -255 -64 -94 -17 -350 -22
|
||||
-359 -8 -2 4 16 26 40 47 42 37 171 184 204 231 176 256 269 425 360 654 118
|
||||
295 181 554 221 907 19 168 21 222 16 503 -4 173 -11 369 -16 434 -30 341
|
||||
-103 828 -177 1168 -15 73 -18 108 -7 108 4 0 50 -22 102 -49z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
@@ -33,7 +33,7 @@
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.BottomSection .CheckboxDisplay {
|
||||
.BottomSection .CheckboxDisplay {
|
||||
margin-right: 6px;
|
||||
box-shadow: 0 0 5px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
@@ -62,20 +62,27 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
|
||||
height: 100%;
|
||||
max-height: 60px;
|
||||
}
|
||||
|
||||
#officialPlay {
|
||||
width: 60%
|
||||
.ServerLaunchButtons .BigButton {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
#officialPlay {
|
||||
max-width: 60%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#akebiLaunch,
|
||||
#serverLaunch {
|
||||
width: 5%;
|
||||
}
|
||||
|
||||
.AkebiIcon,
|
||||
.ServerIcon {
|
||||
height: 20px;
|
||||
filter: invert(28%) sepia(28%) saturate(1141%) hue-rotate(352deg) brightness(96%) contrast(88%);
|
||||
|
||||
@@ -8,6 +8,8 @@ import { translate } from '../../utils/language'
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
|
||||
import Server from '../../resources/icons/server.svg'
|
||||
import Akebi from '../../resources/icons/akebi.svg'
|
||||
|
||||
import './ServerLaunchSection.css'
|
||||
import {dataDir} from '@tauri-apps/api/path'
|
||||
|
||||
@@ -29,6 +31,8 @@ interface IState {
|
||||
|
||||
httpsLabel: string;
|
||||
httpsEnabled: boolean;
|
||||
|
||||
swag: boolean;
|
||||
}
|
||||
|
||||
export default class ServerLaunchSection extends React.Component<IProps, IState> {
|
||||
@@ -45,11 +49,13 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
|
||||
portPlaceholder: '',
|
||||
portHelpText: '',
|
||||
httpsLabel: '',
|
||||
httpsEnabled: false
|
||||
httpsEnabled: false,
|
||||
swag: false
|
||||
}
|
||||
|
||||
this.toggleGrasscutter = this.toggleGrasscutter.bind(this)
|
||||
this.playGame = this.playGame.bind(this)
|
||||
this.launchAkebi = this.launchAkebi.bind(this)
|
||||
this.setIp = this.setIp.bind(this)
|
||||
this.setPort = this.setPort.bind(this)
|
||||
this.toggleHttps = this.toggleHttps.bind(this)
|
||||
@@ -69,6 +75,7 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
|
||||
portHelpText: await translate('help.port_help_text'),
|
||||
httpsLabel: await translate('main.https_enable'),
|
||||
httpsEnabled: config.https_enabled || false,
|
||||
swag: config.swag_mode || false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -85,7 +92,7 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
|
||||
await saveConfig(config)
|
||||
}
|
||||
|
||||
async playGame() {
|
||||
async playGame(exe?: string, proc_name?: string) {
|
||||
const config = await getConfig()
|
||||
|
||||
if (!config.game_install_path) return alert('Game path not set!')
|
||||
@@ -107,7 +114,7 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
|
||||
// Set IP
|
||||
await invoke('set_proxy_addr', { addr: (this.state.httpsEnabled ? 'https':'http') + '://' + this.state.ip + ':' + this.state.port })
|
||||
await invoke('enable_process_watcher', {
|
||||
process: game_exe
|
||||
process: proc_name || game_exe
|
||||
})
|
||||
|
||||
// Connect to proxy
|
||||
@@ -133,11 +140,11 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
|
||||
|
||||
// Launch the program
|
||||
const gameExists = await invoke('dir_exists', {
|
||||
path: config.game_install_path
|
||||
path: exe || config.game_install_path
|
||||
})
|
||||
|
||||
if (gameExists) await invoke('run_program', { path: config.game_install_path })
|
||||
else alert('Game not found! At: ' + config.game_install_path)
|
||||
if (gameExists) await invoke('run_program', { path: exe || config.game_install_path })
|
||||
else alert('Game not found! At: ' + (exe || config.game_install_path))
|
||||
}
|
||||
|
||||
async launchServer() {
|
||||
@@ -161,6 +168,16 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
|
||||
})
|
||||
}
|
||||
|
||||
async launchAkebi() {
|
||||
const config = await getConfig()
|
||||
|
||||
// Get game exe from game path, so we can watch it
|
||||
const pathArr = config.game_install_path.replace(/\\/g, '/').split('/')
|
||||
const gameExec = pathArr[pathArr.length - 1]
|
||||
|
||||
await this.playGame(config.akebi_path, gameExec)
|
||||
}
|
||||
|
||||
setIp(text: string) {
|
||||
this.setState({
|
||||
ip: text
|
||||
@@ -205,13 +222,19 @@ export default class ServerLaunchSection extends React.Component<IProps, IState>
|
||||
<Checkbox id="httpsEnable" label={this.state.httpsLabel} onChange={this.toggleHttps} checked={this.state.httpsEnabled} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
<div className="ServerLaunchButtons" id="serverLaunchContainer">
|
||||
<BigButton onClick={this.playGame} id="officialPlay">{this.state.buttonLabel}</BigButton>
|
||||
{
|
||||
this.state.swag && (
|
||||
<BigButton onClick={this.launchAkebi} id="akebiLaunch">
|
||||
<img className="AkebiIcon" id="akebiIcon" src={Akebi} />
|
||||
</BigButton>
|
||||
)
|
||||
}
|
||||
<BigButton onClick={this.launchServer} id="serverLaunch">
|
||||
<img className="ServerIcon" id="serverLaunchIcon" src={Server} />
|
||||
</BigButton>
|
||||
|
||||
@@ -25,4 +25,30 @@
|
||||
#version {
|
||||
margin: 0px 6px;
|
||||
color: #434343;
|
||||
}
|
||||
|
||||
#unassumingButton {
|
||||
font-weight: bold;
|
||||
margin: 0px 6px;
|
||||
color: #141414;
|
||||
|
||||
transition: color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
#unassumingButton:hover {
|
||||
color: #434343;
|
||||
}
|
||||
|
||||
#unassumingButton.spin {
|
||||
color: #fff;
|
||||
animation: spin 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import gameBtn from '../../resources/icons/game.svg'
|
||||
import Tr from '../../utils/language'
|
||||
|
||||
import './TopBar.css'
|
||||
import { getConfig, setConfigOption } from '../../utils/configuration'
|
||||
|
||||
interface IProps {
|
||||
optFunc: () => void;
|
||||
@@ -19,13 +20,21 @@ interface IProps {
|
||||
|
||||
interface IState {
|
||||
version: string;
|
||||
clicks: number;
|
||||
intv: NodeJS.Timeout | null;
|
||||
}
|
||||
|
||||
export default class TopBar extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props)
|
||||
|
||||
this.state = { version: '0.0.0' }
|
||||
this.state = {
|
||||
version: '0.0.0',
|
||||
clicks: 0,
|
||||
intv: null
|
||||
}
|
||||
|
||||
this.activateClick = this.activateClick.bind(this)
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
@@ -41,6 +50,39 @@ export default class TopBar extends React.Component<IProps, IState> {
|
||||
appWindow.minimize()
|
||||
}
|
||||
|
||||
async activateClick() {
|
||||
const config = await getConfig()
|
||||
|
||||
// They already got it, no need to reactivate
|
||||
if (config.swag_mode) return
|
||||
|
||||
if (this.state.clicks === 2) {
|
||||
setTimeout(() => {
|
||||
// Gotta clear it so it goes back to regular colors
|
||||
this.setState({
|
||||
clicks: 0
|
||||
})
|
||||
}, 600)
|
||||
|
||||
// Activate... SWAG MODE
|
||||
await setConfigOption('swag_mode', true)
|
||||
|
||||
// Reload the window
|
||||
window.location.reload()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (this.state.clicks < 3) {
|
||||
this.setState({
|
||||
clicks: this.state.clicks + 1,
|
||||
intv: setTimeout(() => this.setState({ clicks: 0 }), 1500)
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="TopBar" id="topBarContainer" data-tauri-drag-region>
|
||||
@@ -50,6 +92,16 @@ export default class TopBar extends React.Component<IProps, IState> {
|
||||
</span>
|
||||
<span data-tauri-drag-region id="version">{this.state?.version}</span>
|
||||
</div>
|
||||
{
|
||||
/**
|
||||
* HEY YOU
|
||||
*
|
||||
* If you're looking at the source code to find the swag mode thing, that's okay! If you're not, move along...
|
||||
* Just do me a favor and don't go telling everyone about how you found it. If you are just helping someone who
|
||||
* for some reason needs it, that's fine, but not EVERYONE needs it, which is why it exists in the first place.
|
||||
*/
|
||||
}
|
||||
<div id="unassumingButton" className={this.state.clicks === 2 ? 'spin' : ''} onClick={this.activateClick}>?</div>
|
||||
<div className="TopBtns" id="topBarButtonContainer">
|
||||
<div id="closeBtn" onClick={this.handleClose} className='TopButton'>
|
||||
<img src={closeIcon} alt="close" />
|
||||
@@ -63,9 +115,9 @@ export default class TopBar extends React.Component<IProps, IState> {
|
||||
<div id="downloadsBtn" className='TopButton' onClick={this.props.downFunc}>
|
||||
<img src={downBtn} alt="downloads" />
|
||||
</div>
|
||||
{/* <div id="gameBtn" className="TopButton" onClick={this.props.gameFunc}>
|
||||
<div id="gameBtn" className="TopButton" onClick={this.props.gameFunc}>
|
||||
<img src={gameBtn} alt="game" />
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import React from 'react'
|
||||
import TextInput from './TextInput'
|
||||
import Checkbox from './Checkbox'
|
||||
|
||||
/*
|
||||
* Valid types for the theme option value.
|
||||
* - input: A text input.
|
||||
* - dropdown: A select/dropdown input.
|
||||
* - checkbox: A toggle.
|
||||
* - button: A button.
|
||||
*/
|
||||
|
||||
interface IProps {
|
||||
type: string;
|
||||
className?: string;
|
||||
jsCallback?: string;
|
||||
data: InputSettings;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
toggled: boolean
|
||||
}
|
||||
|
||||
export interface InputSettings {
|
||||
/* Input. */
|
||||
placeholder?: string;
|
||||
initialValue?: string;
|
||||
|
||||
/* Dropdown. */
|
||||
options?: string[];
|
||||
|
||||
/* Checkbox. */
|
||||
toggled?: boolean
|
||||
id?: string;
|
||||
|
||||
/* Button. */
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export default class ThemeOptionValue extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
toggled: false
|
||||
}
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props: IProps, state: IState) {
|
||||
return { toggled: props.data.toggled || state.toggled }
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const data = this.props.data
|
||||
|
||||
if(this.props.type == 'checkbox')
|
||||
this.setState({ toggled: data.toggled || false })
|
||||
}
|
||||
|
||||
async onChange() {
|
||||
// Change toggled state if needed.
|
||||
if(this.props.type == 'checkbox')
|
||||
this.setState({
|
||||
toggled: !this.state.toggled
|
||||
})
|
||||
|
||||
if(!this.props.jsCallback)
|
||||
return
|
||||
}
|
||||
|
||||
render() {
|
||||
const data = this.props.data
|
||||
|
||||
switch(this.props.type) {
|
||||
case 'input':
|
||||
return (
|
||||
<div className={this.props.className}>
|
||||
<TextInput placeholder={data.placeholder} initalValue={data.initialValue} />
|
||||
</div>
|
||||
)
|
||||
case 'dropdown':
|
||||
return (
|
||||
<div className={this.props.className}>
|
||||
<select>
|
||||
{data.options ? data.options.map((option, index) => {
|
||||
return <option key={index}>{option}</option>
|
||||
}) : null}
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
case 'button':
|
||||
return (
|
||||
<div className={this.props.className}>
|
||||
<button>{data.text}</button>
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<div className={this.props.className}>
|
||||
<Checkbox checked={this.state?.toggled} onChange={this.onChange} id={this.props.className || 'a'} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,7 @@ import { getConfigOption, setConfigOption } from '../../../utils/configuration'
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import HelpButton from '../common/HelpButton'
|
||||
|
||||
const STABLE_REPO_DOWNLOAD = 'https://github.com/Grasscutters/Grasscutter/archive/refs/heads/stable.zip'
|
||||
const DEV_REPO_DOWNLOAD = 'https://github.com/Grasscutters/Grasscutter/archive/refs/heads/development.zip'
|
||||
const STABLE_DOWNLOAD = 'https://nightly.link/Grasscutters/Grasscutter/workflows/build/stable/Grasscutter.zip'
|
||||
const DEV_DOWNLOAD = 'https://nightly.link/Grasscutters/Grasscutter/workflows/build/development/Grasscutter.zip'
|
||||
const RESOURCES_DOWNLOAD = 'https://github.com/Koko-boya/Grasscutter_Resources/archive/refs/heads/main.zip'
|
||||
import { getVersionCache, VersionData } from '../../../utils/resources'
|
||||
|
||||
interface IProps {
|
||||
closeFn: () => void;
|
||||
@@ -30,6 +25,7 @@ interface IState {
|
||||
repo_downloading: boolean
|
||||
grasscutter_set: boolean
|
||||
resources_exist: boolean
|
||||
version_data: VersionData | null
|
||||
}
|
||||
|
||||
export default class Downloads extends React.Component<IProps, IState> {
|
||||
@@ -41,7 +37,8 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
resources_downloading: this.props.downloadManager.downloadingResources(),
|
||||
repo_downloading: this.props.downloadManager.downloadingRepo(),
|
||||
grasscutter_set: false,
|
||||
resources_exist: false
|
||||
resources_exist: false,
|
||||
version_data: null
|
||||
}
|
||||
|
||||
this.getGrasscutterFolder = this.getGrasscutterFolder.bind(this)
|
||||
@@ -55,6 +52,11 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
|
||||
async componentDidMount() {
|
||||
const gc_path = await getConfigOption('grasscutter_path')
|
||||
const versionData = await getVersionCache()
|
||||
|
||||
this.setState({
|
||||
version_data: versionData,
|
||||
})
|
||||
|
||||
listen('jar_extracted', () => {
|
||||
this.setState({ grasscutter_set: true }, this.forceUpdate)
|
||||
@@ -109,7 +111,7 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
|
||||
async downloadGrasscutterStableRepo() {
|
||||
const folder = await this.getGrasscutterFolder()
|
||||
this.props.downloadManager.addDownload(STABLE_REPO_DOWNLOAD, folder + '\\grasscutter_repo.zip', () =>{
|
||||
this.props.downloadManager.addDownload(this.state.version_data?.stable, folder + '\\grasscutter_repo.zip', () =>{
|
||||
unzip(folder + '\\grasscutter_repo.zip', folder + '\\', this.toggleButtons)
|
||||
})
|
||||
|
||||
@@ -118,7 +120,7 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
|
||||
async downloadGrasscutterDevRepo() {
|
||||
const folder = await this.getGrasscutterFolder()
|
||||
this.props.downloadManager.addDownload(DEV_REPO_DOWNLOAD, folder + '\\grasscutter_repo.zip', () =>{
|
||||
this.props.downloadManager.addDownload(this.state.version_data?.dev, folder + '\\grasscutter_repo.zip', () =>{
|
||||
unzip(folder + '\\grasscutter_repo.zip', folder + '\\', this.toggleButtons)
|
||||
})
|
||||
|
||||
@@ -127,7 +129,7 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
|
||||
async downloadGrasscutterStable() {
|
||||
const folder = await this.getGrasscutterFolder()
|
||||
this.props.downloadManager.addDownload(STABLE_DOWNLOAD, folder + '\\grasscutter.zip', () =>{
|
||||
this.props.downloadManager.addDownload(this.state.version_data?.stableJar, folder + '\\grasscutter.zip', () =>{
|
||||
unzip(folder + '\\grasscutter.zip', folder + '\\', this.toggleButtons)
|
||||
})
|
||||
|
||||
@@ -139,7 +141,7 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
|
||||
async downloadGrasscutterLatest() {
|
||||
const folder = await this.getGrasscutterFolder()
|
||||
this.props.downloadManager.addDownload(DEV_DOWNLOAD, folder + '\\grasscutter.zip', () =>{
|
||||
this.props.downloadManager.addDownload(this.state.version_data?.devJar, folder + '\\grasscutter.zip', () =>{
|
||||
unzip(folder + '\\grasscutter.zip', folder + '\\', this.toggleButtons)
|
||||
})
|
||||
|
||||
@@ -151,7 +153,7 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
|
||||
async downloadResources() {
|
||||
const folder = await this.getGrasscutterFolder()
|
||||
this.props.downloadManager.addDownload(RESOURCES_DOWNLOAD, folder + '\\resources.zip', async () => {
|
||||
this.props.downloadManager.addDownload(this.state.version_data?.resources, folder + '\\resources.zip', async () => {
|
||||
// Delete the existing folder if it exists
|
||||
if (await invoke('dir_exists', {
|
||||
path: folder + '\\resources'
|
||||
@@ -200,7 +202,7 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
</HelpButton>
|
||||
</div>
|
||||
<div className='DownloadValue' id="downloadMenuButtonGCStable">
|
||||
<BigButton disabled={this.state.grasscutter_downloading} onClick={this.downloadGrasscutterStable} id="grasscutterStableBtn" >
|
||||
<BigButton disabled={this.state.grasscutter_downloading || !this.state.version_data?.stableJar} onClick={this.downloadGrasscutterStable} id="grasscutterStableBtn" >
|
||||
<Tr text="components.download" />
|
||||
</BigButton>
|
||||
</div>
|
||||
@@ -215,7 +217,7 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
</HelpButton>
|
||||
</div>
|
||||
<div className='DownloadValue' id="downloadMenuButtonGCDev">
|
||||
<BigButton disabled={this.state.grasscutter_downloading} onClick={this.downloadGrasscutterLatest} id="grasscutterLatestBtn" >
|
||||
<BigButton disabled={this.state.grasscutter_downloading || !this.state.version_data?.devJar} onClick={this.downloadGrasscutterLatest} id="grasscutterLatestBtn" >
|
||||
<Tr text="components.download" />
|
||||
</BigButton>
|
||||
</div>
|
||||
@@ -233,7 +235,7 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
</HelpButton>
|
||||
</div>
|
||||
<div className='DownloadValue' id="downloadMenuButtonGCStableData">
|
||||
<BigButton disabled={this.state.repo_downloading} onClick={this.downloadGrasscutterStableRepo} id="grasscutterStableRepo" >
|
||||
<BigButton disabled={this.state.repo_downloading || !this.state.version_data?.stable} onClick={this.downloadGrasscutterStableRepo} id="grasscutterStableRepo" >
|
||||
<Tr text="components.download" />
|
||||
</BigButton>
|
||||
</div>
|
||||
@@ -248,7 +250,7 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
</HelpButton>
|
||||
</div>
|
||||
<div className='DownloadValue' id="downloadMenuButtonGCDevData">
|
||||
<BigButton disabled={this.state.repo_downloading} onClick={this.downloadGrasscutterStableRepo} id="grasscutterDevRepo" >
|
||||
<BigButton disabled={this.state.repo_downloading || !this.state.version_data?.dev} onClick={this.downloadGrasscutterStableRepo} id="grasscutterDevRepo" >
|
||||
<Tr text="components.download" />
|
||||
</BigButton>
|
||||
</div>
|
||||
@@ -264,7 +266,11 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
</HelpButton>
|
||||
</div>
|
||||
<div className='DownloadValue' id="downloadMenuButtonResources">
|
||||
<BigButton disabled={this.state.resources_downloading || !this.state.grasscutter_set || this.state.resources_exist} onClick={this.downloadResources} id="resourcesBtn" >
|
||||
<BigButton disabled={
|
||||
this.state.resources_downloading
|
||||
|| !this.state.grasscutter_set
|
||||
|| this.state.resources_exist
|
||||
|| !this.state.version_data?.resources} onClick={this.downloadResources} id="resourcesBtn" >
|
||||
<Tr text="components.download" />
|
||||
</BigButton>
|
||||
</div>
|
||||
|
||||
@@ -8,8 +8,7 @@ import DirInput from '../common/DirInput'
|
||||
import BigButton from '../common/BigButton'
|
||||
import HelpButton from '../common/HelpButton'
|
||||
import { unzip } from '../../../utils/zipUtils'
|
||||
|
||||
const GAME_DOWNLOAD = ''
|
||||
import { getVersionCache } from '../../../utils/resources'
|
||||
|
||||
interface IProps {
|
||||
closeFn: () => void;
|
||||
@@ -20,6 +19,7 @@ interface IState {
|
||||
gameDownloading: boolean;
|
||||
gameDownloadFolder: string;
|
||||
dirPlaceholder: string;
|
||||
clientDownloadLink: string | null | undefined;
|
||||
}
|
||||
|
||||
export default class Downloads extends React.Component<IProps, IState> {
|
||||
@@ -29,23 +29,25 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
this.state = {
|
||||
gameDownloading: false,
|
||||
gameDownloadFolder: '',
|
||||
dirPlaceholder: ''
|
||||
dirPlaceholder: '',
|
||||
clientDownloadLink: ''
|
||||
}
|
||||
|
||||
this.downloadGame = this.downloadGame.bind(this)
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
this.setState({
|
||||
dirPlaceholder: await translate('components.select_folder')
|
||||
})
|
||||
const versionCache = await getVersionCache()
|
||||
|
||||
console.log(this.state)
|
||||
this.setState({
|
||||
dirPlaceholder: await translate('components.select_folder'),
|
||||
clientDownloadLink: versionCache?.client_download_link
|
||||
})
|
||||
}
|
||||
|
||||
async downloadGame() {
|
||||
const folder = this.state.gameDownloadFolder
|
||||
this.props.downloadManager.addDownload(GAME_DOWNLOAD, folder + '\\game.zip', () =>{
|
||||
this.props.downloadManager.addDownload(this.state.clientDownloadLink, folder + '\\game.zip', () =>{
|
||||
unzip(folder + '\\game.zip', folder + '\\', () => {
|
||||
this.setState({
|
||||
gameDownloading: false
|
||||
@@ -63,7 +65,7 @@ export default class Downloads extends React.Component<IProps, IState> {
|
||||
<Menu heading='Download Game' closeFn={this.props.closeFn} className="GameDownloadMenu">
|
||||
<div className="GameDownload">
|
||||
{
|
||||
this.state.gameDownloadFolder !== '' && !this.state.gameDownloading ?
|
||||
this.state.gameDownloadFolder !== '' && !this.state.gameDownloading && this.state.clientDownloadLink ?
|
||||
<BigButton id="downloadGameBtn" onClick={this.downloadGame}>Download Game</BigButton>
|
||||
: <BigButton id="disabledGameBtn" onClick={() => null} disabled>Download Game</BigButton>
|
||||
}
|
||||
|
||||
@@ -14,6 +14,12 @@
|
||||
border-radius: 10px;
|
||||
|
||||
box-shadow: 0px 0px 5px 3px rgba(0, 0, 0, 0.2);
|
||||
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.Menu::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.MenuInner {
|
||||
|
||||
@@ -7,12 +7,12 @@ import Tr, { getLanguages, translate } from '../../../utils/language'
|
||||
import { setConfigOption, getConfig, getConfigOption } from '../../../utils/configuration'
|
||||
import Checkbox from '../common/Checkbox'
|
||||
import Divider from './Divider'
|
||||
import { getTheme, getThemeList, ThemeList } from '../../../utils/themes'
|
||||
import { getThemeList } from '../../../utils/themes'
|
||||
import * as server from '../../../utils/server'
|
||||
|
||||
import './Options.css'
|
||||
import BigButton from '../common/BigButton'
|
||||
import ThemeOptionValue from '../common/ThemeOptionValue'
|
||||
import { cacheLauncherResources, getVersionCache, getVersions } from '../../../utils/resources'
|
||||
|
||||
interface IProps {
|
||||
closeFn: () => void;
|
||||
@@ -21,6 +21,8 @@ interface IProps {
|
||||
interface IState {
|
||||
game_install_path: string
|
||||
grasscutter_path: string
|
||||
client_version: string
|
||||
meta_download: string | null | undefined
|
||||
java_path: string
|
||||
grasscutter_with_game: boolean
|
||||
language_options: { [key: string]: string }[],
|
||||
@@ -29,8 +31,10 @@ interface IState {
|
||||
themes: string[]
|
||||
theme: string
|
||||
encryption: boolean
|
||||
|
||||
theme_object: ThemeList|null;
|
||||
swag: boolean
|
||||
|
||||
// Swag stuff
|
||||
akebi_path: string
|
||||
}
|
||||
|
||||
export default class Options extends React.Component<IProps, IState> {
|
||||
@@ -40,6 +44,8 @@ export default class Options extends React.Component<IProps, IState> {
|
||||
this.state = {
|
||||
game_install_path: '',
|
||||
grasscutter_path: '',
|
||||
client_version: '',
|
||||
meta_download: '',
|
||||
java_path: '',
|
||||
grasscutter_with_game: false,
|
||||
language_options: [],
|
||||
@@ -48,13 +54,17 @@ export default class Options extends React.Component<IProps, IState> {
|
||||
themes: ['default'],
|
||||
theme: '',
|
||||
encryption: false,
|
||||
|
||||
theme_object: null
|
||||
swag: false,
|
||||
|
||||
// Swag stuff
|
||||
akebi_path: '',
|
||||
}
|
||||
|
||||
this.setGameExec = this.setGameExec.bind(this)
|
||||
this.setGrasscutterJar = this.setGrasscutterJar.bind(this)
|
||||
this.setJavaPath = this.setJavaPath.bind(this)
|
||||
this.setClientVersion = this.setClientVersion.bind(this)
|
||||
this.setAkebi = this.setAkebi.bind(this)
|
||||
this.toggleGrasscutterWithGame = this.toggleGrasscutterWithGame.bind(this)
|
||||
this.setCustomBackground = this.setCustomBackground.bind(this)
|
||||
this.toggleEncryption = this.toggleEncryption.bind(this)
|
||||
@@ -73,6 +83,8 @@ export default class Options extends React.Component<IProps, IState> {
|
||||
game_install_path: config.game_install_path || '',
|
||||
grasscutter_path: config.grasscutter_path || '',
|
||||
java_path: config.java_path || '',
|
||||
client_version: config.client_version || '',
|
||||
meta_download: (await getVersionCache())?.metadata_backup_link || '',
|
||||
grasscutter_with_game: config.grasscutter_with_game || false,
|
||||
language_options: languages,
|
||||
current_language: config.language || 'en',
|
||||
@@ -80,8 +92,10 @@ export default class Options extends React.Component<IProps, IState> {
|
||||
themes: (await getThemeList()).map(t => t.name),
|
||||
theme: config.theme || 'default',
|
||||
encryption: await translate(encEnabled ? 'options.enabled' : 'options.disabled'),
|
||||
|
||||
theme_object: (await getTheme(config.theme))
|
||||
swag: config.swag_mode || false,
|
||||
|
||||
// Swag stuff
|
||||
akebi_path: config.akebi_path || '',
|
||||
})
|
||||
|
||||
this.forceUpdate()
|
||||
@@ -95,6 +109,17 @@ export default class Options extends React.Component<IProps, IState> {
|
||||
})
|
||||
}
|
||||
|
||||
async setClientVersion(value: string) {
|
||||
await setConfigOption('client_version', value)
|
||||
|
||||
const newCache = await cacheLauncherResources()
|
||||
|
||||
this.setState({
|
||||
client_version: value,
|
||||
meta_download: newCache?.metadata_backup_link
|
||||
})
|
||||
}
|
||||
|
||||
setGrasscutterJar(value: string) {
|
||||
setConfigOption('grasscutter_path', value)
|
||||
|
||||
@@ -111,6 +136,14 @@ export default class Options extends React.Component<IProps, IState> {
|
||||
})
|
||||
}
|
||||
|
||||
setAkebi(value: string) {
|
||||
setConfigOption('akebi_path', value)
|
||||
|
||||
this.setState({
|
||||
akebi_path: value
|
||||
})
|
||||
}
|
||||
|
||||
async setLanguage(value: string) {
|
||||
await setConfigOption('language', value)
|
||||
window.location.reload()
|
||||
@@ -131,7 +164,7 @@ export default class Options extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
async setCustomBackground(value: string) {
|
||||
const isUrl = /^http(s)?:\/\//gm.test(value)
|
||||
const isUrl = /^(?:http(s)?:\/\/)/gm.test(value)
|
||||
|
||||
if (!value) return await setConfigOption('customBackground', '')
|
||||
|
||||
@@ -175,8 +208,6 @@ export default class Options extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const themeSettings = this.state.theme_object?.settings
|
||||
|
||||
return (
|
||||
<Menu closeFn={this.props.closeFn} className="Options" heading="Options">
|
||||
<div className='OptionSection' id="menuOptionsContainerGameExec">
|
||||
@@ -187,7 +218,51 @@ export default class Options extends React.Component<IProps, IState> {
|
||||
<DirInput onChange={this.setGameExec} value={this.state?.game_install_path} extensions={['exe']} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='OptionSection' id="menuOptionsContainerClientVersion">
|
||||
<div className='OptionLabel' id="menuOptionsLabelClientVersion">
|
||||
<Tr text="options.game_version" />
|
||||
</div>
|
||||
<div className='OptionValue' id="menuOptionsDirClientVersion">
|
||||
<select value={this.state.client_version} id="menuOptionsSelectMenuThemes" onChange={(event) => {
|
||||
this.setClientVersion(event.target.value)
|
||||
}}>
|
||||
{getVersions().map(t => (
|
||||
<option
|
||||
key={t}
|
||||
value={t}>
|
||||
|
||||
{t}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className='OptionSection' id="menuOptionsContainerMetadataDownload">
|
||||
<div className='OptionLabel' id="menuOptionsLabelMetadataDownload">
|
||||
<Tr text="options.emergency_metadata" />
|
||||
</div>
|
||||
<div className='OptionValue' id="menuOptionsButtonMetadataDownload">
|
||||
<BigButton disabled={this.state.meta_download === ''} onClick={this.toggleEncryption} id="toggleEnc">
|
||||
<Tr text='components.download' />
|
||||
</BigButton>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
this.state.swag && (
|
||||
<>
|
||||
<Divider />
|
||||
<div className='OptionSection' id="menuOptionsContainerAkebi">
|
||||
<div className='OptionLabel' id="menuOptionsLabelAkebi">
|
||||
<Tr text="swag.akebi" />
|
||||
</div>
|
||||
<div className='OptionValue' id="menuOptionsDirAkebi">
|
||||
<DirInput onChange={this.setAkebi} value={this.state?.akebi_path} extensions={['exe']} />
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
</>
|
||||
)
|
||||
}
|
||||
<div className='OptionSection' id="menuOptionsContainerGCJar">
|
||||
<div className='OptionLabel' id="menuOptionsLabelGCJar">
|
||||
<Tr text="options.grasscutter_jar" />
|
||||
@@ -196,13 +271,12 @@ export default class Options extends React.Component<IProps, IState> {
|
||||
<DirInput onChange={this.setGrasscutterJar} value={this.state?.grasscutter_path} extensions={['jar']} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='OptionSection' id="menuOptionsContainerToggleEnc">
|
||||
<div className='OptionLabel' id="menuOptionsLabelToggleEnc">
|
||||
<Tr text="options.toggle_encryption" />
|
||||
</div>
|
||||
<div className='OptionValue' id="menuOptionsButtonToggleEnc">
|
||||
<BigButton onClick={this.toggleEncryption} id="toggleEnc">
|
||||
<BigButton disabled={this.state.grasscutter_path === ''} onClick={this.toggleEncryption} id="toggleEnc">
|
||||
{
|
||||
this.state.encryption
|
||||
}
|
||||
@@ -292,23 +366,6 @@ export default class Options extends React.Component<IProps, IState> {
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{
|
||||
themeSettings ? themeSettings.map((settings, index) => {
|
||||
return (
|
||||
<div className='OptionSection' key={index}>
|
||||
<div className='OptionLabel'>
|
||||
{settings.label}
|
||||
</div>
|
||||
<div className='OptionValue'>
|
||||
<ThemeOptionValue type={settings.type} className={settings.className} data={settings.data} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}) : null
|
||||
}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { fs } from '@tauri-apps/api'
|
||||
import { dataDir } from '@tauri-apps/api/path'
|
||||
import { cacheLauncherResources } from './resources'
|
||||
|
||||
let configFilePath: string
|
||||
let defaultConfig: Configuration
|
||||
@@ -17,6 +18,7 @@ let defaultConfig: Configuration
|
||||
last_port: '443',
|
||||
language: 'en',
|
||||
customBackground: '',
|
||||
client_version: '2.7.0',
|
||||
cert_generated: false,
|
||||
theme: 'default',
|
||||
https_enabled: false,
|
||||
@@ -39,10 +41,15 @@ export interface Configuration {
|
||||
last_port: string
|
||||
language: string
|
||||
customBackground: string
|
||||
client_version: string
|
||||
cert_generated: boolean
|
||||
theme: string
|
||||
https_enabled: boolean
|
||||
debug_enabled: boolean
|
||||
swag_mode?: boolean
|
||||
|
||||
// Swag stuff
|
||||
akebi_path?: string
|
||||
}
|
||||
|
||||
export async function setConfigOption(key: string, value: any): Promise<void> {
|
||||
@@ -84,7 +91,7 @@ export async function saveConfig(obj: Configuration) {
|
||||
async function readConfigFile() {
|
||||
const local = await dataDir()
|
||||
|
||||
if (!configFilePath) configFilePath = local + 'cultivation\\configuration.json'
|
||||
if (!configFilePath) configFilePath = local + 'cultivation/configuration.json'
|
||||
|
||||
// Ensure Cultivation dir exists
|
||||
const dirs = await fs.readDir(local)
|
||||
@@ -94,12 +101,12 @@ async function readConfigFile() {
|
||||
await fs.createDir(local + 'cultivation').catch(e => console.log(e))
|
||||
}
|
||||
|
||||
const innerDirs = await fs.readDir(local + '\\cultivation')
|
||||
const innerDirs = await fs.readDir(local + '/cultivation')
|
||||
|
||||
// Create grasscutter dir for potential installation
|
||||
if (!innerDirs.find((fileOrDir) => fileOrDir?.name === 'grasscutter')) {
|
||||
// Create dir
|
||||
await fs.createDir(local + 'cultivation\\grasscutter').catch(e => console.log(e))
|
||||
await fs.createDir(local + 'cultivation/grasscutter').catch(e => console.log(e))
|
||||
}
|
||||
|
||||
const dataFiles = await fs.readDir(local + 'cultivation')
|
||||
@@ -112,6 +119,12 @@ async function readConfigFile() {
|
||||
contents: JSON.stringify(defaultConfig)
|
||||
}
|
||||
|
||||
// Also just shoe-horning this in, cache resources on first launch
|
||||
const versionData = await cacheLauncherResources()
|
||||
|
||||
defaultConfig.client_version = versionData?.game || ''
|
||||
|
||||
// Write config
|
||||
await fs.writeFile(file)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { setConfigOption } from './configuration'
|
||||
|
||||
interface DOMMessage {
|
||||
type: string
|
||||
data: ConfigUpdate
|
||||
}
|
||||
|
||||
interface ConfigUpdate {
|
||||
setting: string
|
||||
value: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a message received from the DOM.
|
||||
* @param document The document.
|
||||
* @param msg The message received from the DOM.
|
||||
*/
|
||||
export function parseMessageFromDOM(document: Document, msg: any): void {
|
||||
msg = msg.detail
|
||||
|
||||
if(!msg || !msg.type || !msg.data)
|
||||
return
|
||||
|
||||
switch(msg.type) {
|
||||
case 'updateConfig':
|
||||
if(!msg.data.setting || !msg.data.value)
|
||||
return
|
||||
setConfigOption(msg.data.setting, msg.data.value)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -107,7 +107,9 @@ export default class DownloadHandler {
|
||||
return this.downloads.some(d => d.path.includes('grasscutter_repo.zip'))
|
||||
}
|
||||
|
||||
addDownload(url: string, path: string, onFinish?: () => void) {
|
||||
addDownload(url: string | null | undefined, path: string, onFinish?: () => void) {
|
||||
if (!url) return
|
||||
|
||||
// Begin download from rust backend, don't add if the download addition fails
|
||||
invoke('download_file', { url, path })
|
||||
const obj = {
|
||||
|
||||
106
src/utils/resources.ts
Normal file
106
src/utils/resources.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { fs, invoke } from '@tauri-apps/api'
|
||||
import { dataDir } from '@tauri-apps/api/path'
|
||||
import { getConfig } from './configuration'
|
||||
|
||||
export interface VersionData {
|
||||
game: string
|
||||
metadata: string | null
|
||||
metadata_backup_link: string | null
|
||||
client_download_link: string | null
|
||||
resources: string
|
||||
stableJar: string | null
|
||||
devJar: string | null
|
||||
stable: string | null
|
||||
dev: string | null
|
||||
}
|
||||
|
||||
const globals: {
|
||||
[key: string]: VersionData
|
||||
} = {
|
||||
'2.8.0': {
|
||||
game: '2.8.0',
|
||||
metadata: '2.8.0',
|
||||
metadata_backup_link: null,
|
||||
client_download_link: null,
|
||||
resources: 'https://gitlab.com/yukiz/GrasscutterResources/-/archive/2.8/GrasscutterResources-2.8.zip',
|
||||
stableJar: null,
|
||||
devJar: 'https://nightly.link/Grasscutters/Grasscutter/actions/runs/2661955213/Grasscutter.zip',
|
||||
stable: null,
|
||||
dev: 'https://github.com/Grasscutters/Grasscutter/archive/refs/heads/2.8.zip'
|
||||
},
|
||||
'2.7.0': {
|
||||
game: '2.7.0',
|
||||
metadata: null,
|
||||
metadata_backup_link: null,
|
||||
client_download_link: null,
|
||||
resources: 'https://github.com/Koko-boya/Grasscutter_Resources/archive/refs/heads/main.zip',
|
||||
stableJar: 'https://nightly.link/Grasscutters/Grasscutter/workflows/build/stable/Grasscutter.zip',
|
||||
devJar: 'https://nightly.link/Grasscutters/Grasscutter/workflows/build/development/Grasscutter.zip',
|
||||
stable: 'https://github.com/Grasscutters/Grasscutter/archive/refs/heads/stable.zip',
|
||||
dev: 'https://github.com/Grasscutters/Grasscutter/archive/refs/heads/development.zip'
|
||||
},
|
||||
'2.6.0': {
|
||||
game: '2.6.0',
|
||||
metadata: null,
|
||||
metadata_backup_link: null,
|
||||
client_download_link: null,
|
||||
resources: 'https://github.com/Koko-boya/Grasscutter_Resources/archive/0e99a59218a346c2d56c54953f99077882de4a6d.zip',
|
||||
stableJar: 'https://github.com/Grasscutters/Grasscutter/releases/download/v1.1.0/grasscutter-1.1.0.jar',
|
||||
devJar: null,
|
||||
stable: 'https://github.com/Grasscutters/Grasscutter/archive/refs/heads/2.6.zip',
|
||||
dev: null
|
||||
}
|
||||
}
|
||||
|
||||
export async function cacheLauncherResources() {
|
||||
const config = await getConfig()
|
||||
const versionAPIUrl = 'https://sdk-os-static.mihoyo.com/hk4e_global/mdk/launcher/api/resource?channel_id=1&key=gcStgarh&launcher_id=10&sub_channel_id=0'
|
||||
|
||||
// Get versions from API
|
||||
const versions = JSON.parse(await invoke('web_get', {
|
||||
url: versionAPIUrl
|
||||
}))
|
||||
|
||||
if (!versions || versions.retcode !== 0) {
|
||||
console.log('Failed to get versions from API')
|
||||
return null
|
||||
}
|
||||
|
||||
const selectedVersion = config.client_version || versions.data.game.latest.version
|
||||
const selectedVersionData = globals[selectedVersion]
|
||||
|
||||
if (!selectedVersionData) {
|
||||
console.log('Failed to get version for selected version')
|
||||
return null
|
||||
}
|
||||
|
||||
const latest = versions.data.game.latest
|
||||
const latestData = globals[latest.version]
|
||||
|
||||
if (latestData) {
|
||||
latestData.metadata_backup_link = latest.decompressed_path + '/GenshinImpact_Data/Managed/Metadata/global-metadata.dat'
|
||||
latestData.client_download_link = latest.path
|
||||
}
|
||||
|
||||
// Write
|
||||
fs.writeFile({
|
||||
path: await dataDir() + 'cultivation/resources.json',
|
||||
contents: JSON.stringify(selectedVersionData)
|
||||
})
|
||||
|
||||
// In case we want to get it right away too
|
||||
return selectedVersionData
|
||||
}
|
||||
|
||||
export async function getVersionCache() {
|
||||
const raw = await fs.readTextFile(await dataDir() + 'cultivation/resources.json').catch(e => {
|
||||
console.log(e)
|
||||
return null
|
||||
})
|
||||
|
||||
return raw ? JSON.parse(raw) as VersionData : null
|
||||
}
|
||||
|
||||
export function getVersions() {
|
||||
return Object.keys(globals)
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export async function toggleEncryption(path: string) {
|
||||
// Write file
|
||||
await fs.writeFile({
|
||||
path,
|
||||
contents: JSON.stringify(serverConf)
|
||||
contents: JSON.stringify(serverConf, null, 2),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import {invoke} from '@tauri-apps/api'
|
||||
import {dataDir} from '@tauri-apps/api/path'
|
||||
import {convertFileSrc} from '@tauri-apps/api/tauri'
|
||||
import {getConfig, setConfigOption} from './configuration'
|
||||
|
||||
import {InputSettings} from '../ui/components/common/ThemeOptionValue'
|
||||
import { invoke } from '@tauri-apps/api'
|
||||
import { dataDir } from '@tauri-apps/api/path'
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri'
|
||||
import { getConfig, setConfigOption } from './configuration'
|
||||
|
||||
interface Theme {
|
||||
name: string
|
||||
@@ -15,16 +13,6 @@ interface Theme {
|
||||
css: string[]
|
||||
js: string[]
|
||||
}
|
||||
|
||||
// Custom settings.
|
||||
settings?: {
|
||||
label: string // The setting's label.
|
||||
type: string // The setting's type.
|
||||
data: InputSettings // The data for the setting.
|
||||
|
||||
className?: string // The name of the class this setting should take.
|
||||
jsCallback?: string // The name of the callback method that should be invoked.
|
||||
}[]
|
||||
|
||||
customBackgroundURL?: string
|
||||
customBackgroundPath?: string
|
||||
@@ -35,7 +23,7 @@ interface BackendThemeList {
|
||||
path: string
|
||||
}
|
||||
|
||||
export interface ThemeList extends Theme {
|
||||
interface ThemeList extends Theme {
|
||||
path: string
|
||||
}
|
||||
|
||||
@@ -49,7 +37,6 @@ const defaultTheme = {
|
||||
},
|
||||
path: 'default'
|
||||
}
|
||||
|
||||
export async function getThemeList() {
|
||||
// Do some invoke to backend to get the theme list
|
||||
const themes = await invoke('get_theme_list', {
|
||||
@@ -90,11 +77,6 @@ export async function getTheme(name: string) {
|
||||
return themes.find(t => t.name === name) || defaultTheme
|
||||
}
|
||||
|
||||
export async function getSelectedTheme() {
|
||||
const config = await getConfig()
|
||||
return await getTheme(config.theme)
|
||||
}
|
||||
|
||||
export async function loadTheme(theme: ThemeList, document: Document) {
|
||||
// Get config, since we will set the custom background in there
|
||||
const config = await getConfig()
|
||||
|
||||
Reference in New Issue
Block a user