diff --git a/.eslintrc.json b/.eslintrc.json
index 638c08f..1f792fa 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -33,6 +33,7 @@
"semi": [
"error",
"never"
- ]
+ ],
+ "no-explicit-any": "off"
}
}
diff --git a/public/index.html b/public/index.html
index 030b8e7..89e366f 100644
--- a/public/index.html
+++ b/public/index.html
@@ -12,7 +12,9 @@
Cultivation
+
+
diff --git a/public/theme-engine.js b/public/theme-engine.js
new file mode 100644
index 0000000..bfc7eb0
--- /dev/null
+++ b/public/theme-engine.js
@@ -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})
+}
\ No newline at end of file
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index f2d5257..4837726 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -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]]
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 7c90f77..8b84fa8 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -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"
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 58af5c8..5ab5241 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -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}}",
diff --git a/src/index.tsx b/src/index.tsx
index e2b5cab..b049f99 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -17,6 +17,7 @@ let isDebug = false;
isDebug = await getConfigOption('debug_enabled')
})
+// Render the app.
root.render(
{
@@ -25,5 +26,10 @@ root.render(
)
+// Enable web vitals if needed.
import reportWebVitals from './utils/reportWebVitals'
-isDebug && reportWebVitals(console.log)
\ No newline at end of file
+isDebug && reportWebVitals(console.log)
+
+// Setup DOM message passing.
+import { parseMessageFromDOM } from './utils/dom'
+document.addEventListener('domMessage', parseMessageFromDOM)
\ No newline at end of file
diff --git a/src/resources/example-theme/index.json b/src/resources/example-theme/index.json
new file mode 100644
index 0000000..b73503d
--- /dev/null
+++ b/src/resources/example-theme/index.json
@@ -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": ""
+}
\ No newline at end of file
diff --git a/src/ui/components/ServerLaunchSection.css b/src/ui/components/ServerLaunchSection.css
index 1ed8762..a5f6850 100644
--- a/src/ui/components/ServerLaunchSection.css
+++ b/src/ui/components/ServerLaunchSection.css
@@ -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);
}
diff --git a/src/ui/components/common/ThemeOptionValue.tsx b/src/ui/components/common/ThemeOptionValue.tsx
new file mode 100644
index 0000000..d8bcb4d
--- /dev/null
+++ b/src/ui/components/common/ThemeOptionValue.tsx
@@ -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 {
+ 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 (
+
+
+
+ )
+ case 'dropdown':
+ return (
+
+
+
+ )
+ case 'button':
+ return (
+
+
+
+ )
+ default:
+ return (
+
+
+
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/ui/components/menu/Options.tsx b/src/ui/components/menu/Options.tsx
index 4341fc5..50e6af5 100644
--- a/src/ui/components/menu/Options.tsx
+++ b/src/ui/components/menu/Options.tsx
@@ -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 {
@@ -44,7 +47,9 @@ export default class Options extends React.Component {
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 {
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 {
}
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 {
}
render() {
+ const themeSettings = this.state.theme_object?.settings
+
return (
)
}
diff --git a/src/utils/dom.ts b/src/utils/dom.ts
new file mode 100644
index 0000000..f8ba56d
--- /dev/null
+++ b/src/utils/dom.ts
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/src/utils/themes.ts b/src/utils/themes.ts
index 6e78587..8562ebf 100644
--- a/src/utils/themes.ts
+++ b/src/utils/themes.ts
@@ -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()