Compare commits

..

1 Commits

Author SHA1 Message Date
KingRainbow44
c1a41bec65 Push custom-options 2022-07-06 01:38:34 -04:00
25 changed files with 1114 additions and 754 deletions

View File

@@ -33,6 +33,7 @@
"semi": [ "semi": [
"error", "error",
"never" "never"
] ],
"no-explicit-any": "off"
} }
} }

View File

@@ -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. During this open-beta testing period, **helpful issues are appreciated**, while unhelpful ones will be closed.
## Fair Warning ## Fair Warning
Cultivation is **VERY MUCH IN BETA**. 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** unless you want to deal with the alpha state of the current builds. 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 found elsewhere**. Only use releases from this GitHub repository. Please do **NOT install, download, or use pre-compiled versions of Cultivation**. Only use releases from this GitHub repository.
# Cultivation # Cultivation
A game launcher designed to easily proxy traffic from anime game to private servers. A game launcher designed to easily proxy traffic from anime game to private servers.

View File

@@ -1,6 +1,6 @@
{ {
"name": "cultivation", "name": "cultivation",
"version": "1.0.2", "version": "1.0.1",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@tauri-apps/api": "^1.0.0-rc.5", "@tauri-apps/api": "^1.0.0-rc.5",

View File

@@ -12,7 +12,9 @@
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Cultivation</title> <title>Cultivation</title>
<script src="%PUBLIC_URL%/theme-engine.js"></script>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>

14
public/theme-engine.js Normal file
View File

@@ -0,0 +1,14 @@
/**
* 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})
}

1265
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
[package] [package]
name = "cultivation" name = "cultivation"
version = "0.1.0" version = "1.0.1"
description = "A custom launcher for anime game." description = "A custom launcher for anime game."
authors = ["KingRainbow44", "SpikeHD"] authors = ["KingRainbow44", "SpikeHD"]
license = "" license = "Apache-2.0"
repository = "https://github.com/Grasscutters/Cultivation.git" repository = "https://github.com/Grasscutters/Cultivation.git"
default-run = "cultivation" default-run = "cultivation"
edition = "2021" edition = "2021"
@@ -14,26 +14,22 @@ rust-version = "1.57"
[build-dependencies] [build-dependencies]
tauri-build = { version = "1.0.0-rc.8", features = [] } 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] [dependencies]
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.0.0-rc.9", features = ["api-all"] } tauri = { version = "1.0.0-rc.9", features = ["api-all", "updater"] }
# Access system process info. # Access system process info.
sysinfo = "0.24.6" sysinfo = "0.23.12"
# ZIP-archive library. # ZIP-archive library.
zip-extract = "0.1.1" zip-extract = "0.1.1"
zip = "0.6.2" zip = "0.6.2"
# For creating a "global" downloads list. # For creating a "global" downloads list.
once_cell = "1.13.0" lazy_static = "1.4.0"
# Access to the Windows Registry.
registry = "1.2.1"
# Program opener. # Program opener.
open = "2.1.2" open = "2.1.2"
@@ -42,6 +38,10 @@ duct = "0.13.5"
# Serialization. # Serialization.
serde_json = "1" serde_json = "1"
# System process elevation.
is_elevated = "0.1.2"
runas = "0.2.1"
# Dependencies for the HTTP(S) proxy. # Dependencies for the HTTP(S) proxy.
http = "0.2" http = "0.2"
hudsucker = "0.17.2" hudsucker = "0.17.2"

View File

@@ -1,61 +0,0 @@
{
"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"
}
}

View File

@@ -1,4 +1,4 @@
use once_cell::sync::Lazy; use lazy_static::lazy_static;
use std::sync::Mutex; use std::sync::Mutex;
use std::cmp::min; use std::cmp::min;
@@ -8,7 +8,12 @@ use std::io::Write;
use futures_util::StreamExt; use futures_util::StreamExt;
// This will create a downloads list that will be used to check if we should continue downloading the file // This will create a downloads list that will be used to check if we should continue downloading the file
static DOWNLOADS: Lazy<Mutex<Vec<String>>> = Lazy::new(|| Mutex::new(Vec::new())); lazy_static! {
static ref DOWNLOADS: Mutex<Vec<String>> = {
let m = Vec::new();
Mutex::new(m)
};
}
// Lots of help from: https://gist.github.com/giuliano-oliveira/4d11d6b3bb003dba3a1b53f43d81b30d // Lots of help from: https://gist.github.com/giuliano-oliveira/4d11d6b3bb003dba3a1b53f43d81b30d
// and docs ofc // and docs ofc
@@ -54,17 +59,17 @@ pub async fn download_file(window: tauri::Window, url: &str, path: &str) -> Resu
let chunk = match item { let chunk = match item {
Ok(itm) => itm, Ok(itm) => itm,
Err(e) => { Err(e) => {
emit_download_err(window, "Error while downloading file".to_string(), path); emit_download_err(window, format!("Error while downloading file"), path);
return Err(format!("Error while downloading file: {}", e)); return Err(format!("Error while downloading file: {}", e));
} }
}; };
let vect = &chunk.to_vec()[..]; let vect = &chunk.to_vec()[..];
// Write bytes // Write bytes
match file.write_all(vect) { match file.write_all(&vect) {
Ok(x) => x, Ok(x) => x,
Err(e) => { Err(e) => {
emit_download_err(window, "Error while writing file".to_string(), path); emit_download_err(window, format!("Error while writing file"), path);
return Err(format!("Error while writing file: {}", e)); return Err(format!("Error while writing file: {}", e));
} }
} }
@@ -73,7 +78,7 @@ pub async fn download_file(window: tauri::Window, url: &str, path: &str) -> Resu
let new = min(downloaded + (chunk.len() as u64), total_size); let new = min(downloaded + (chunk.len() as u64), total_size);
downloaded = new; downloaded = new;
total_downloaded += chunk.len() as u64; total_downloaded = total_downloaded + chunk.len() as u64;
let mut res_hash = std::collections::HashMap::new(); let mut res_hash = std::collections::HashMap::new();
@@ -105,15 +110,15 @@ pub async fn download_file(window: tauri::Window, url: &str, path: &str) -> Resu
window.emit("download_finished", &path).unwrap(); window.emit("download_finished", &path).unwrap();
// We are done // We are done
Ok(()) return Ok(());
} }
pub fn emit_download_err(window: tauri::Window, msg: String, path: &str) { pub fn emit_download_err(window: tauri::Window, msg: std::string::String, path: &str) {
let mut res_hash = std::collections::HashMap::new(); let mut res_hash = std::collections::HashMap::new();
res_hash.insert( res_hash.insert(
"error".to_string(), "error".to_string(),
msg, msg.to_string(),
); );
res_hash.insert( res_hash.insert(

View File

@@ -5,51 +5,31 @@ pub fn rename(path: String, new_name: String) {
let mut new_path = path.clone(); let mut new_path = path.clone();
// Check if file/folder to replace exists // Check if file/folder to replace exists
if fs::metadata(&path).is_err() { if !fs::metadata(&path).is_ok() {
return; return;
} }
// Check if path uses forward or back slashes // Check if path uses forward or back slashes
if new_path.contains('\\') { if new_path.contains("\\") {
new_path = path.replace('\\', "/"); 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(); fs::rename(path, &path_replaced).unwrap();
} }
#[tauri::command] #[tauri::command]
pub fn dir_exists(path: &str) -> bool { pub fn dir_exists(path: &str) -> bool {
fs::metadata(&path).is_ok() return fs::metadata(&path).is_ok();
} }
#[tauri::command] #[tauri::command]
pub fn dir_is_empty(path: &str) -> bool { pub fn dir_is_empty(path: &str) -> bool {
fs::read_dir(&path).unwrap().count() == 0 return fs::read_dir(&path).unwrap().count() == 0;
} }
#[tauri::command] #[tauri::command]
pub fn dir_delete(path: &str) { pub fn dir_delete(path: &str) {
fs::remove_dir_all(path).unwrap(); 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
}
}
}

View File

@@ -7,13 +7,15 @@ pub async fn get_lang(window: tauri::Window, lang: String) -> String {
// Send contents of language file back // Send contents of language file back
let lang_path: PathBuf = [&install_location(), "lang", &format!("{}.json", lang)].iter().collect(); let lang_path: PathBuf = [&install_location(), "lang", &format!("{}.json", lang)].iter().collect();
match std::fs::read_to_string(&lang_path) { let contents = match std::fs::read_to_string(&lang_path) {
Ok(x) => x, Ok(x) => x,
Err(e) => { Err(e) => {
emit_lang_err(window, format!("Failed to read language file: {}", e)); emit_lang_err(window, format!("Failed to read language file: {}", e));
"".to_string() return "".to_string();
} }
} };
return contents;
} }
#[tauri::command] #[tauri::command]
@@ -21,9 +23,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 // 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 languages = std::collections::HashMap::new();
let lang_files = std::fs::read_dir(Path::new(&install_location()).join("lang")).unwrap(); let mut lang_files = std::fs::read_dir(Path::new(&install_location()).join("lang")).unwrap();
for entry in lang_files { while let Some(entry) = lang_files.next() {
let entry = entry.unwrap(); let entry = entry.unwrap();
let path = entry.path(); let path = entry.path();
let filename = path.file_name().unwrap().to_str().unwrap(); let filename = path.file_name().unwrap().to_str().unwrap();
@@ -39,15 +41,15 @@ pub async fn get_languages() -> std::collections::HashMap<String, String> {
languages.insert(filename.to_string(), content); languages.insert(filename.to_string(), content);
} }
languages return languages;
} }
pub fn emit_lang_err(window: tauri::Window, msg: String) { pub fn emit_lang_err(window: tauri::Window, msg: std::string::String) {
let mut res_hash = std::collections::HashMap::new(); let mut res_hash = std::collections::HashMap::new();
res_hash.insert( res_hash.insert(
"error".to_string(), "error".to_string(),
msg, msg.to_string(),
); );
window.emit("lang_error", &res_hash).unwrap(); window.emit("lang_error", &res_hash).unwrap();

View File

@@ -3,7 +3,7 @@ all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows" windows_subsystem = "windows"
)] )]
use once_cell::sync::Lazy; use lazy_static::lazy_static;
use std::{sync::Mutex, collections::HashMap}; use std::{sync::Mutex, collections::HashMap};
use std::path::PathBuf; use std::path::PathBuf;
@@ -20,12 +20,21 @@ mod lang;
mod proxy; mod proxy;
mod web; mod web;
static WATCH_GAME_PROCESS: Lazy<Mutex<String>> = Lazy::new(|| Mutex::new(String::new())); lazy_static! {
static ref WATCH_GAME_PROCESS: Mutex<String> = {
let m = "".to_string();
Mutex::new(m)
};
}
fn main() { fn main() {
// Start the game process watcher. // Start the game process watcher.
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() tauri::Builder::default()
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
enable_process_watcher, enable_process_watcher,
@@ -39,6 +48,7 @@ fn main() {
system_helpers::run_program, system_helpers::run_program,
system_helpers::run_jar, system_helpers::run_jar,
system_helpers::open_in_browser, system_helpers::open_in_browser,
system_helpers::copy_file,
system_helpers::install_location, system_helpers::install_location,
system_helpers::is_elevated, system_helpers::is_elevated,
proxy::set_proxy_addr, proxy::set_proxy_addr,
@@ -48,7 +58,6 @@ fn main() {
file_helpers::dir_exists, file_helpers::dir_exists,
file_helpers::dir_is_empty, file_helpers::dir_is_empty,
file_helpers::dir_delete, file_helpers::dir_delete,
file_helpers::copy_file,
downloader::download_file, downloader::download_file,
downloader::stop_download, downloader::stop_download,
lang::get_lang, lang::get_lang,
@@ -75,9 +84,14 @@ fn process_watcher() {
// Grab the game process name // Grab the game process name
let proc = WATCH_GAME_PROCESS.lock().unwrap().to_string(); let proc = WATCH_GAME_PROCESS.lock().unwrap().to_string();
if !proc.is_empty() { if !&proc.is_empty() {
let mut proc_with_name = system.processes_by_exact_name(&proc); let proc_with_name = system.processes_by_exact_name(&proc);
let exists = proc_with_name.next().is_some(); let mut exists = false;
for _p in proc_with_name {
exists = true;
break;
}
// If the game process closes, disable the proxy. // If the game process closes, disable the proxy.
if !exists { if !exists {
@@ -95,7 +109,7 @@ fn is_game_running() -> bool {
// Grab the game process name // Grab the game process name
let proc = WATCH_GAME_PROCESS.lock().unwrap().to_string(); let proc = WATCH_GAME_PROCESS.lock().unwrap().to_string();
!proc.is_empty() return !proc.is_empty();
} }
#[tauri::command] #[tauri::command]
@@ -126,8 +140,11 @@ fn disconnect() {
#[tauri::command] #[tauri::command]
async fn req_get(url: String) -> String { async fn req_get(url: String) -> String {
// Send a GET request to the specified URL and send the response body back to the client. // Send a GET request to the specified URL.
web::query(&url.to_string()).await let response = web::query(&url.to_string()).await;
// Send the response body back to the client.
return response;
} }
#[tauri::command] #[tauri::command]
@@ -163,7 +180,7 @@ async fn get_theme_list(data_dir: String) -> Vec<HashMap<String, String>> {
} }
} }
themes return themes;
} }
#[tauri::command] #[tauri::command]
@@ -187,7 +204,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. // Now we check if the bg folder, which is one directory above the game_path, exists.
let bg_img_path = format!("{}\\{}", &bg_path, &file_name); let bg_img_path = format!("{}\\{}", bg_path.clone().to_string(), file_name.as_str());
// If it doesn't, then we do not have backgrounds to grab. // If it doesn't, then we do not have backgrounds to grab.
if !file_helpers::dir_exists(&bg_path) { if !file_helpers::dir_exists(&bg_path) {
@@ -203,15 +220,15 @@ async fn get_bg_file(bg_path: String, appdata: String) -> String {
// The image exists, lets copy it to our local '\bg' folder. // The image exists, lets copy it to our local '\bg' folder.
let bg_img_path_local = format!("{}\\bg\\{}", copy_loc, file_name.as_str()); let bg_img_path_local = format!("{}\\bg\\{}", copy_loc, file_name.as_str());
match std::fs::copy(bg_img_path, bg_img_path_local) { return match std::fs::copy(bg_img_path, bg_img_path_local) {
Ok(_) => { Ok(_) => {
// Copy was successful, lets return true. // Copy was successful, lets return true.
format!("{}\\{}", copy_loc, response_data.bg_file) format!("{}\\{}", copy_loc, response_data.bg_file.as_str())
} }
Err(e) => { Err(e) => {
// Copy failed, lets return false // Copy failed, lets return false
println!("Failed to copy background image: {}", e); println!("Failed to copy background image: {}", e);
"".to_string() "".to_string()
} }
} };
} }

View File

@@ -3,7 +3,7 @@
* https://github.com/omjadas/hudsucker/blob/main/examples/log.rs * https://github.com/omjadas/hudsucker/blob/main/examples/log.rs
*/ */
use once_cell::sync::Lazy; use lazy_static::lazy_static;
use std::{sync::Mutex, str::FromStr}; use std::{sync::Mutex, str::FromStr};
use rcgen::*; use rcgen::*;
@@ -17,12 +17,11 @@ use hudsucker::{
use std::fs; use std::fs;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::Path; use std::path::Path;
use registry::{Hive, Data, Security};
use rustls_pemfile as pemfile; use rustls_pemfile as pemfile;
use tauri::http::Uri; use tauri::http::Uri;
use crate::system_helpers::run_command;
#[cfg(windows)]
use registry::{Hive, Data, Security};
async fn shutdown_signal() { async fn shutdown_signal() {
tokio::signal::ctrl_c().await tokio::signal::ctrl_c().await
@@ -30,7 +29,12 @@ async fn shutdown_signal() {
} }
// Global ver for getting server address. // Global ver for getting server address.
static SERVER: Lazy<Mutex<String>> = Lazy::new(|| Mutex::new("http://localhost:443".to_string())); lazy_static! {
static ref SERVER: Mutex<String> = {
let m = "http://localhost:443".to_string();
Mutex::new(m)
};
}
#[derive(Clone)] #[derive(Clone)]
struct ProxyHandler; struct ProxyHandler;
@@ -105,43 +109,37 @@ pub async fn create_proxy(proxy_port: u16, certificate_path: String) {
/** /**
* Connects to the local HTTP(S) proxy server. * Connects to the local HTTP(S) proxy server.
*/ */
#[cfg(windows)]
pub fn connect_to_proxy(proxy_port: u16) { pub fn connect_to_proxy(proxy_port: u16) {
// Create 'ProxyServer' string. if cfg!(target_os = "windows") {
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. // Fetch the 'Internet Settings' registry key.
let settings = Hive::CurrentUser.open(r"Software\Microsoft\Windows\CurrentVersion\Internet Settings", Security::Write).unwrap(); let settings = Hive::CurrentUser.open(r"Software\Microsoft\Windows\CurrentVersion\Internet Settings", Security::Write).unwrap();
// Set registry values. // Set registry values.
settings.set_value("ProxyServer", &Data::String(server_string.parse().unwrap())).unwrap(); settings.set_value("ProxyServer", &Data::String(server_string.parse().unwrap())).unwrap();
settings.set_value("ProxyEnable", &Data::U32(1)).unwrap(); settings.set_value("ProxyEnable", &Data::U32(1)).unwrap();
}
println!("Connected to the proxy."); 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. * Disconnects from the local HTTP(S) proxy server.
*/ */
#[cfg(windows)]
pub fn disconnect_from_proxy() { pub fn disconnect_from_proxy() {
// Fetch the 'Internet Settings' registry key. if cfg!(target_os = "windows") {
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. // Set registry values.
settings.set_value("ProxyEnable", &Data::U32(0)).unwrap(); settings.set_value("ProxyEnable", &Data::U32(0)).unwrap();
}
println!("Disconnected from proxy."); println!("Disconnected from proxy.");
} }
#[cfg(not(windows))]
pub fn disconnect_from_proxy() {}
/* /*
* Generates a private key and certificate used by the certificate authority. * Generates a private key and certificate used by the certificate authority.
* Additionally installs the certificate and private key in the Root CA store. * Additionally installs the certificate and private key in the Root CA store.
@@ -203,19 +201,12 @@ pub fn generate_ca_files(path: &Path) {
/* /*
* Attempts to install the certificate authority's certificate into the Root CA store. * Attempts to install the certificate authority's certificate into the Root CA store.
*/ */
#[cfg(windows)]
pub fn install_ca_files(cert_path: &Path) { pub fn install_ca_files(cert_path: &Path) {
crate::system_helpers::run_command("certutil", vec!["-user", "-addstore", "Root", cert_path.to_str().unwrap()]); 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()]);
}
println!("Installed certificate."); 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.");
}

View File

@@ -1,3 +1,7 @@
use std::thread;
use tauri;
use open;
use duct::cmd; use duct::cmd;
use crate::file_helpers; use crate::file_helpers;
@@ -5,7 +9,11 @@ use crate::file_helpers;
#[tauri::command] #[tauri::command]
pub fn run_program(path: String) { pub fn run_program(path: String) {
// Open the program from the specified path. // Open the program from the specified path.
open::that(&path).unwrap();
// Open in new thread to prevent blocking.
thread::spawn(move || {
open::that(&path).unwrap();
});
} }
#[tauri::command] #[tauri::command]
@@ -23,7 +31,7 @@ pub fn run_jar(path: String, execute_in: String, java_path: String) {
}; };
// Open the program from the specified path. // Open the program from the specified path.
match open::with(format!("/k cd /D \"{}\" & {}", &execute_in, &command), "C:\\Windows\\System32\\cmd.exe") { match open::with(format!("/k cd /D \"{}\" & {}", &execute_in, &command).to_string(), "C:\\Windows\\System32\\cmd.exe") {
Ok(_) => (), Ok(_) => (),
Err(e) => println!("Failed to open jar ({} from {}): {}", &path, &execute_in, e), Err(e) => println!("Failed to open jar ({} from {}): {}", &path, &execute_in, e),
}; };
@@ -38,6 +46,25 @@ 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] #[tauri::command]
pub fn install_location() -> String { pub fn install_location() -> String {
@@ -49,14 +76,7 @@ pub fn install_location() -> String {
return exe_path.to_str().unwrap().to_string(); return exe_path.to_str().unwrap().to_string();
} }
#[cfg(windows)]
#[tauri::command] #[tauri::command]
pub fn is_elevated() -> bool { pub fn is_elevated() -> bool {
is_elevated::is_elevated() return is_elevated::is_elevated();
}
#[cfg(unix)]
#[tauri::command]
pub fn is_elevated() -> bool {
sudo::check() == sudo::RunningAs::Root
} }

View File

@@ -1,3 +1,5 @@
use zip_extract;
use zip;
use std::fs::File; use std::fs::File;
use std::path; use std::path;
use std::thread; use std::thread;

View File

@@ -4,7 +4,7 @@ pub(crate) async fn query(site: &str) -> String {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let response = client.get(site).header(USER_AGENT, "cultivation").send().await.unwrap(); let response = client.get(site).header(USER_AGENT, "cultivation").send().await.unwrap();
response.text().await.unwrap() return response.text().await.unwrap();
} }
#[tauri::command] #[tauri::command]
@@ -14,5 +14,5 @@ pub(crate) async fn valid_url(url: String) -> bool {
let response = client.get(url).header(USER_AGENT, "cultivation").send().await.unwrap(); let response = client.get(url).header(USER_AGENT, "cultivation").send().await.unwrap();
response.status().as_str() == "200" return response.status().as_str() == "200";
} }

