Push custom-options

This commit is contained in:
KingRainbow44 2022-07-06 01:38:34 -04:00
parent a3e1898d82
commit c1a41bec65
No known key found for this signature in database
GPG Key ID: FC2CB64B00D257BE
13 changed files with 265 additions and 17 deletions

View File

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

View File

@ -12,7 +12,9 @@
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Cultivation</title>
<script src="%PUBLIC_URL%/theme-engine.js"></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<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]]
name = "cultivation"
version = "0.1.0"
version = "1.0.1"
dependencies = [
"duct",
"futures-util",
@ -2103,6 +2103,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "minisign-verify"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "933dca44d65cdd53b355d0b73d380a2ff5da71f87f036053188bf1eab6a19881"
[[package]]
name = "miniz_oxide"
version = "0.5.1"
@ -3886,6 +3892,7 @@ checksum = "a34cef4a0ebee0230baaa319b1709c4336f4add550149d2b005a9a5dc5d33617"
dependencies = [
"anyhow",
"attohttpc",
"base64",
"bincode",
"cocoa",
"dirs-next",
@ -3899,6 +3906,7 @@ dependencies = [
"heck 0.4.0",
"http",
"ignore",
"minisign-verify",
"notify-rust",
"objc",
"once_cell",
@ -3930,6 +3938,7 @@ dependencies = [
"webkit2gtk",
"webview2-com",
"windows 0.30.0",
"zip 0.6.2",
]
[[package]]

View File

@ -1,9 +1,9 @@
[package]
name = "cultivation"
version = "0.1.0"
version = "1.0.1"
description = "A custom launcher for anime game."
authors = ["KingRainbow44", "SpikeHD"]
license = ""
license = "Apache-2.0"
repository = "https://github.com/Grasscutters/Cultivation.git"
default-run = "cultivation"
edition = "2021"
@ -16,7 +16,7 @@ tauri-build = { version = "1.0.0-rc.8", features = [] }
[dependencies]
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.
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"
},
"updater": {
"active": false,
"active": true,
"dialog": true,
"endpoints": [
"https://api.grasscutter.io/cultivation/updater?version={{current_version}}",

View File

@ -17,6 +17,7 @@ let isDebug = false;
isDebug = await getConfigOption('debug_enabled')
})
// Render the app.
root.render(
<React.StrictMode>
{
@ -25,5 +26,10 @@ root.render(
</React.StrictMode>
)
// Enable web vitals if needed.
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;
}
.BottomSection .CheckboxDisplay {
.BottomSection .CheckboxDisplay {
margin-right: 6px;
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 Checkbox from '../common/Checkbox'
import Divider from './Divider'
import { getThemeList } from '../../../utils/themes'
import { getTheme, getThemeList, ThemeList } from '../../../utils/themes'
import * as server from '../../../utils/server'
import './Options.css'
import BigButton from '../common/BigButton'
import ThemeOptionValue from '../common/ThemeOptionValue'
interface IProps {
closeFn: () => void;
@ -28,6 +29,8 @@ interface IState {
themes: string[]
theme: string
encryption: boolean
theme_object: ThemeList|null;
}
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: '',
themes: ['default'],
theme: '',
encryption: false
encryption: false,
theme_object: null
}
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 || '',
themes: (await getThemeList()).map(t => t.name),
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()
@ -124,7 +131,7 @@ export default class Options extends React.Component<IProps, IState> {
}
async setCustomBackground(value: string) {
const isUrl = /^(?:http(s)?:\/\/)/gm.test(value)
const isUrl = /^http(s)?:\/\//gm.test(value)
if (!value) return await setConfigOption('customBackground', '')
@ -168,6 +175,8 @@ export default class Options extends React.Component<IProps, IState> {
}
render() {
const themeSettings = this.state.theme_object?.settings
return (
<Menu closeFn={this.props.closeFn} className="Options" heading="Options">
<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']} />
</div>
</div>
<div className='OptionSection' id="menuOptionsContainerGCJar">
<div className='OptionLabel' id="menuOptionsLabelGCJar">
<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']} />
</div>
</div>
<div className='OptionSection' id="menuOptionsContainerToggleEnc">
<div className='OptionLabel' id="menuOptionsLabelToggleEnc">
<Tr text="options.toggle_encryption" />
@ -281,6 +292,23 @@ export default class Options extends React.Component<IProps, IState> {
</select>
</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>
)
}

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