diff --git a/src-tauri/src/lang.rs b/src-tauri/src/lang.rs index 23c518e..8a7f53d 100644 --- a/src-tauri/src/lang.rs +++ b/src-tauri/src/lang.rs @@ -1,9 +1,11 @@ +use crate::system_helpers::*; + #[tauri::command] pub async fn get_lang(window: tauri::Window, lang: String) -> String { let lang = lang.to_lowercase(); // Send contents of language file back - let contents = match std::fs::read_to_string(format!("./lang/{}.json", lang)) { + let contents = match std::fs::read_to_string(format!("{}/lang/{}.json", install_location(), lang)) { Ok(x) => x, Err(e) => { emit_lang_err(window, format!("Failed to read language file: {}", e)); @@ -19,7 +21,7 @@ pub async fn get_languages() -> std::collections::HashMap { // 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("./lang").unwrap(); + let mut lang_files = std::fs::read_dir(format!("{}/lang", install_location())).unwrap(); while let Some(entry) = lang_files.next() { let entry = entry.unwrap(); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index e2bce0b..3d2418d 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -29,6 +29,12 @@ lazy_static! { fn main() { process_watcher(); + // Make BG folder if it doesn't exist + let bg_folder = format!("{}/bg", system_helpers::install_location()); + if !std::path::Path::new(&bg_folder).exists() { + std::fs::create_dir_all(&bg_folder).unwrap(); + } + tauri::Builder::default() .invoke_handler(tauri::generate_handler![ enable_process_watcher, @@ -42,6 +48,7 @@ fn main() { system_helpers::run_program, system_helpers::run_jar, system_helpers::open_in_browser, + system_helpers::copy_file, proxy::set_proxy_addr, proxy::generate_ca_files, unzip::unzip, @@ -50,7 +57,8 @@ fn main() { downloader::download_file, downloader::stop_download, lang::get_lang, - lang::get_languages + lang::get_languages, + web::valid_url ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); @@ -136,7 +144,8 @@ async fn req_get(url: String) -> String { } #[tauri::command] -async fn get_bg_file(bg_path: String) -> String { +async fn get_bg_file(bg_path: String, appdata: String) -> String { + let copy_loc = appdata; let query = web::query("https://api.grasscutters.xyz/cultivation/query").await; let response_data: APIQuery = match serde_json::from_str(&query) { Ok(data) => data, @@ -149,9 +158,8 @@ async fn get_bg_file(bg_path: String) -> String { let file_name = response_data.bg_file.to_string(); // First we see if the file already exists in our local bg folder. - if file_helpers::dir_exists(format!(".\\bg\\{}", file_name).as_str()) { - let cwd = std::env::current_dir().unwrap(); - return format!("{}\\{}", cwd.display(), response_data.bg_file.as_str()); + if file_helpers::dir_exists(format!("{}\\bg\\{}", copy_loc, file_name).as_str()) { + return format!("{}\\{}", copy_loc, response_data.bg_file.as_str()); } // Now we check if the bg folder, which is one directory above the game_path, exists. @@ -169,13 +177,12 @@ async fn get_bg_file(bg_path: String) -> String { } // The image exists, lets copy it to our local '\bg' folder. - let bg_img_path_local = format!(".\\bg\\{}", file_name.as_str()); + 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) { Ok(_) => { // Copy was successful, lets return true. - let cwd = std::env::current_dir().unwrap(); - format!("{}\\{}", cwd.display(), response_data.bg_file.as_str()) + format!("{}\\{}", copy_loc, response_data.bg_file.as_str()) } Err(e) => { // Copy failed, lets return false diff --git a/src-tauri/src/system_helpers.rs b/src-tauri/src/system_helpers.rs index 4a79d79..8beb1e1 100644 --- a/src-tauri/src/system_helpers.rs +++ b/src-tauri/src/system_helpers.rs @@ -4,6 +4,8 @@ use std::process::Command; use tauri; use open; +use crate::file_helpers; + #[tauri::command] pub fn run_program(path: String) { // Open the program from the specified path. @@ -54,3 +56,32 @@ pub fn open_in_browser(url: String) { Err(e) => println!("Failed to open URL: {}", e), }; } + +#[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 + } + } +} + +pub fn install_location() -> String { + let mut exe_path = std::env::current_exe().unwrap(); + + // Get the path to the executable. + exe_path.pop(); + + return exe_path.to_str().unwrap().to_string(); +} \ No newline at end of file diff --git a/src-tauri/src/web.rs b/src-tauri/src/web.rs index 420a05b..c155f86 100644 --- a/src-tauri/src/web.rs +++ b/src-tauri/src/web.rs @@ -5,4 +5,14 @@ pub(crate) async fn query(site: &str) -> String { let response = client.get(site).header(USER_AGENT, "cultivation").send().await.unwrap(); return response.text().await.unwrap(); +} + +#[tauri::command] +pub(crate) async fn valid_url(url: String) -> bool { + // Check if we get a 200 response + let client = reqwest::Client::new(); + + let response = client.get(url).header(USER_AGENT, "cultivation").send().await.unwrap(); + + return response.status().as_str() == "200"; } \ No newline at end of file diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index a8c7ea7..6992257 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -18,6 +18,15 @@ "$DATA/cultivation/*" ] }, + "protocol": { + "all": true, + "asset": true, + "assetScope": [ + "$DATA", + "$DATA/cultivation", + "$DATA/cultivation/*" + ] + }, "all": true }, "bundle": { @@ -54,7 +63,7 @@ } }, "security": { - "csp": null + "csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost" }, "updater": { "active": false diff --git a/src/ui/App.css b/src/ui/App.css index 21d1f5c..99074c7 100644 --- a/src/ui/App.css +++ b/src/ui/App.css @@ -27,11 +27,8 @@ select:focus { } .App { - background-repeat: no-repeat; - background-size: cover; - -webkit-background-size: cover; - -moz-background-size: cover; - -o-background-size: cover; + background-size: cover !important; + background-position: center !important; } .TopButton { diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 08724c6..147cf29 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -20,6 +20,7 @@ import { getConfigOption, setConfigOption } from '../utils/configuration' import { invoke } from '@tauri-apps/api' import { dataDir } from '@tauri-apps/api/path' import { appWindow } from '@tauri-apps/api/window' +import { convertFileSrc } from '@tauri-apps/api/tauri' interface IProps { [key: string]: never; @@ -34,6 +35,8 @@ interface IState { bgFile: string; } +const DEFAULT_BG = 'https://api.grasscutters.xyz/content/bgfile' + const downloadHandler = new DownloadHandler() class App extends React.Component { @@ -45,7 +48,7 @@ class App extends React.Component { miniDownloadsOpen: false, downloadsOpen: false, gameDownloadsOpen: false, - bgFile: 'https://api.grasscutters.xyz/content/bgfile', + bgFile: DEFAULT_BG, } listen('lang_error', (payload) => { @@ -76,23 +79,39 @@ class App extends React.Component { const cert_generated = await getConfigOption('cert_generated') const game_exe = await getConfigOption('game_install_path') const custom_bg = await getConfigOption('customBackground') - const game_path = game_exe.substring(0, game_exe.lastIndexOf('\\')) - const root_path = game_path.substring(0, game_path.lastIndexOf('\\')) + const game_path = game_exe.substring(0, game_exe.replace(/\\/g, '/').lastIndexOf('/')) + const root_path = game_path.substring(0, game_path.replace(/\\/g, '/').lastIndexOf('/')) - if(!custom_bg) { + if(!custom_bg || !/png|jpg|jpeg$/.test(custom_bg)) { if(game_path) { // Get the bg by invoking, then set the background to that bg. const bgLoc: string = await invoke('get_bg_file', { - bgPath: root_path + bgPath: root_path, + appdata: await dataDir() }) bgLoc && this.setState({ bgFile: bgLoc - }) + }, this.forceUpdate) } - } else this.setState({ - bgFile: custom_bg - }) + } else { + const isUrl = /^(?:http(s)?:\/\/)/gm.test(custom_bg) + + if (!isUrl) { + this.setState({ + bgFile: convertFileSrc(custom_bg) + }, this.forceUpdate) + } else { + // Check if URL returns a valid image. + const isValid = await invoke('valid_url', { + url: custom_bg + }) + + this.setState({ + bgFile: isValid ? custom_bg : DEFAULT_BG + }, this.forceUpdate) + } + } if (!cert_generated) { // Generate the certificate @@ -109,15 +128,13 @@ class App extends React.Component { isDownloading: downloadHandler.getDownloads().filter(d => d.status !== 'finished')?.length > 0 }) }, 1000) - - console.log('mounting app component with background: ' + this.state.bgFile) } render() { return (
} // Launch the program - await invoke('run_program', { path: config.game_install_path }) + const gameExists = await invoke('dir_exists', { + path: 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) } async launchServer() { diff --git a/src/ui/components/common/TextInput.tsx b/src/ui/components/common/TextInput.tsx index 2d8dbe2..596b0d6 100644 --- a/src/ui/components/common/TextInput.tsx +++ b/src/ui/components/common/TextInput.tsx @@ -30,12 +30,6 @@ export default class TextInput extends React.Component { } static getDerivedStateFromProps(props: IProps, state: IState) { - if (!props.readOnly) { - return { - value: state.value - } - } - return { value: props.value || '' } } diff --git a/src/ui/components/menu/Downloads.tsx b/src/ui/components/menu/Downloads.tsx index 5bc75a3..6fb891f 100644 --- a/src/ui/components/menu/Downloads.tsx +++ b/src/ui/components/menu/Downloads.tsx @@ -10,6 +10,7 @@ import './Downloads.css' import Divider from './Divider' import { getConfigOption, setConfigOption } from '../../../utils/configuration' import { invoke } from '@tauri-apps/api' +import { listen } from '@tauri-apps/api/event' 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' @@ -48,12 +49,16 @@ export default class Downloads extends React.Component { this.downloadGrasscutterStable = this.downloadGrasscutterStable.bind(this) this.downloadGrasscutterLatest = this.downloadGrasscutterLatest.bind(this) this.downloadResources = this.downloadResources.bind(this) - this.disableButtons = this.disableButtons.bind(this) + this.toggleButtons = this.toggleButtons.bind(this) } async componentDidMount() { const gc_path = await getConfigOption('grasscutter_path') + listen('jar_extracted', () => { + this.setState({ grasscutter_set: true }, this.forceUpdate) + }) + if (!gc_path || gc_path === '') { this.setState({ grasscutter_set: false, @@ -102,43 +107,43 @@ export default class Downloads extends React.Component { async downloadGrasscutterStableRepo() { const folder = await this.getGrasscutterFolder() this.props.downloadManager.addDownload(STABLE_REPO_DOWNLOAD, folder + '\\grasscutter_repo.zip', () =>{ - unzip(folder + '\\grasscutter_repo.zip', folder + '\\') + unzip(folder + '\\grasscutter_repo.zip', folder + '\\', this.toggleButtons) }) - this.disableButtons() + this.toggleButtons() } async downloadGrasscutterDevRepo() { const folder = await this.getGrasscutterFolder() this.props.downloadManager.addDownload(DEV_REPO_DOWNLOAD, folder + '\\grasscutter_repo.zip', () =>{ - unzip(folder + '\\grasscutter_repo.zip', folder + '\\') + unzip(folder + '\\grasscutter_repo.zip', folder + '\\', this.toggleButtons) }) - this.disableButtons() + this.toggleButtons() } async downloadGrasscutterStable() { const folder = await this.getGrasscutterFolder() this.props.downloadManager.addDownload(STABLE_DOWNLOAD, folder + '\\grasscutter.zip', () =>{ - unzip(folder + '\\grasscutter.zip', folder + '\\') + unzip(folder + '\\grasscutter.zip', folder + '\\', this.toggleButtons) }) // Also add repo download this.downloadGrasscutterStableRepo() - this.disableButtons() + this.toggleButtons() } async downloadGrasscutterLatest() { const folder = await this.getGrasscutterFolder() this.props.downloadManager.addDownload(DEV_DOWNLOAD, folder + '\\grasscutter.zip', () =>{ - unzip(folder + '\\grasscutter.zip', folder + '\\') + unzip(folder + '\\grasscutter.zip', folder + '\\', this.toggleButtons) }) // Also add repo download this.downloadGrasscutterDevRepo() - this.disableButtons() + this.toggleButtons() } async downloadResources() { @@ -150,18 +155,23 @@ export default class Downloads extends React.Component { path: folder + '\\Resources', newName: 'resources' }) + + this.toggleButtons() }) }) - this.disableButtons() + this.toggleButtons() } - disableButtons() { + async toggleButtons() { + const gc_path = await getConfigOption('grasscutter_path') + // Set states since we know we are downloading something if this is called this.setState({ grasscutter_downloading: this.props.downloadManager.downloadingJar(), resources_downloading: this.props.downloadManager.downloadingResources(), - repo_downloading: this.props.downloadManager.downloadingRepo() + repo_downloading: this.props.downloadManager.downloadingRepo(), + grasscutter_set: gc_path && gc_path !== '', }) } diff --git a/src/ui/components/menu/Options.tsx b/src/ui/components/menu/Options.tsx index 9d0a35c..03b8826 100644 --- a/src/ui/components/menu/Options.tsx +++ b/src/ui/components/menu/Options.tsx @@ -6,6 +6,8 @@ import './Options.css' import { setConfigOption, getConfig, getConfigOption } from '../../../utils/configuration' import Checkbox from '../common/Checkbox' import Divider from './Divider' +import { invoke } from '@tauri-apps/api' +import { dataDir } from '@tauri-apps/api/path' interface IProps { closeFn: () => void; @@ -36,6 +38,7 @@ export default class Options extends React.Component { } this.toggleGrasscutterWithGame = this.toggleGrasscutterWithGame.bind(this) + this.setCustomBackground = this.setCustomBackground.bind(this) } async componentDidMount() { @@ -80,8 +83,28 @@ export default class Options extends React.Component { }) } - setCustomBackground(value: string) { - setConfigOption('customBackground', value) + async setCustomBackground(value: string) { + const isUrl = /^(?:http(s)?:\/\/)/gm.test(value) + + if (!value) return await setConfigOption('customBackground', '') + + if (!isUrl) { + const filename = value.replace(/\\/g, '/').split('/').pop() + const localBgPath = (await dataDir() as string).replace(/\\/g, '/') + + await setConfigOption('customBackground', `${localBgPath}/cultivation/bg/${filename}`) + + // Copy the file over to the local directory + await invoke('copy_file', { + path: value.replace(/\\/g, '/'), + newPath: `${localBgPath}cultivation/bg/` + }) + + window.location.reload() + } else { + await setConfigOption('customBackground', value) + window.location.reload() + } } render() {