mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-10 02:54:15 +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 useUrlQueryActiveKey from "hooks/useUrlQueryActiveKey"
|
||||
|
||||
import AuthModel from "models/auth"
|
||||
|
||||
import { Icons, createIconRender } from "components/Icons"
|
||||
|
||||
import getSettingsList from "schemas/settings"
|
||||
import {
|
||||
composedSettingsByGroups as settings
|
||||
} from "schemas/settings"
|
||||
|
||||
import menuGroupsDecorators from "schemas/settingsMenuGroupsDecorators"
|
||||
import groupsDecorators from "schemas/settingsGroupsDecorators"
|
||||
|
||||
import SettingTab from "./components/SettingTab"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const SettingsList = await getSettingsList()
|
||||
|
||||
const extraMenuItems = [
|
||||
{
|
||||
id: "donate",
|
||||
label: "Support us",
|
||||
icon: "Heart",
|
||||
props: {
|
||||
style: {
|
||||
color: "#f72585"
|
||||
}
|
||||
}
|
||||
key: "donate",
|
||||
label: <div style={{
|
||||
color: "#f72585"
|
||||
}}>
|
||||
{createIconRender("Heart")}
|
||||
Support us
|
||||
</div>,
|
||||
},
|
||||
{
|
||||
id: "logout",
|
||||
label: "Logout",
|
||||
icon: "MdOutlineLogout",
|
||||
danger: true
|
||||
key: "logout",
|
||||
label: <div>
|
||||
{createIconRender("MdOutlineLogout")}
|
||||
Logout
|
||||
</div>,
|
||||
danger: true,
|
||||
}
|
||||
]
|
||||
|
||||
@ -44,493 +45,16 @@ const menuEvents = {
|
||||
}
|
||||
},
|
||||
"logout": () => {
|
||||
antd.Modal.confirm({
|
||||
title: "Logout",
|
||||
content: "Are you sure you want to logout?",
|
||||
onOk: () => {
|
||||
AuthModel.logout()
|
||||
},
|
||||
})
|
||||
app.eventBus.emit("app.logout_request")
|
||||
}
|
||||
}
|
||||
|
||||
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 groups = {}
|
||||
|
||||
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 settings.map((entry, index) => {
|
||||
const children = entry.groupModule.map((item) => {
|
||||
return {
|
||||
key: item.id,
|
||||
type: "item",
|
||||
label: <div {...item.props}>
|
||||
{createIconRender(item.icon ?? "Settings")}
|
||||
{item.label}
|
||||
@ -541,38 +65,37 @@ const generateMenuItems = () => {
|
||||
}
|
||||
})
|
||||
|
||||
if (index !== groupsKeys.length - 1) {
|
||||
if (index !== settings.length - 1) {
|
||||
children.push({
|
||||
type: "divider",
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
key: groupKey,
|
||||
label: groupKey === "bottom" ? null : <>
|
||||
key: entry.group,
|
||||
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>
|
||||
{
|
||||
t => t(menuGroupsDecorators[groupKey]?.label ?? groupKey)
|
||||
t => t(menuGroupsDecorators[entry.group]?.label ?? entry.group)
|
||||
}
|
||||
</Translation>
|
||||
</>,
|
||||
type: "group",
|
||||
children: children,
|
||||
</>
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export default () => {
|
||||
const [activeKey, setActiveKey] = useUrlQueryActiveKey({
|
||||
defaultKey: "general",
|
||||
queryKey: "tab"
|
||||
})
|
||||
|
||||
const [menuItems, setMenuItems] = React.useState([])
|
||||
|
||||
const onChangeTab = (event) => {
|
||||
if (typeof menuEvents[event.key] === "function") {
|
||||
return menuEvents[event.key]()
|
||||
@ -583,16 +106,19 @@ export default () => {
|
||||
setActiveKey(event.key)
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
setMenuItems(generateMenuItems())
|
||||
const menuItems = React.useMemo(() => {
|
||||
const items = generateMenuItems()
|
||||
|
||||
extraMenuItems.forEach((item) => {
|
||||
items[settings.length - 1].children.push(item)
|
||||
})
|
||||
|
||||
return items
|
||||
}, [])
|
||||
|
||||
return <div
|
||||
className={classnames(
|
||||
"settings_wrapper",
|
||||
{
|
||||
["mobile"]: app.isMobile,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="settings_menu">
|
||||
@ -605,12 +131,10 @@ export default () => {
|
||||
</div>
|
||||
|
||||
<div className="settings_content">
|
||||
{
|
||||
SettingsList[activeKey] &&
|
||||
React.createElement(SettingsList[activeKey].render ?? SettingTab, {
|
||||
tab: SettingsList[activeKey],
|
||||
})
|
||||
}
|
||||
<SettingTab
|
||||
activeKey={activeKey}
|
||||
withGroups
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
@ -6,6 +6,8 @@
|
||||
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
|
||||
padding-bottom: 20px;
|
||||
|
||||
.settings_menu {
|
||||
@ -21,8 +23,8 @@
|
||||
|
||||
align-items: center;
|
||||
|
||||
width: 30%;
|
||||
max-width: 300px;
|
||||
width: 250px;
|
||||
min-width: 250px;
|
||||
|
||||
padding: 0 30px;
|
||||
|
||||
@ -41,8 +43,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
width: 700px;
|
||||
|
||||
gap: 20px;
|
||||
|
||||
@ -62,13 +63,10 @@
|
||||
gap: 20px;
|
||||
|
||||
.settings_content_group_header {
|
||||
position: absolute;
|
||||
position: relative;
|
||||
|
||||
width: 100%;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
@ -78,65 +76,68 @@
|
||||
margin: 0;
|
||||
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;
|
||||
flex-direction: column;
|
||||
|
||||
margin-top: 50px;
|
||||
|
||||
gap: 30px;
|
||||
|
||||
.settings_content_group_item {
|
||||
width: 100%;
|
||||
|
||||
.setting_item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
|
||||
padding: 0 20px;
|
||||
|
||||
.settings_content_group_item_header {
|
||||
.setting_item_header {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
|
||||
.settings_content_group_item_header_title {
|
||||
.setting_item_info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--background-color-contrast);
|
||||
flex-direction: column;
|
||||
|
||||
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);
|
||||
|
||||
h1 {
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
color: var(--background-color-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 11px;
|
||||
color: var(--background-color-contrast);
|
||||
margin: 0;
|
||||
.setting_item_header_description {
|
||||
p {
|
||||
color: var(--background-color-contrast);
|
||||
font-size: 0.7rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
>div {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.settings_content_group_item_header_actions {
|
||||
.setting_item_header_actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@ -155,16 +156,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
.settings_content_group_item_component {
|
||||
.setting_item_content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
--ignore-dragger: true;
|
||||
padding: 6px 20px;
|
||||
|
||||
width: 100%;
|
||||
|
||||
span {
|
||||
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