diff --git a/src-tauri/lang/en.json b/src-tauri/lang/en.json index 4696d01..04ae9da 100644 --- a/src-tauri/lang/en.json +++ b/src-tauri/lang/en.json @@ -16,8 +16,9 @@ "grasscutter_jar": "Set Grasscutter JAR", "java_path": "Set Custom Java Path", "grasscutter_with_game": "Automatically launch Grasscutter with game", - "language": "Select Language (requires restart)", - "background": "Set Custom Background (link or image file)" + "language": "Select Language", + "background": "Set Custom Background (link or image file)", + "theme": "Set Theme" }, "downloads": { "grasscutter_stable_data": "Download Grasscutter Stable Data", diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 3d2418d..0f1aae1 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -4,7 +4,7 @@ windows_subsystem = "windows" )] use lazy_static::lazy_static; -use std::sync::Mutex; +use std::{sync::Mutex, collections::HashMap}; use std::thread; use sysinfo::{System, SystemExt}; @@ -44,11 +44,13 @@ fn main() { get_bg_file, base64_decode, is_game_running, + get_theme_list, system_helpers::run_command, system_helpers::run_program, system_helpers::run_jar, system_helpers::open_in_browser, system_helpers::copy_file, + system_helpers::install_location, proxy::set_proxy_addr, proxy::generate_ca_files, unzip::unzip, @@ -143,6 +145,42 @@ async fn req_get(url: String) -> String { return response; } +#[tauri::command] +async fn get_theme_list(dataDir: String) -> Vec> { + let theme_loc = format!("{}/themes", dataDir); + + // Ensure folder exists + if !std::path::Path::new(&theme_loc).exists() { + std::fs::create_dir_all(&theme_loc).unwrap(); + } + + // Read each index.json folder in each theme folder + let mut themes = Vec::new(); + + for entry in std::fs::read_dir(&theme_loc).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + + if path.is_dir() { + let index_path = format!("{}/index.json", path.to_str().unwrap()); + + if std::path::Path::new(&index_path).exists() { + let theme_json = std::fs::read_to_string(&index_path).unwrap(); + + let mut map = HashMap::new(); + + map.insert("json".to_string(), theme_json); + map.insert("path".to_string(), path.to_str().unwrap().to_string()); + + // Push key-value pair containing "json" and "path" + themes.push(map); + } + } + } + + return themes; +} + #[tauri::command] async fn get_bg_file(bg_path: String, appdata: String) -> String { let copy_loc = appdata; diff --git a/src-tauri/src/system_helpers.rs b/src-tauri/src/system_helpers.rs index 8beb1e1..5d608c2 100644 --- a/src-tauri/src/system_helpers.rs +++ b/src-tauri/src/system_helpers.rs @@ -36,9 +36,9 @@ pub fn run_command(command: String) { #[tauri::command] pub fn run_jar(path: String, execute_in: String, java_path: String) { let command = if java_path.is_empty() { - format!("java -jar {}", path) + format!("java -jar \"{}\"", path) } else { - format!("\"{}\" -jar {}", java_path, path) + format!("\"{}\" -jar \"{}\"", java_path, path) }; // Open the program from the specified path. @@ -77,6 +77,7 @@ pub fn copy_file(path: String, new_path: String) -> bool { } } +#[tauri::command] pub fn install_location() -> String { let mut exe_path = std::env::current_exe().unwrap(); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 6992257..38087a7 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -63,7 +63,7 @@ } }, "security": { - "csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost" + "csp": "default-src 'self' https://asset.localhost; img-src 'self'; img-src https://* asset: https://asset.localhost" }, "updater": { "active": false diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 147cf29..581a48f 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -18,9 +18,10 @@ import Game from './components/menu/Game' import RightBar from './components/RightBar' import { getConfigOption, setConfigOption } from '../utils/configuration' import { invoke } from '@tauri-apps/api' -import { dataDir } from '@tauri-apps/api/path' +import { appDir, dataDir } from '@tauri-apps/api/path' import { appWindow } from '@tauri-apps/api/window' import { convertFileSrc } from '@tauri-apps/api/tauri' +import { getTheme, loadTheme } from '../utils/themes' interface IProps { [key: string]: never; @@ -79,8 +80,15 @@ 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.replace(/\\/g, '/').lastIndexOf('/')) - const root_path = game_path.substring(0, game_path.replace(/\\/g, '/').lastIndexOf('/')) + const game_path = game_exe?.substring(0, game_exe.replace(/\\/g, '/').lastIndexOf('/')) || '' + const root_path = game_path?.substring(0, game_path.replace(/\\/g, '/').lastIndexOf('/')) || '' + + // Load a theme if it exists + const theme = await getConfigOption('theme') + if (theme && theme !== 'default') { + const themeObj = await getTheme(theme) + loadTheme(themeObj, document) + } if(!custom_bg || !/png|jpg|jpeg$/.test(custom_bg)) { if(game_path) { @@ -98,8 +106,12 @@ class App extends React.Component { const isUrl = /^(?:http(s)?:\/\/)/gm.test(custom_bg) if (!isUrl) { + const isValid = await invoke('dir_exists', { + path: custom_bg + }) + this.setState({ - bgFile: convertFileSrc(custom_bg) + bgFile: isValid ? convertFileSrc(custom_bg) : DEFAULT_BG }, this.forceUpdate) } else { // Check if URL returns a valid image. diff --git a/src/ui/components/common/DirInput.tsx b/src/ui/components/common/DirInput.tsx index 024b2eb..ece7bd3 100644 --- a/src/ui/components/common/DirInput.tsx +++ b/src/ui/components/common/DirInput.tsx @@ -14,6 +14,7 @@ interface IProps { readonly?: boolean placeholder?: string folder?: boolean + customClearBehaviour?: () => void } interface IState { @@ -94,7 +95,9 @@ export default class DirInput extends React.Component { this.setState({ value: text }) if (this.props.onChange) this.props.onChange(text) + this.forceUpdate() }} + customClearBehaviour={this.props.customClearBehaviour} />
diff --git a/src/ui/components/common/TextInput.tsx b/src/ui/components/common/TextInput.tsx index 596b0d6..e2009ea 100644 --- a/src/ui/components/common/TextInput.tsx +++ b/src/ui/components/common/TextInput.tsx @@ -11,6 +11,7 @@ interface IProps { readOnly?: boolean; id?: string; clearable?: boolean; + customClearBehaviour?: () => void; style?: { [key: string]: any; } @@ -29,10 +30,6 @@ export default class TextInput extends React.Component { } } - static getDerivedStateFromProps(props: IProps, state: IState) { - return { value: props.value || '' } - } - async componentDidMount() { if (this.props.initalValue) { this.setState({ @@ -41,6 +38,10 @@ export default class TextInput extends React.Component { } } + static getDerivedStateFromProps(props: IProps, state: IState) { + return { value: props.value || state.value } + } + render() { return (
@@ -51,6 +52,9 @@ export default class TextInput extends React.Component { { this.props.clearable ?
{ + // Run custom behaviour first + if (this.props.customClearBehaviour) return this.props.customClearBehaviour() + this.setState({ value: '' }) if (this.props.onChange) this.props.onChange('') diff --git a/src/ui/components/menu/Options.tsx b/src/ui/components/menu/Options.tsx index 03b8826..a54062b 100644 --- a/src/ui/components/menu/Options.tsx +++ b/src/ui/components/menu/Options.tsx @@ -1,13 +1,15 @@ import React from 'react' +import { invoke } from '@tauri-apps/api' +import { dataDir } from '@tauri-apps/api/path' import DirInput from '../common/DirInput' import Menu from './Menu' import Tr, { getLanguages } from '../../../utils/language' -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' +import { getThemeList } from '../../../utils/themes' + +import './Options.css' interface IProps { closeFn: () => void; @@ -21,6 +23,8 @@ interface IState { language_options: { [key: string]: string }[], current_language: string bg_url_or_path: string + themes: string[] + theme: string } export default class Options extends React.Component { @@ -34,9 +38,14 @@ export default class Options extends React.Component { grasscutter_with_game: false, language_options: [], current_language: 'en', - bg_url_or_path: '' + bg_url_or_path: '', + themes: ['default'], + theme: '' } + this.setGameExec = this.setGameExec.bind(this) + this.setGrasscutterJar = this.setGrasscutterJar.bind(this) + this.setJavaPath = this.setJavaPath.bind(this) this.toggleGrasscutterWithGame = this.toggleGrasscutterWithGame.bind(this) this.setCustomBackground = this.setCustomBackground.bind(this) } @@ -52,7 +61,9 @@ export default class Options extends React.Component { grasscutter_with_game: config.grasscutter_with_game || false, language_options: languages, current_language: config.language || 'en', - bg_url_or_path: config.customBackground || '' + bg_url_or_path: config.customBackground || '', + themes: (await getThemeList()).map(t => t.name), + theme: config.theme || 'default' }) this.forceUpdate() @@ -60,18 +71,36 @@ export default class Options extends React.Component { setGameExec(value: string) { setConfigOption('game_install_path', value) + + this.setState({ + game_install_path: value + }) } setGrasscutterJar(value: string) { setConfigOption('grasscutter_path', value) + + this.setState({ + grasscutter_path: value + }) } setJavaPath(value: string) { setConfigOption('java_path', value) + + this.setState({ + java_path: value + }) } - setLanguage(value: string) { - setConfigOption('language', value) + async setLanguage(value: string) { + await setConfigOption('language', value) + window.location.reload() + } + + async setTheme(value: string) { + await setConfigOption('theme', value) + window.location.reload() } async toggleGrasscutterWithGame() { @@ -140,6 +169,28 @@ export default class Options extends React.Component { +
+
+ +
+
+ +
+
+ + +
@@ -154,7 +205,17 @@ export default class Options extends React.Component {
- + { + await setConfigOption('customBackground', '') + window.location.reload() + }} + />
diff --git a/src/ui/components/news/NewsSection.css b/src/ui/components/news/NewsSection.css index 104178f..57c4dd6 100644 --- a/src/ui/components/news/NewsSection.css +++ b/src/ui/components/news/NewsSection.css @@ -65,7 +65,7 @@ scrollbar-width: none; } -.NewsContent tbody::-webkit-scrollbar { +.NewsContent tbody::-webkit-scrollbar { display: none; } @@ -94,4 +94,4 @@ -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 5px; -} +} \ No newline at end of file diff --git a/src/utils/configuration.ts b/src/utils/configuration.ts index 4594d4a..3b984d2 100644 --- a/src/utils/configuration.ts +++ b/src/utils/configuration.ts @@ -7,7 +7,7 @@ let defaultConfig: Configuration (async() => { defaultConfig = { toggle_grasscutter: false, - game_install_path: 'C:\\Program Files\\Genshin Impact\\Genshin Impact game\\Genshin Impact.exe', + game_install_path: 'C:\\Program Files\\Genshin Impact\\Genshin Impact game\\GenshinImpact.exe', grasscutter_with_game: false, grasscutter_path: '', java_path: '', @@ -18,6 +18,7 @@ let defaultConfig: Configuration language: 'en', customBackground: '', cert_generated: false, + theme: 'default' } })() @@ -37,6 +38,7 @@ export interface Configuration { language: string customBackground: string cert_generated: boolean + theme: string; } export async function setConfigOption(key: string, value: any): Promise { diff --git a/src/utils/themes.ts b/src/utils/themes.ts new file mode 100644 index 0000000..e0c3309 --- /dev/null +++ b/src/utils/themes.ts @@ -0,0 +1,128 @@ +import { invoke } from '@tauri-apps/api' +import { dataDir } from '@tauri-apps/api/path' +import { convertFileSrc } from '@tauri-apps/api/tauri' + +interface Theme { + name: string + version: string + description: string + + // Included custom CSS and JS files + includes: { + css: string[] + js: string[] + } + + customBackgroundURL?: string + customBackgroundPath?: string +} + +interface BackendThemeList { + json: string + path: string +} + +interface ThemeList extends Theme { + path: string +} + +const defaultTheme = { + name: 'default', + version: '1.0.0', + description: 'Default theme', + includes: { + css: [], + js: [] + }, + path: 'default' +} +export async function getThemeList() { + // Do some invoke to backend to get the theme list + const themes = await invoke('get_theme_list', { + dataDir: `${await dataDir()}/cultivation` + }) as BackendThemeList[] + const list: ThemeList[] = [ + // ALWAYS include default theme + { + name: 'default', + version: '1.0.0', + description: 'Default theme', + includes: { + css: [], + js: [] + }, + path: 'default' + } + ] + + themes.forEach(t => { + let obj + + try { + obj = JSON.parse(t.json) + } catch (e) { + console.error(e) + } + + list.push({ ...obj, path: t.path }) + }) + + return list +} + +export async function getTheme(name: string) { + const themes = await getThemeList() + + return themes.find(t => t.name === name) || defaultTheme +} + +export async function loadTheme(theme: ThemeList, document: Document) { + // We are going to dynamically load stylesheets into the document + const head = document.head + + // Get all CSS includes + const cssIncludes = theme.includes.css + const jsIncludes = theme.includes.js + + // Load CSS files + cssIncludes.forEach(css => { + if (!css) return + + const link = document.createElement('link') + + link.rel = 'stylesheet' + link.href = convertFileSrc(theme.path + '/' + css) + head.appendChild(link) + }) + + // Load JS files + jsIncludes.forEach(js => { + if (!js) return + + const script = document.createElement('script') + + script.src = convertFileSrc(theme.path + '/' + js) + head.appendChild(script) + }) + + // Set custom background + if (theme.customBackgroundURL) { + document.body.style.backgroundImage = `url('${theme.customBackgroundURL}')` + } + + // Set custom background + if (theme.customBackgroundPath) { + const bgPath = await dataDir() + 'cultivation/grasscutter/theme.png' + + // Save the background to our data dir + await invoke('copy_file', { + path: theme.path + '/' + theme.customBackgroundPath, + new_path: bgPath + }) + + // Set the background + document.body.style.backgroundImage = `url('${bgPath}')` + } + + return +} \ No newline at end of file