View File

@@ -7,7 +7,7 @@
}, },
"package": { "package": {
"productName": "Cultivation", "productName": "Cultivation",
"version": "1.0.2" "version": "1.0.1"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {
@@ -72,7 +72,7 @@
"csp": "default-src 'self' https://asset.localhost; img-src 'self'; img-src https://* asset: https://asset.localhost" "csp": "default-src 'self' https://asset.localhost; img-src 'self'; img-src https://* asset: https://asset.localhost"
}, },
"updater": { "updater": {
"active": false, "active": true,
"dialog": true, "dialog": true,
"endpoints": [ "endpoints": [
"https://api.grasscutter.io/cultivation/updater?version={{current_version}}", "https://api.grasscutter.io/cultivation/updater?version={{current_version}}",

View File

@@ -17,6 +17,7 @@ let isDebug = false;
isDebug = await getConfigOption('debug_enabled') isDebug = await getConfigOption('debug_enabled')
}) })
// Render the app.
root.render( root.render(
<React.StrictMode> <React.StrictMode>
{ {
@@ -25,5 +26,10 @@ root.render(
</React.StrictMode> </React.StrictMode>
) )
// Enable web vitals if needed.
import reportWebVitals from './utils/reportWebVitals' import reportWebVitals from './utils/reportWebVitals'
isDebug && reportWebVitals(console.log) isDebug && reportWebVitals(console.log)
// Setup DOM message passing.
import { parseMessageFromDOM } from './utils/dom'
document.addEventListener<string>('domMessage', parseMessageFromDOM)

View File

@@ -0,0 +1,34 @@
{
"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": ""
}

View File

@@ -0,0 +1,105 @@
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>
)
}
}
}

