mirror of
https://github.com/Grasscutters/Cultivation.git
synced 2025-01-07 03:26:56 +08:00
Push custom-options
This commit is contained in:
parent
a3e1898d82
commit
c1a41bec65
@ -33,6 +33,7 @@
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
]
|
||||
],
|
||||
"no-explicit-any": "off"
|
||||
}
|
||||
}
|
||||
|
@ -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
14
public/theme-engine.js
Normal 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
11
src-tauri/Cargo.lock
generated
@ -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]]
|
||||
|
@ -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"
|
||||
|
@ -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}}",
|
||||
|
@ -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)
|
34
src/resources/example-theme/index.json
Normal file
34
src/resources/example-theme/index.json
Normal 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": ""
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
105
src/ui/components/common/ThemeOptionValue.tsx
Normal file
105
src/ui/components/common/ThemeOptionValue.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
31
src/utils/dom.ts
Normal 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
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user