Merge remote-tracking branch 'origin/main'

This commit is contained in:
KingRainbow44
2022-06-05 13:53:05 -04:00
11 changed files with 275 additions and 25 deletions

View File

@@ -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",

View File

@@ -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<HashMap<String, String>> {
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;

View File

@@ -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();

View File

@@ -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

View File

@@ -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<IProps, IState> {
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<IProps, IState> {
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.

View File

@@ -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<IProps, IState> {
this.setState({ value: text })
if (this.props.onChange) this.props.onChange(text)
this.forceUpdate()
}}
customClearBehaviour={this.props.customClearBehaviour}
/>
<div className="FileSelectIcon" onClick={this.handleIconClick}>
<img src={File} />

View File

@@ -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<IProps, IState> {
}
}
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<IProps, IState> {
}
}
static getDerivedStateFromProps(props: IProps, state: IState) {
return { value: props.value || state.value }
}
render() {
return (
<div className="TextInputWrapper" style={this.props.style || {}}>
@@ -51,6 +52,9 @@ export default class TextInput extends React.Component<IProps, IState> {
{
this.props.clearable ?
<div className="TextClear" onClick={() => {
// Run custom behaviour first
if (this.props.customClearBehaviour) return this.props.customClearBehaviour()
this.setState({ value: '' })
if (this.props.onChange) this.props.onChange('')

View File

@@ -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<IProps, IState> {
@@ -34,9 +38,14 @@ export default class Options extends React.Component<IProps, IState> {
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<IProps, IState> {
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<IProps, IState> {
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<IProps, IState> {
<Divider />
<div className='OptionSection'>
<div className='OptionLabel'>
<Tr text="options.theme" />
</div>
<div className='OptionValue'>
<select value={this.state.theme} onChange={(event) => {
this.setTheme(event.target.value)
}}>
{this.state.themes.map(t => (
<option
key={t}
value={t}>
{t}
</option>
))}
</select>
</div>
</div>
<Divider />
<div className='OptionSection'>
<div className='OptionLabel'>
<Tr text="options.java_path" />
@@ -154,7 +205,17 @@ export default class Options extends React.Component<IProps, IState> {
<Tr text="options.background" />
</div>
<div className='OptionValue'>
<DirInput onChange={this.setCustomBackground} value={this.state?.bg_url_or_path} extensions={['png', 'jpg', 'jpeg']} readonly={false} />
<DirInput
onChange={this.setCustomBackground}
value={this.state?.bg_url_or_path}
extensions={['png', 'jpg', 'jpeg']}
readonly={false}
clearable={true}
customClearBehaviour={async () => {
await setConfigOption('customBackground', '')
window.location.reload()
}}
/>
</div>
</div>

View File

@@ -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;
}
}

View File

@@ -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<void> {

128
src/utils/themes.ts Normal file
View File

@@ -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
}