View File

@@ -7,11 +7,12 @@ import Tr, { getLanguages, translate } from '../../../utils/language'
import { setConfigOption, getConfig, getConfigOption } from '../../../utils/configuration' import { setConfigOption, getConfig, getConfigOption } from '../../../utils/configuration'
import Checkbox from '../common/Checkbox' import Checkbox from '../common/Checkbox'
import Divider from './Divider' import Divider from './Divider'
import { getThemeList } from '../../../utils/themes' import { getTheme, getThemeList, ThemeList } from '../../../utils/themes'
import * as server from '../../../utils/server' import * as server from '../../../utils/server'
import './Options.css' import './Options.css'
import BigButton from '../common/BigButton' import BigButton from '../common/BigButton'
import ThemeOptionValue from '../common/ThemeOptionValue'
interface IProps { interface IProps {
closeFn: () => void; closeFn: () => void;
@@ -28,6 +29,8 @@ interface IState {
themes: string[] themes: string[]
theme: string theme: string
encryption: boolean encryption: boolean
theme_object: ThemeList|null;
} }
export default class Options extends React.Component<IProps, IState> { export default class Options extends React.Component<IProps, IState> {
@@ -44,7 +47,9 @@ export default class Options extends React.Component<IProps, IState> {
bg_url_or_path: '', bg_url_or_path: '',
themes: ['default'], themes: ['default'],
theme: '', theme: '',
encryption: false encryption: false,
theme_object: null
} }
this.setGameExec = this.setGameExec.bind(this) this.setGameExec = this.setGameExec.bind(this)
@@ -74,7 +79,9 @@ export default class Options extends React.Component<IProps, IState> {
bg_url_or_path: config.customBackground || '', bg_url_or_path: config.customBackground || '',
themes: (await getThemeList()).map(t => t.name), themes: (await getThemeList()).map(t => t.name),
theme: config.theme || 'default', theme: config.theme || 'default',
encryption: await translate(encEnabled ? 'options.enabled' : 'options.disabled') encryption: await translate(encEnabled ? 'options.enabled' : 'options.disabled'),
theme_object: (await getTheme(config.theme))
}) })
this.forceUpdate() this.forceUpdate()
@@ -124,7 +131,7 @@ export default class Options extends React.Component<IProps, IState> {
} }
async setCustomBackground(value: string) { async setCustomBackground(value: string) {
const isUrl = /^(?:http(s)?:\/\/)/gm.test(value) const isUrl = /^http(s)?:\/\//gm.test(value)
if (!value) return await setConfigOption('customBackground', '') if (!value) return await setConfigOption('customBackground', '')
@@ -168,6 +175,8 @@ export default class Options extends React.Component<IProps, IState> {
} }
render() { render() {
const themeSettings = this.state.theme_object?.settings
return ( return (
<Menu closeFn={this.props.closeFn} className="Options" heading="Options"> <Menu closeFn={this.props.closeFn} className="Options" heading="Options">
<div className='OptionSection' id="menuOptionsContainerGameExec"> <div className='OptionSection' id="menuOptionsContainerGameExec">
@@ -178,6 +187,7 @@ export default class Options extends React.Component<IProps, IState> {
<DirInput onChange={this.setGameExec} value={this.state?.game_install_path} extensions={['exe']} /> <DirInput onChange={this.setGameExec} value={this.state?.game_install_path} extensions={['exe']} />
</div> </div>
</div> </div>
<div className='OptionSection' id="menuOptionsContainerGCJar"> <div className='OptionSection' id="menuOptionsContainerGCJar">
<div className='OptionLabel' id="menuOptionsLabelGCJar"> <div className='OptionLabel' id="menuOptionsLabelGCJar">
<Tr text="options.grasscutter_jar" /> <Tr text="options.grasscutter_jar" />
@@ -186,6 +196,7 @@ export default class Options extends React.Component<IProps, IState> {
<DirInput onChange={this.setGrasscutterJar} value={this.state?.grasscutter_path} extensions={['jar']} /> <DirInput onChange={this.setGrasscutterJar} value={this.state?.grasscutter_path} extensions={['jar']} />
</div> </div>
</div> </div>
<div className='OptionSection' id="menuOptionsContainerToggleEnc"> <div className='OptionSection' id="menuOptionsContainerToggleEnc">
<div className='OptionLabel' id="menuOptionsLabelToggleEnc"> <div className='OptionLabel' id="menuOptionsLabelToggleEnc">
<Tr text="options.toggle_encryption" /> <Tr text="options.toggle_encryption" />
@@ -281,6 +292,23 @@ export default class Options extends React.Component<IProps, IState> {
</select> </select>
</div> </div>
</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> </Menu>
) )
} }

View File

@@ -84,7 +84,7 @@ export async function saveConfig(obj: Configuration) {
async function readConfigFile() { async function readConfigFile() {
const local = await dataDir() const local = await dataDir()
if (!configFilePath) configFilePath = local + 'cultivation/configuration.json' if (!configFilePath) configFilePath = local + 'cultivation\\configuration.json'
// Ensure Cultivation dir exists // Ensure Cultivation dir exists
const dirs = await fs.readDir(local) const dirs = await fs.readDir(local)
@@ -94,12 +94,12 @@ async function readConfigFile() {
await fs.createDir(local + 'cultivation').catch(e => console.log(e)) 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 // Create grasscutter dir for potential installation
if (!innerDirs.find((fileOrDir) => fileOrDir?.name === 'grasscutter')) { if (!innerDirs.find((fileOrDir) => fileOrDir?.name === 'grasscutter')) {
// Create dir // 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') const dataFiles = await fs.readDir(local + 'cultivation')

31
src/utils/dom.ts Normal file
View File

@@ -0,0 +1,31 @@
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
}
}

View File

@@ -1,7 +1,9 @@
import { invoke } from '@tauri-apps/api' import {invoke} from '@tauri-apps/api'
import { dataDir } from '@tauri-apps/api/path' import {dataDir} from '@tauri-apps/api/path'
import { convertFileSrc } from '@tauri-apps/api/tauri' import {convertFileSrc} from '@tauri-apps/api/tauri'
import { getConfig, setConfigOption } from './configuration' import {getConfig, setConfigOption} from './configuration'
import {InputSettings} from '../ui/components/common/ThemeOptionValue'
interface Theme { interface Theme {
name: string name: string
@@ -14,6 +16,16 @@ interface Theme {
js: 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 customBackgroundURL?: string
customBackgroundPath?: string customBackgroundPath?: string
} }
@@ -23,7 +35,7 @@ interface BackendThemeList {
path: string path: string
} }
interface ThemeList extends Theme { export interface ThemeList extends Theme {
path: string path: string
} }
@@ -37,6 +49,7 @@ const defaultTheme = {
}, },
path: 'default' path: 'default'
} }
export async function getThemeList() { export async function getThemeList() {
// Do some invoke to backend to get the theme list // Do some invoke to backend to get the theme list
const themes = await invoke('get_theme_list', { const themes = await invoke('get_theme_list', {
@@ -77,6 +90,11 @@ export async function getTheme(name: string) {
return themes.find(t => t.name === name) || defaultTheme 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) { export async function loadTheme(theme: ThemeList, document: Document) {
// Get config, since we will set the custom background in there // Get config, since we will set the custom background in there
const config = await getConfig() const config = await getConfig()