mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-10 19:14:16 +00:00
split components
This commit is contained in:
parent
aa3e6dc53c
commit
7069073d06
@ -0,0 +1,517 @@
|
|||||||
|
import React from "react"
|
||||||
|
import * as antd from "antd"
|
||||||
|
|
||||||
|
import { Translation } from "react-i18next"
|
||||||
|
import { SliderPicker } from "react-color"
|
||||||
|
|
||||||
|
import { Icons, createIconRender } from "components/Icons"
|
||||||
|
|
||||||
|
class PerformanceLog {
|
||||||
|
constructor(
|
||||||
|
id,
|
||||||
|
params = {
|
||||||
|
disabled: false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
this.id = id
|
||||||
|
this.params = params
|
||||||
|
|
||||||
|
this.table = {}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
start(event) {
|
||||||
|
if (this.params.disabled) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.table[event]) {
|
||||||
|
this.table[event] = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.table[event]["start"] = performance.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
end(event) {
|
||||||
|
if (this.params.disabled) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.table[event]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.table[event]["end"] = performance.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
finally() {
|
||||||
|
if (this.params.disabled) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
console.group(this.id)
|
||||||
|
|
||||||
|
Object.entries(this.table).forEach(([entry, value]) => {
|
||||||
|
console.log(entry, `${(value.end - value.start).toFixed(0)}ms`)
|
||||||
|
})
|
||||||
|
|
||||||
|
console.groupEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SettingsComponents = {
|
||||||
|
button: {
|
||||||
|
component: antd.Button,
|
||||||
|
props: (_this) => {
|
||||||
|
return {
|
||||||
|
onClick: (event) => _this.onUpdateItem(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
switch: {
|
||||||
|
component: antd.Switch,
|
||||||
|
props: (_this) => {
|
||||||
|
return {
|
||||||
|
onChange: (event) => _this.onUpdateItem(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
slider: {
|
||||||
|
component: antd.Slider,
|
||||||
|
props: (_this) => {
|
||||||
|
return {
|
||||||
|
onAfterChange: (event) => _this.onUpdateItem(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
component: antd.Input,
|
||||||
|
props: (_this) => {
|
||||||
|
return {
|
||||||
|
defaultValue: _this.state.value,
|
||||||
|
onChange: (event) => _this.onUpdateItem(event.target.value),
|
||||||
|
onPressEnter: (event) => _this.dispatchUpdate(event.target.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
textarea: {
|
||||||
|
component: antd.Input.TextArea,
|
||||||
|
props: (_this) => {
|
||||||
|
return {
|
||||||
|
defaultValue: _this.state.value,
|
||||||
|
onChange: (event) => _this.onUpdateItem(event.target.value),
|
||||||
|
onPressEnter: (event) => _this.dispatchUpdate(event.target.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
inputnumber: {
|
||||||
|
component: antd.InputNumber,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
component: antd.Select,
|
||||||
|
props: (_this) => {
|
||||||
|
return {
|
||||||
|
onChange: (event) => {
|
||||||
|
console.log(event)
|
||||||
|
_this.onUpdateItem(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
slidercolorpicker: {
|
||||||
|
component: SliderPicker,
|
||||||
|
props: (_this) => {
|
||||||
|
return {
|
||||||
|
onChange: (color) => {
|
||||||
|
_this.setState({
|
||||||
|
componentProps: {
|
||||||
|
..._this.state.componentProps,
|
||||||
|
color
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onChangeComplete: (color) => {
|
||||||
|
_this.onUpdateItem(color.hex)
|
||||||
|
},
|
||||||
|
color: _this.state.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class SettingItemComponent extends React.PureComponent {
|
||||||
|
state = {
|
||||||
|
value: null,
|
||||||
|
debouncedValue: null,
|
||||||
|
|
||||||
|
componentProps: Object(),
|
||||||
|
loading: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
perf = new PerformanceLog(`Init ${this.props.setting.id}`, {
|
||||||
|
disabled: true
|
||||||
|
})
|
||||||
|
|
||||||
|
componentType = null
|
||||||
|
|
||||||
|
componentRef = React.createRef()
|
||||||
|
|
||||||
|
componentDidMount = async () => {
|
||||||
|
if (typeof this.props.setting.component === "string") {
|
||||||
|
this.componentType = String(this.props.setting.component).toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount = () => {
|
||||||
|
this.setState({
|
||||||
|
value: null,
|
||||||
|
componentProps: Object(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (typeof this.props.setting.dependsOn === "object") {
|
||||||
|
for (const key in this.props.setting.dependsOn) {
|
||||||
|
window.app.eventBus.off(`setting.update.${key}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generateInhertedProps = () => {
|
||||||
|
if (!SettingsComponents[this.componentType]) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof SettingsComponents[this.componentType].props === "function") {
|
||||||
|
const inhertedProps = SettingsComponents[this.componentType].props(this)
|
||||||
|
|
||||||
|
return inhertedProps
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
toogleLoading = (to) => {
|
||||||
|
if (typeof to === "undefined") {
|
||||||
|
to = !this.state.loading
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
loading: to
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize = async () => {
|
||||||
|
this.perf.start(`init tooks`)
|
||||||
|
|
||||||
|
this.toogleLoading(true)
|
||||||
|
|
||||||
|
if (this.props.setting.storaged) {
|
||||||
|
this.perf.start(`get value from storaged`)
|
||||||
|
|
||||||
|
await this.setState({
|
||||||
|
value: window.app.cores.settings.get(this.props.setting.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
this.perf.end(`get value from storaged`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this.props.setting.defaultValue === "function") {
|
||||||
|
this.perf.start(`execute default value fn`)
|
||||||
|
|
||||||
|
this.toogleLoading(true)
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
value: await this.props.setting.defaultValue(this.props.ctx)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.toogleLoading(false)
|
||||||
|
|
||||||
|
this.perf.end(`execute default value fn`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this.props.setting.dependsOn === "object") {
|
||||||
|
this.perf.start(`register dependsOn events`)
|
||||||
|
|
||||||
|
Object.keys(this.props.setting.dependsOn).forEach((key) => {
|
||||||
|
// create a event handler to watch changes
|
||||||
|
window.app.eventBus.on(`setting.update.${key}`, () => {
|
||||||
|
this.setState({
|
||||||
|
componentProps: {
|
||||||
|
...this.state.componentProps,
|
||||||
|
disabled: this.checkDependsValidation()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.perf.end(`register dependsOn events`)
|
||||||
|
|
||||||
|
this.perf.start(`check depends validation`)
|
||||||
|
|
||||||
|
// by default check depends validation
|
||||||
|
this.setState({
|
||||||
|
componentProps: {
|
||||||
|
...this.state.componentProps,
|
||||||
|
disabled: this.checkDependsValidation()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.perf.end(`check depends validation`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this.props.setting.listenUpdateValue === "string") {
|
||||||
|
this.perf.start(`listen "on update" value`)
|
||||||
|
|
||||||
|
window.app.eventBus.on(`setting.update.${this.props.setting.listenUpdateValue}`, () => {
|
||||||
|
this.setState({
|
||||||
|
value: window.app.cores.settings.get(this.props.setting.id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.perf.end(`listen "on update" value`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.setting.reloadValueOnUpdateEvent) {
|
||||||
|
this.perf.start(`Reinitializing setting [${this.props.setting.id}]`)
|
||||||
|
|
||||||
|
window.app.eventBus.on(this.props.setting.reloadValueOnUpdateEvent, () => {
|
||||||
|
console.log(`Reinitializing setting [${this.props.setting.id}]`)
|
||||||
|
this.initialize()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.perf.end(`Reinitializing setting [${this.props.setting.id}]`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.toogleLoading(false)
|
||||||
|
|
||||||
|
this.perf.end(`init tooks`)
|
||||||
|
|
||||||
|
this.perf.finally()
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchUpdate = async (updateValue) => {
|
||||||
|
if (typeof this.props.setting.onUpdate === "function") {
|
||||||
|
try {
|
||||||
|
const result = await this.props.setting.onUpdate(updateValue)
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
updateValue = result
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
|
||||||
|
if (error.response.data.error) {
|
||||||
|
app.message.error(error.response.data.error)
|
||||||
|
} else {
|
||||||
|
app.message.error(error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const storagedValue = window.app.cores.settings.get(this.props.setting.id)
|
||||||
|
|
||||||
|
if (typeof updateValue === "undefined") {
|
||||||
|
updateValue = !storagedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.setting.storaged) {
|
||||||
|
await window.app.cores.settings.set(this.props.setting.id, updateValue)
|
||||||
|
|
||||||
|
if (typeof this.props.setting.beforeSave === "function") {
|
||||||
|
await this.props.setting.beforeSave(updateValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this.props.setting.emitEvent !== "undefined") {
|
||||||
|
if (typeof this.props.setting.emitEvent === "string") {
|
||||||
|
this.props.setting.emitEvent = [this.props.setting.emitEvent]
|
||||||
|
}
|
||||||
|
|
||||||
|
let emissionPayload = updateValue
|
||||||
|
|
||||||
|
if (typeof this.props.setting.emissionValueUpdate === "function") {
|
||||||
|
emissionPayload = this.props.setting.emissionValueUpdate(emissionPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
for await (const event of this.props.setting.emitEvent) {
|
||||||
|
window.app.eventBus.emit(event, emissionPayload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.setting.noUpdate) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset debounced value
|
||||||
|
if (this.props.setting.debounced) {
|
||||||
|
await this.setState({
|
||||||
|
debouncedValue: null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.componentRef.current) {
|
||||||
|
if (typeof this.componentRef.current.onDebounceSave === "function") {
|
||||||
|
await this.componentRef.current.onDebounceSave(updateValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// finaly update value
|
||||||
|
await this.setState({
|
||||||
|
value: updateValue
|
||||||
|
})
|
||||||
|
|
||||||
|
return updateValue
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdateItem = async (updateValue) => {
|
||||||
|
this.setState({
|
||||||
|
value: updateValue
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.props.setting.debounced) {
|
||||||
|
return await this.setState({
|
||||||
|
debouncedValue: updateValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.dispatchUpdate(updateValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkDependsValidation = () => {
|
||||||
|
return !Boolean(Object.keys(this.props.setting.dependsOn).every((key) => {
|
||||||
|
const storagedValue = window.app.cores.settings.get(key)
|
||||||
|
|
||||||
|
console.debug(`Checking validation for [${key}] with now value [${storagedValue}]`)
|
||||||
|
|
||||||
|
if (typeof this.props.setting.dependsOn[key] === "function") {
|
||||||
|
return this.props.setting.dependsOn[key](storagedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return storagedValue === this.props.setting.dependsOn[key]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.props.setting) {
|
||||||
|
console.error(`Item [${this.props.setting.id}] has no an setting!`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.props.setting.component) {
|
||||||
|
console.error(`Item [${this.props.setting.id}] has no an setting component!`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalProps = {
|
||||||
|
...this.state.componentProps,
|
||||||
|
...this.props.setting.props,
|
||||||
|
|
||||||
|
ctx: {
|
||||||
|
updateCurrentValue: (updateValue) => this.setState({
|
||||||
|
value: updateValue
|
||||||
|
}),
|
||||||
|
getCurrentValue: () => this.state.value,
|
||||||
|
currentValue: this.state.value,
|
||||||
|
dispatchUpdate: this.dispatchUpdate,
|
||||||
|
onUpdateItem: this.onUpdateItem,
|
||||||
|
},
|
||||||
|
ref: this.componentRef,
|
||||||
|
|
||||||
|
...this.generateInhertedProps(),
|
||||||
|
|
||||||
|
// set values
|
||||||
|
checked: this.state.value,
|
||||||
|
value: this.state.value,
|
||||||
|
|
||||||
|
size: app.isMobile ? "large" : "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.setting.children) {
|
||||||
|
finalProps.children = this.props.setting.children
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app.isMobile) {
|
||||||
|
finalProps.size = "large"
|
||||||
|
}
|
||||||
|
|
||||||
|
const Component = SettingsComponents[String(this.props.setting.component).toLowerCase()]?.component ?? this.props.setting.component
|
||||||
|
|
||||||
|
return <div className="setting_item" id={this.props.setting.id} key={this.props.setting.id}>
|
||||||
|
<div className="setting_item_header">
|
||||||
|
<div className="setting_item_info">
|
||||||
|
<div className="setting_item_header_title">
|
||||||
|
<h1>
|
||||||
|
{
|
||||||
|
createIconRender(this.props.setting.icon)
|
||||||
|
}
|
||||||
|
<Translation>
|
||||||
|
{(t) => t(this.props.setting.title ?? this.props.setting.id)}
|
||||||
|
</Translation>
|
||||||
|
</h1>
|
||||||
|
{this.props.setting.experimental && <antd.Tag> Experimental </antd.Tag>}
|
||||||
|
</div>
|
||||||
|
<div className="setting_item_header_description">
|
||||||
|
<p>
|
||||||
|
<Translation>
|
||||||
|
{(t) => t(this.props.setting.description)}
|
||||||
|
</Translation>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
this.props.setting.extraActions && <div className="setting_item_header_actions">
|
||||||
|
{
|
||||||
|
this.props.setting.extraActions.map((action, index) => {
|
||||||
|
if (typeof action === "function") {
|
||||||
|
return React.createElement(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOnClick = () => {
|
||||||
|
if (action.onClick) {
|
||||||
|
action.onClick(finalProps.ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <antd.Button
|
||||||
|
key={action.id}
|
||||||
|
id={action.id}
|
||||||
|
onClick={handleOnClick}
|
||||||
|
icon={action.icon && createIconRender(action.icon)}
|
||||||
|
type={action.type ?? "round"}
|
||||||
|
disabled={this.props.setting.disabled}
|
||||||
|
>
|
||||||
|
{action.title}
|
||||||
|
</antd.Button>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="setting_item_content">
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
!this.state.loading && React.createElement(Component, finalProps)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
this.state.loading && <antd.Spin />
|
||||||
|
}
|
||||||
|
{
|
||||||
|
this.state.debouncedValue && <antd.Button
|
||||||
|
type="round"
|
||||||
|
icon={<Icons.Save />}
|
||||||
|
onClick={async () => await this.dispatchUpdate(this.state.debouncedValue)}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</antd.Button>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
117
packages/app/src/pages/settings/components/SettingTab/index.jsx
Normal file
117
packages/app/src/pages/settings/components/SettingTab/index.jsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import React from "react"
|
||||||
|
import * as antd from "antd"
|
||||||
|
import { Translation } from "react-i18next"
|
||||||
|
|
||||||
|
import { Icons } from "components/Icons"
|
||||||
|
|
||||||
|
import {
|
||||||
|
composedTabs,
|
||||||
|
composeGroupsFromSettingsTab,
|
||||||
|
} from "schemas/settings"
|
||||||
|
|
||||||
|
import groupsDecorators from "schemas/settingsGroupsDecorators"
|
||||||
|
|
||||||
|
import SettingItemComponent from "../SettingItemComponent"
|
||||||
|
|
||||||
|
export default class SettingTab extends React.Component {
|
||||||
|
state = {
|
||||||
|
loading: true,
|
||||||
|
processedCtx: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
tab = composedTabs[this.props.activeKey]
|
||||||
|
|
||||||
|
processCtx = async () => {
|
||||||
|
if (typeof this.tab.ctxData === "function") {
|
||||||
|
this.setState({ loading: true })
|
||||||
|
|
||||||
|
const resultCtx = await this.tab.ctxData()
|
||||||
|
|
||||||
|
console.log(resultCtx)
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
processedCtx: resultCtx
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if props.activeKey change
|
||||||
|
componentDidUpdate = async (prevProps) => {
|
||||||
|
if (prevProps.activeKey !== this.props.activeKey) {
|
||||||
|
this.tab = composedTabs[this.props.activeKey]
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
loading: !!this.tab.ctxData,
|
||||||
|
processedCtx: {}
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.processCtx()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount = async () => {
|
||||||
|
this.setState({
|
||||||
|
loading: !!this.tab.ctxData,
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.processCtx()
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
loading: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.loading) {
|
||||||
|
return <antd.Skeleton active />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.tab.render) {
|
||||||
|
return React.createElement(this.tab.render, {
|
||||||
|
ctx: this.state.processedCtx
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.withGroups) {
|
||||||
|
const group = composeGroupsFromSettingsTab(this.tab.settings)
|
||||||
|
|
||||||
|
return Object.entries(group).map(([groupKey, settings], index) => {
|
||||||
|
const fromDecoratorIcon = groupsDecorators[groupKey]?.icon
|
||||||
|
const fromDecoratorTitle = groupsDecorators[groupKey]?.title
|
||||||
|
|
||||||
|
return <div id={groupKey} key={index} className="settings_content_group">
|
||||||
|
<div className="settings_content_group_header">
|
||||||
|
<h1>
|
||||||
|
{
|
||||||
|
fromDecoratorIcon ? React.createElement(Icons[fromDecoratorIcon]) : null
|
||||||
|
}
|
||||||
|
<Translation>
|
||||||
|
{
|
||||||
|
t => t(fromDecoratorTitle ?? groupKey)
|
||||||
|
}
|
||||||
|
</Translation>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="settings_list">
|
||||||
|
{
|
||||||
|
settings.map((setting) => <SettingItemComponent
|
||||||
|
setting={setting}
|
||||||
|
ctx={this.state.processedCtx}
|
||||||
|
/>)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.tab.settings.map((setting, index) => {
|
||||||
|
return <SettingItemComponent
|
||||||
|
key={index}
|
||||||
|
setting={setting}
|
||||||
|
ctx={this.state.processedCtx}
|
||||||
|
/>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
564
packages/app/src/pages/settings/index.jsx
Executable file → Normal file
564
packages/app/src/pages/settings/index.jsx
Executable file → Normal file
@ -6,34 +6,35 @@ import classnames from "classnames"
|
|||||||
import config from "config"
|
import config from "config"
|
||||||
import useUrlQueryActiveKey from "hooks/useUrlQueryActiveKey"
|
import useUrlQueryActiveKey from "hooks/useUrlQueryActiveKey"
|
||||||
|
|
||||||
import AuthModel from "models/auth"
|
|
||||||
|
|
||||||
import { Icons, createIconRender } from "components/Icons"
|
import { Icons, createIconRender } from "components/Icons"
|
||||||
|
|
||||||
import getSettingsList from "schemas/settings"
|
import {
|
||||||
|
composedSettingsByGroups as settings
|
||||||
|
} from "schemas/settings"
|
||||||
|
|
||||||
import menuGroupsDecorators from "schemas/settingsMenuGroupsDecorators"
|
import menuGroupsDecorators from "schemas/settingsMenuGroupsDecorators"
|
||||||
import groupsDecorators from "schemas/settingsGroupsDecorators"
|
|
||||||
|
import SettingTab from "./components/SettingTab"
|
||||||
|
|
||||||
import "./index.less"
|
import "./index.less"
|
||||||
|
|
||||||
const SettingsList = await getSettingsList()
|
|
||||||
|
|
||||||
const extraMenuItems = [
|
const extraMenuItems = [
|
||||||
{
|
{
|
||||||
id: "donate",
|
key: "donate",
|
||||||
label: "Support us",
|
label: <div style={{
|
||||||
icon: "Heart",
|
color: "#f72585"
|
||||||
props: {
|
}}>
|
||||||
style: {
|
{createIconRender("Heart")}
|
||||||
color: "#f72585"
|
Support us
|
||||||
}
|
</div>,
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "logout",
|
key: "logout",
|
||||||
label: "Logout",
|
label: <div>
|
||||||
icon: "MdOutlineLogout",
|
{createIconRender("MdOutlineLogout")}
|
||||||
danger: true
|
Logout
|
||||||
|
</div>,
|
||||||
|
danger: true,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -44,493 +45,16 @@ const menuEvents = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"logout": () => {
|
"logout": () => {
|
||||||
antd.Modal.confirm({
|
app.eventBus.emit("app.logout_request")
|
||||||
title: "Logout",
|
|
||||||
content: "Are you sure you want to logout?",
|
|
||||||
onOk: () => {
|
|
||||||
AuthModel.logout()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ItemTypes = {
|
|
||||||
Button: antd.Button,
|
|
||||||
Switch: antd.Switch,
|
|
||||||
Slider: antd.Slider,
|
|
||||||
Checkbox: antd.Checkbox,
|
|
||||||
Input: antd.Input,
|
|
||||||
TextArea: antd.Input.TextArea,
|
|
||||||
InputNumber: antd.InputNumber,
|
|
||||||
Select: antd.Select,
|
|
||||||
SliderColorPicker: SliderPicker,
|
|
||||||
}
|
|
||||||
|
|
||||||
const SettingItem = (props) => {
|
|
||||||
let { item } = props
|
|
||||||
|
|
||||||
const [loading, setLoading] = React.useState(true)
|
|
||||||
const [value, setValue] = React.useState(null)
|
|
||||||
const [delayedValue, setDelayedValue] = React.useState(null)
|
|
||||||
const [disabled, setDisabled] = React.useState(false)
|
|
||||||
const componentRef = React.useRef(null)
|
|
||||||
|
|
||||||
let SettingComponent = item.component
|
|
||||||
|
|
||||||
if (!SettingComponent) {
|
|
||||||
console.error(`Item [${item.id}] has no an component!`)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof item.props === "undefined") {
|
|
||||||
item.props = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const dispatchUpdate = async (updateValue) => {
|
|
||||||
if (typeof item.onUpdate === "function") {
|
|
||||||
try {
|
|
||||||
const result = await item.onUpdate(updateValue)
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
updateValue = result
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
if (error.response.data.error) {
|
|
||||||
app.message.error(error.response.data.error)
|
|
||||||
} else {
|
|
||||||
app.message.error(error.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const storagedValue = await window.app.cores.settings.get(item.id)
|
|
||||||
|
|
||||||
if (typeof updateValue === "undefined") {
|
|
||||||
updateValue = !storagedValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.storaged) {
|
|
||||||
await window.app.cores.settings.set(item.id, updateValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.storaged && typeof item.beforeSave === "function") {
|
|
||||||
await item.beforeSave(updateValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof item.emitEvent !== "undefined") {
|
|
||||||
let emissionPayload = updateValue
|
|
||||||
|
|
||||||
if (typeof item.emissionValueUpdate === "function") {
|
|
||||||
emissionPayload = item.emissionValueUpdate(emissionPayload)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(item.emitEvent)) {
|
|
||||||
window.app.eventBus.emit(...item.emitEvent, emissionPayload)
|
|
||||||
} else if (typeof item.emitEvent === "string") {
|
|
||||||
window.app.eventBus.emit(item.emitEvent, emissionPayload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.noUpdate) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.debounced) {
|
|
||||||
setDelayedValue(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (componentRef.current) {
|
|
||||||
if (typeof componentRef.current.onDebounceSave === "function") {
|
|
||||||
await componentRef.current.onDebounceSave(updateValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setValue(updateValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onUpdateItem = async (updateValue) => {
|
|
||||||
setValue(updateValue)
|
|
||||||
|
|
||||||
if (!item.debounced) {
|
|
||||||
await dispatchUpdate(updateValue)
|
|
||||||
} else {
|
|
||||||
setDelayedValue(updateValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkDependsValidation = () => {
|
|
||||||
return !Boolean(Object.keys(item.dependsOn).every((key) => {
|
|
||||||
const storagedValue = window.app.cores.settings.get(key)
|
|
||||||
|
|
||||||
console.debug(`Checking validation for [${key}] with now value [${storagedValue}]`)
|
|
||||||
|
|
||||||
if (typeof item.dependsOn[key] === "function") {
|
|
||||||
return item.dependsOn[key](storagedValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
return storagedValue === item.dependsOn[key]
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const settingInitialization = async () => {
|
|
||||||
if (item.storaged) {
|
|
||||||
const storagedValue = window.app.cores.settings.get(item.id)
|
|
||||||
setValue(storagedValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof item.defaultValue === "function") {
|
|
||||||
setLoading(true)
|
|
||||||
|
|
||||||
setValue(await item.defaultValue(props.ctx))
|
|
||||||
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.disabled === true) {
|
|
||||||
setDisabled(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof item.dependsOn === "object") {
|
|
||||||
// create a event handler to watch changes
|
|
||||||
Object.keys(item.dependsOn).forEach((key) => {
|
|
||||||
window.app.eventBus.on(`setting.update.${key}`, () => {
|
|
||||||
setDisabled(checkDependsValidation())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// by default check depends validation
|
|
||||||
setDisabled(checkDependsValidation())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof item.listenUpdateValue === "string") {
|
|
||||||
window.app.eventBus.on(`setting.update.${item.listenUpdateValue}`, (value) => setValue(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.reloadValueOnUpdateEvent) {
|
|
||||||
window.app.eventBus.on(item.reloadValueOnUpdateEvent, () => {
|
|
||||||
console.log(`Reloading value for item [${item.id}]`)
|
|
||||||
settingInitialization()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
settingInitialization()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (typeof item.dependsOn === "object") {
|
|
||||||
for (let key in item.dependsOn) {
|
|
||||||
window.app.eventBus.off(`setting.update.${key}`, onUpdateItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
if (typeof SettingComponent === "string") {
|
|
||||||
if (typeof ItemTypes[SettingComponent] === "undefined") {
|
|
||||||
console.error(`Item [${item.id}] has an invalid component: ${item.component}`)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (SettingComponent.toLowerCase()) {
|
|
||||||
case "slidercolorpicker": {
|
|
||||||
item.props.onChange = (color) => {
|
|
||||||
item.props.color = color.hex
|
|
||||||
}
|
|
||||||
item.props.onChangeComplete = (color) => {
|
|
||||||
onUpdateItem(color.hex)
|
|
||||||
}
|
|
||||||
|
|
||||||
item.props.color = value
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case "textarea": {
|
|
||||||
item.props.defaultValue = value
|
|
||||||
item.props.onPressEnter = (event) => dispatchUpdate(event.target.value)
|
|
||||||
item.props.onChange = (event) => onUpdateItem(event.target.value)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case "input": {
|
|
||||||
item.props.defaultValue = value
|
|
||||||
item.props.onPressEnter = (event) => dispatchUpdate(event.target.value)
|
|
||||||
item.props.onChange = (event) => onUpdateItem(event.target.value)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case "switch": {
|
|
||||||
item.props.checked = value
|
|
||||||
item.props.onClick = (event) => onUpdateItem(event)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case "select": {
|
|
||||||
item.props.onChange = (value) => onUpdateItem(value)
|
|
||||||
item.props.defaultValue = value
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case "slider": {
|
|
||||||
item.props.defaultValue = value
|
|
||||||
item.props.onAfterChange = (value) => onUpdateItem(value)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
if (!item.props.children) {
|
|
||||||
item.props.children = item.title ?? item.id
|
|
||||||
}
|
|
||||||
|
|
||||||
item.props.value = item.defaultValue
|
|
||||||
item.props.onClick = (event) => onUpdateItem(event)
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// override with default item component
|
|
||||||
SettingComponent = ItemTypes[SettingComponent]
|
|
||||||
}
|
|
||||||
|
|
||||||
item.props["disabled"] = disabled
|
|
||||||
|
|
||||||
const elementsCtx = {
|
|
||||||
updateCurrentValue: (value) => setValue(value),
|
|
||||||
currentValue: value,
|
|
||||||
dispatchUpdate,
|
|
||||||
onUpdateItem,
|
|
||||||
...props.ctx,
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div key={item.id} className="settings_content_group_item">
|
|
||||||
<div className="settings_content_group_item_header">
|
|
||||||
<div className="settings_content_group_item_header_title">
|
|
||||||
<div>
|
|
||||||
<h4>
|
|
||||||
{Icons[item.icon] ? React.createElement(Icons[item.icon]) : null}
|
|
||||||
<Translation>{
|
|
||||||
t => t(item.title ?? item.id)
|
|
||||||
}</Translation>
|
|
||||||
</h4>
|
|
||||||
<p> <Translation>{
|
|
||||||
t => t(item.description)
|
|
||||||
}</Translation></p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{item.experimental && <antd.Tag> Experimental </antd.Tag>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{item.extraActions &&
|
|
||||||
<div className="settings_content_group_item_header_actions">
|
|
||||||
{item.extraActions.map((action, index) => {
|
|
||||||
if (typeof action === "function") {
|
|
||||||
return React.createElement(action, {
|
|
||||||
ctx: elementsCtx,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleOnClick = () => {
|
|
||||||
if (action.onClick) {
|
|
||||||
action.onClick(elementsCtx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <antd.Button
|
|
||||||
key={action.id}
|
|
||||||
id={action.id}
|
|
||||||
onClick={handleOnClick}
|
|
||||||
icon={action.icon && createIconRender(action.icon)}
|
|
||||||
type={action.type ?? "round"}
|
|
||||||
disabled={item.props.disabled}
|
|
||||||
>
|
|
||||||
{action.title}
|
|
||||||
</antd.Button>
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div className="settings_content_group_item_component">
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
loading
|
|
||||||
? <div> Loading... </div>
|
|
||||||
: React.createElement(SettingComponent, {
|
|
||||||
...item.props,
|
|
||||||
ctx: elementsCtx,
|
|
||||||
ref: componentRef,
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
delayedValue && <div>
|
|
||||||
<antd.Button
|
|
||||||
type="round"
|
|
||||||
icon={<Icons.Save />}
|
|
||||||
onClick={async () => await dispatchUpdate(value)}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</antd.Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
const SettingGroup = React.memo((props) => {
|
|
||||||
const {
|
|
||||||
ctx,
|
|
||||||
groupKey,
|
|
||||||
settings,
|
|
||||||
loading,
|
|
||||||
disabled
|
|
||||||
} = props
|
|
||||||
|
|
||||||
const fromDecoratorIcon = groupsDecorators[groupKey]?.icon
|
|
||||||
const fromDecoratorTitle = groupsDecorators[groupKey]?.title
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <antd.Skeleton active />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (disabled) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div index={groupKey} key={groupKey} className="settings_content_group">
|
|
||||||
<div className="settings_content_group_header">
|
|
||||||
<h1>
|
|
||||||
{
|
|
||||||
fromDecoratorIcon ? React.createElement(Icons[fromDecoratorIcon]) : null
|
|
||||||
}
|
|
||||||
<Translation>
|
|
||||||
{
|
|
||||||
t => t(fromDecoratorTitle ?? groupKey)
|
|
||||||
}
|
|
||||||
</Translation>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div className="settings_content_group_settings">
|
|
||||||
{
|
|
||||||
settings.map((item) => <SettingItem
|
|
||||||
item={item}
|
|
||||||
ctx={ctx}
|
|
||||||
/>)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
|
|
||||||
const SettingTab = (props) => {
|
|
||||||
const [groups, setGroups] = React.useState({})
|
|
||||||
const [loading, setLoading] = React.useState(true)
|
|
||||||
const [ctxData, setCtxData] = React.useState({})
|
|
||||||
|
|
||||||
const processCtx = async () => {
|
|
||||||
setLoading(true)
|
|
||||||
|
|
||||||
if (typeof props.tab.ctxData === "function") {
|
|
||||||
const resultCtx = await props.tab.ctxData()
|
|
||||||
|
|
||||||
setCtxData(resultCtx)
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!Array.isArray(props.tab.settings)) {
|
|
||||||
console.error("Cannot generate settings from non-array")
|
|
||||||
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
let groupsSettings = {}
|
|
||||||
|
|
||||||
props.tab.settings.forEach((item) => {
|
|
||||||
if (!groupsSettings[item.group]) {
|
|
||||||
groupsSettings[item.group] = []
|
|
||||||
}
|
|
||||||
|
|
||||||
groupsSettings[item.group].push(item)
|
|
||||||
})
|
|
||||||
|
|
||||||
setGroups(groupsSettings)
|
|
||||||
|
|
||||||
processCtx()
|
|
||||||
}, [props.tab])
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <antd.Skeleton active />
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.keys(groups).map((groupKey) => {
|
|
||||||
return <SettingGroup
|
|
||||||
groupKey={groupKey}
|
|
||||||
settings={groups[groupKey]}
|
|
||||||
loading={loading}
|
|
||||||
ctx={ctxData}
|
|
||||||
/>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateMenuItems = () => {
|
const generateMenuItems = () => {
|
||||||
const groups = {}
|
return settings.map((entry, index) => {
|
||||||
|
const children = entry.groupModule.map((item) => {
|
||||||
Object.keys(SettingsList).forEach((tabKey) => {
|
|
||||||
const tab = SettingsList[tabKey]
|
|
||||||
|
|
||||||
if (!tab.group) {
|
|
||||||
tab.group = "Others"
|
|
||||||
}
|
|
||||||
|
|
||||||
groups[tab.group] = groups[tab.group] ?? []
|
|
||||||
|
|
||||||
groups[tab.group].push(tab)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (typeof groups["bottom"] === undefined) {
|
|
||||||
groups["bottom"] = []
|
|
||||||
}
|
|
||||||
|
|
||||||
// add extra menu items
|
|
||||||
extraMenuItems.forEach((item) => {
|
|
||||||
groups["bottom"].push(item)
|
|
||||||
})
|
|
||||||
|
|
||||||
let groupsKeys = Object.keys(groups)
|
|
||||||
|
|
||||||
// make "bottom" group last
|
|
||||||
groupsKeys = groupsKeys.sort((a, b) => {
|
|
||||||
if (a === "bottom") {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (b === "bottom") {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
|
|
||||||
return groupsKeys.map((groupKey, index) => {
|
|
||||||
const ordererItems = groups[groupKey].sort((a, b) => {
|
|
||||||
if (typeof a.order === "undefined") {
|
|
||||||
a.order = groups[groupKey].indexOf(a)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof b.order === "undefined") {
|
|
||||||
b.order = groups[groupKey].indexOf(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if value is close to 0, more to the top
|
|
||||||
return a.order - b.order
|
|
||||||
})
|
|
||||||
|
|
||||||
const children = ordererItems.map((item) => {
|
|
||||||
return {
|
return {
|
||||||
key: item.id,
|
key: item.id,
|
||||||
|
type: "item",
|
||||||
label: <div {...item.props}>
|
label: <div {...item.props}>
|
||||||
{createIconRender(item.icon ?? "Settings")}
|
{createIconRender(item.icon ?? "Settings")}
|
||||||
{item.label}
|
{item.label}
|
||||||
@ -541,38 +65,37 @@ const generateMenuItems = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (index !== groupsKeys.length - 1) {
|
if (index !== settings.length - 1) {
|
||||||
children.push({
|
children.push({
|
||||||
type: "divider",
|
type: "divider",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: groupKey,
|
key: entry.group,
|
||||||
label: groupKey === "bottom" ? null : <>
|
type: "group",
|
||||||
|
children: children,
|
||||||
|
label: entry.group === "bottom" ? null : <>
|
||||||
{
|
{
|
||||||
menuGroupsDecorators[groupKey]?.icon && createIconRender(menuGroupsDecorators[groupKey]?.icon ?? "Settings")
|
menuGroupsDecorators[entry.group]?.icon && createIconRender(menuGroupsDecorators[groupKey]?.icon ?? "Settings")
|
||||||
}
|
}
|
||||||
<Translation>
|
<Translation>
|
||||||
{
|
{
|
||||||
t => t(menuGroupsDecorators[groupKey]?.label ?? groupKey)
|
t => t(menuGroupsDecorators[entry.group]?.label ?? entry.group)
|
||||||
}
|
}
|
||||||
</Translation>
|
</Translation>
|
||||||
</>,
|
</>
|
||||||
type: "group",
|
|
||||||
children: children,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const [activeKey, setActiveKey] = useUrlQueryActiveKey({
|
const [activeKey, setActiveKey] = useUrlQueryActiveKey({
|
||||||
defaultKey: "general",
|
defaultKey: "general",
|
||||||
queryKey: "tab"
|
queryKey: "tab"
|
||||||
})
|
})
|
||||||
|
|
||||||
const [menuItems, setMenuItems] = React.useState([])
|
|
||||||
|
|
||||||
const onChangeTab = (event) => {
|
const onChangeTab = (event) => {
|
||||||
if (typeof menuEvents[event.key] === "function") {
|
if (typeof menuEvents[event.key] === "function") {
|
||||||
return menuEvents[event.key]()
|
return menuEvents[event.key]()
|
||||||
@ -583,16 +106,19 @@ export default () => {
|
|||||||
setActiveKey(event.key)
|
setActiveKey(event.key)
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
const menuItems = React.useMemo(() => {
|
||||||
setMenuItems(generateMenuItems())
|
const items = generateMenuItems()
|
||||||
|
|
||||||
|
extraMenuItems.forEach((item) => {
|
||||||
|
items[settings.length - 1].children.push(item)
|
||||||
|
})
|
||||||
|
|
||||||
|
return items
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <div
|
return <div
|
||||||
className={classnames(
|
className={classnames(
|
||||||
"settings_wrapper",
|
"settings_wrapper",
|
||||||
{
|
|
||||||
["mobile"]: app.isMobile,
|
|
||||||
}
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="settings_menu">
|
<div className="settings_menu">
|
||||||
@ -605,12 +131,10 @@ export default () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="settings_content">
|
<div className="settings_content">
|
||||||
{
|
<SettingTab
|
||||||
SettingsList[activeKey] &&
|
activeKey={activeKey}
|
||||||
React.createElement(SettingsList[activeKey].render ?? SettingTab, {
|
withGroups
|
||||||
tab: SettingsList[activeKey],
|
/>
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
|
|
||||||
.settings_menu {
|
.settings_menu {
|
||||||
@ -21,8 +23,8 @@
|
|||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
width: 30%;
|
width: 250px;
|
||||||
max-width: 300px;
|
min-width: 250px;
|
||||||
|
|
||||||
padding: 0 30px;
|
padding: 0 30px;
|
||||||
|
|
||||||
@ -41,8 +43,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
width: 100%;
|
width: 700px;
|
||||||
max-width: 700px;
|
|
||||||
|
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
|
||||||
@ -62,13 +63,10 @@
|
|||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
|
||||||
.settings_content_group_header {
|
.settings_content_group_header {
|
||||||
position: absolute;
|
position: relative;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
h3,
|
h3,
|
||||||
@ -78,65 +76,68 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--background-color-contrast);
|
color: var(--background-color-contrast);
|
||||||
}
|
}
|
||||||
|
|
||||||
//-webkit-box-shadow: @card-shadow;
|
|
||||||
//-moz-box-shadow: @card-shadow;
|
|
||||||
//box-shadow: @card-shadow;
|
|
||||||
|
|
||||||
padding: 20px;
|
|
||||||
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings_content_group_settings {
|
.settings_list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
margin-top: 50px;
|
|
||||||
|
|
||||||
gap: 30px;
|
gap: 30px;
|
||||||
|
|
||||||
.settings_content_group_item {
|
width: 100%;
|
||||||
|
|
||||||
|
.setting_item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
|
|
||||||
.settings_content_group_item_header {
|
.setting_item_header {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.settings_content_group_item_header_title {
|
.setting_item_info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
color: var(--background-color-contrast);
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.setting_item_header_title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6 {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--background-color-contrast);
|
color: var(--background-color-contrast);
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--background-color-contrast);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
.setting_item_header_description {
|
||||||
font-size: 11px;
|
p {
|
||||||
color: var(--background-color-contrast);
|
color: var(--background-color-contrast);
|
||||||
margin: 0;
|
font-size: 0.7rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
>div {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings_content_group_item_header_actions {
|
.setting_item_header_actions {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
@ -155,16 +156,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings_content_group_item_component {
|
.setting_item_content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
--ignore-dragger: true;
|
--ignore-dragger: true;
|
||||||
padding: 6px 20px;
|
padding: 6px 20px;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
color: var(--background-color-contrast);
|
color: var(--background-color-contrast);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: min-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-btn.ant-btn-icon-only {
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
135
packages/app/src/pages/settings/index.mobile.jsx
Normal file
135
packages/app/src/pages/settings/index.mobile.jsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import React from "react"
|
||||||
|
import * as antd from "antd"
|
||||||
|
|
||||||
|
import { Translation } from "react-i18next"
|
||||||
|
import useUrlQueryActiveKey from "hooks/useUrlQueryActiveKey"
|
||||||
|
|
||||||
|
import { Icons, createIconRender } from "components/Icons"
|
||||||
|
|
||||||
|
import {
|
||||||
|
composedSettingsByGroups as settingsGroups,
|
||||||
|
composedTabs,
|
||||||
|
} from "schemas/settings"
|
||||||
|
|
||||||
|
import menuGroupsDecorators from "schemas/settingsMenuGroupsDecorators"
|
||||||
|
import SettingTab from "./components/SettingTab"
|
||||||
|
|
||||||
|
import "./index.mobile.less"
|
||||||
|
|
||||||
|
const SettingsHeader = ({
|
||||||
|
activeKey,
|
||||||
|
back = () => { }
|
||||||
|
} = {}) => {
|
||||||
|
if (activeKey) {
|
||||||
|
const currentTab = composedTabs[activeKey]
|
||||||
|
|
||||||
|
return <div className="__mobile__settings_header nav">
|
||||||
|
<antd.Button
|
||||||
|
icon={<Icons.MdChevronLeft />}
|
||||||
|
onClick={back}
|
||||||
|
size="large"
|
||||||
|
type="ghost"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h1>
|
||||||
|
{
|
||||||
|
createIconRender(currentTab?.icon)
|
||||||
|
}
|
||||||
|
<Translation>
|
||||||
|
{(t) => t(currentTab?.label ?? activeKey)}
|
||||||
|
</Translation>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="__mobile__settings_header">
|
||||||
|
<h1>
|
||||||
|
{
|
||||||
|
createIconRender("Settings")
|
||||||
|
}
|
||||||
|
<Translation>
|
||||||
|
{(t) => t("Settings")}
|
||||||
|
</Translation>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
let lastKey = null
|
||||||
|
|
||||||
|
const [activeKey, setActiveKey] = useUrlQueryActiveKey({
|
||||||
|
queryKey: "tab",
|
||||||
|
defaultKey: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleTabChange = (key) => {
|
||||||
|
// star page transition using new chrome transition api
|
||||||
|
if (document.startViewTransition) {
|
||||||
|
return document.startViewTransition(() => {
|
||||||
|
changeTab(key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return changeTab(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
handleTabChange(lastKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeTab = (key) => {
|
||||||
|
lastKey = key
|
||||||
|
setActiveKey(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="__mobile__settings">
|
||||||
|
<SettingsHeader
|
||||||
|
activeKey={activeKey}
|
||||||
|
back={goBack}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="settings_list">
|
||||||
|
{
|
||||||
|
!activeKey && settingsGroups.map((entry) => {
|
||||||
|
const groupDecorator = menuGroupsDecorators[entry.group]
|
||||||
|
|
||||||
|
return <div className="settings_list_group">
|
||||||
|
<span >
|
||||||
|
<Translation>
|
||||||
|
{(t) => t(groupDecorator?.label ?? entry.group)}
|
||||||
|
</Translation>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="settings_list_group_items">
|
||||||
|
{
|
||||||
|
entry.groupModule.map((settingsModule, index) => {
|
||||||
|
return <antd.Button
|
||||||
|
size="large"
|
||||||
|
key={settingsModule.id}
|
||||||
|
id={settingsModule.id}
|
||||||
|
icon={createIconRender(settingsModule.icon)}
|
||||||
|
onClick={() => {
|
||||||
|
handleTabChange(settingsModule.id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Translation>
|
||||||
|
{(t) => t(settingsModule.label)}
|
||||||
|
</Translation>
|
||||||
|
</antd.Button>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
activeKey && <div className="settings_list_render">
|
||||||
|
<SettingTab
|
||||||
|
activeKey={activeKey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
156
packages/app/src/pages/settings/index.mobile.less
Normal file
156
packages/app/src/pages/settings/index.mobile.less
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
@top_nav_height: 52px;
|
||||||
|
|
||||||
|
.__mobile__settings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: var(--text-color);
|
||||||
|
|
||||||
|
gap: 30px;
|
||||||
|
|
||||||
|
.__mobile__settings_header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 200;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: @top_nav_height;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
padding: 5px 20px;
|
||||||
|
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
background-color: var(--background-color-accent);
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
view-transition-name: main-header-text;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--colorPrimary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
font-size: 2rem;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings_list {
|
||||||
|
view-transition-name: settings-list;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
color: var(--text-color);
|
||||||
|
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.settings_list_group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings_list_group_items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
padding: 10px 15px;
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
gap: 10vw;
|
||||||
|
|
||||||
|
padding: 30px 20px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings_list_render {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.setting_item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.setting_item_header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
.setting_item_info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
.setting_item_header_title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting_item_header_description {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting_item_content {
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user