Push custom-options

This commit is contained in:
KingRainbow44
2022-07-06 01:38:34 -04:00
parent a3e1898d82
commit c1a41bec65
13 changed files with 265 additions and 17 deletions

View File

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

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

11
src-tauri/Cargo.lock generated
View File

@@ -740,7 +740,7 @@ checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35"
[[package]] [[package]]
name = "cultivation" name = "cultivation"
version = "0.1.0" version = "1.0.1"
dependencies = [ dependencies = [
"duct", "duct",
"futures-util", "futures-util",
@@ -2103,6 +2103,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "minisign-verify"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "933dca44d65cdd53b355d0b73d380a2ff5da71f87f036053188bf1eab6a19881"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.5.1" version = "0.5.1"
@@ -3886,6 +3892,7 @@ checksum = "a34cef4a0ebee0230baaa319b1709c4336f4add550149d2b005a9a5dc5d33617"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"attohttpc", "attohttpc",
"base64",
"bincode", "bincode",
"cocoa", "cocoa",
"dirs-next", "dirs-next",
@@ -3899,6 +3906,7 @@ dependencies = [
"heck 0.4.0", "heck 0.4.0",
"http", "http",
"ignore", "ignore",
"minisign-verify",
"notify-rust", "notify-rust",
"objc", "objc",
"once_cell", "once_cell",
@@ -3930,6 +3938,7 @@ dependencies = [
"webkit2gtk", "webkit2gtk",
"webview2-com", "webview2-com",
"windows 0.30.0", "windows 0.30.0",
"zip 0.6.2",
] ]
[[package]] [[package]]

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"
@@ -16,7 +16,7 @@ tauri-build = { version = "1.0.0-rc.8", features = [] }
[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.23.12" sysinfo = "0.23.12"

View File

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

@@ -33,7 +33,7 @@
background: #fff; background: #fff;
} }
.BottomSection .CheckboxDisplay { .BottomSection .CheckboxDisplay {
margin-right: 6px; margin-right: 6px;
box-shadow: 0 0 5px 4px rgba(0, 0, 0, 0.2); box-shadow: 0 0 5px 4px rgba(0, 0, 0, 0.2);
} }

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

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
@@ -13,6 +15,16 @@ interface Theme {
css: string[] css: string[]
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()