diff --git a/packages/app/src/pages/settings/components/SettingItemComponent/index.jsx b/packages/app/src/pages/settings/components/SettingItemComponent/index.jsx
new file mode 100644
index 00000000..db5f8ed6
--- /dev/null
+++ b/packages/app/src/pages/settings/components/SettingItemComponent/index.jsx
@@ -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
+
+
+
+
+ {
+ createIconRender(this.props.setting.icon)
+ }
+
+ {(t) => t(this.props.setting.title ?? this.props.setting.id)}
+
+
+ {this.props.setting.experimental &&
Experimental }
+
+
+
+
+ {(t) => t(this.props.setting.description)}
+
+
+
+
+
+ {
+ this.props.setting.extraActions &&
+ {
+ 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
+ {action.title}
+
+ })
+ }
+
+ }
+
+
+
+ <>
+ {
+ !this.state.loading && React.createElement(Component, finalProps)
+ }
+ {
+ this.state.loading &&
+ }
+ {
+ this.state.debouncedValue &&
}
+ onClick={async () => await this.dispatchUpdate(this.state.debouncedValue)}
+ >
+ Save
+
+ }
+ >
+
+
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/pages/settings/components/SettingTab/index.jsx b/packages/app/src/pages/settings/components/SettingTab/index.jsx
new file mode 100644
index 00000000..96067104
--- /dev/null
+++ b/packages/app/src/pages/settings/components/SettingTab/index.jsx
@@ -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
+ }
+
+ 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
+
+
+ {
+ fromDecoratorIcon ? React.createElement(Icons[fromDecoratorIcon]) : null
+ }
+
+ {
+ t => t(fromDecoratorTitle ?? groupKey)
+ }
+
+
+
+
+
+ {
+ settings.map((setting) => )
+ }
+
+
+ })
+ }
+
+ return this.tab.settings.map((setting, index) => {
+ return
+ })
+ }
+}
\ No newline at end of file
diff --git a/packages/app/src/pages/settings/index.jsx b/packages/app/src/pages/settings/index.jsx
old mode 100755
new mode 100644
index 3f216d92..c56e7ff6
--- a/packages/app/src/pages/settings/index.jsx
+++ b/packages/app/src/pages/settings/index.jsx
@@ -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:
+ {createIconRender("Heart")}
+ Support us
+
,
},
{
- id: "logout",
- label: "Logout",
- icon: "MdOutlineLogout",
- danger: true
+ key: "logout",
+ label:
+ {createIconRender("MdOutlineLogout")}
+ Logout
+
,
+ 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
-
-
-
-
- {Icons[item.icon] ? React.createElement(Icons[item.icon]) : null}
- {
- t => t(item.title ?? item.id)
- }
-
-
{
- t => t(item.description)
- }
-
-
- {item.experimental &&
Experimental }
-
-
- {item.extraActions &&
-
- {item.extraActions.map((action, index) => {
- if (typeof action === "function") {
- return React.createElement(action, {
- ctx: elementsCtx,
- })
- }
-
- const handleOnClick = () => {
- if (action.onClick) {
- action.onClick(elementsCtx)
- }
- }
-
- return
- {action.title}
-
- })}
-
- }
-
-
-
- {
- loading
- ?
Loading...
- : React.createElement(SettingComponent, {
- ...item.props,
- ctx: elementsCtx,
- ref: componentRef,
- })}
-
-
- {
- delayedValue &&
-
}
- onClick={async () => await dispatchUpdate(value)}
- >
- Save
-
-
- }
-
-
-}
-
-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
- }
-
- if (disabled) {
- return null
- }
-
- return
-
-
- {
- fromDecoratorIcon ? React.createElement(Icons[fromDecoratorIcon]) : null
- }
-
- {
- t => t(fromDecoratorTitle ?? groupKey)
- }
-
-
-
-
- {
- settings.map((item) => )
- }
-
-
-})
-
-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
- }
-
- return Object.keys(groups).map((groupKey) => {
- return
- })
-}
-
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:
{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")
}
{
- t => t(menuGroupsDecorators[groupKey]?.label ?? groupKey)
+ t => t(menuGroupsDecorators[entry.group]?.label ?? entry.group)
}
- >,
- 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
@@ -605,12 +131,10 @@ export default () => {
- {
- SettingsList[activeKey] &&
- React.createElement(SettingsList[activeKey].render ?? SettingTab, {
- tab: SettingsList[activeKey],
- })
- }
+
}
\ No newline at end of file
diff --git a/packages/app/src/pages/settings/index.less b/packages/app/src/pages/settings/index.less
index 8c1eed05..52037fe5 100755
--- a/packages/app/src/pages/settings/index.less
+++ b/packages/app/src/pages/settings/index.less
@@ -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 {}
}
}
}
diff --git a/packages/app/src/pages/settings/index.mobile.jsx b/packages/app/src/pages/settings/index.mobile.jsx
new file mode 100644
index 00000000..b6f99c2b
--- /dev/null
+++ b/packages/app/src/pages/settings/index.mobile.jsx
@@ -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
+
}
+ onClick={back}
+ size="large"
+ type="ghost"
+ />
+
+
+ {
+ createIconRender(currentTab?.icon)
+ }
+
+ {(t) => t(currentTab?.label ?? activeKey)}
+
+
+
+ }
+
+ return
+
+ {
+ createIconRender("Settings")
+ }
+
+ {(t) => t("Settings")}
+
+
+
+}
+
+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
+
+
+
+ {
+ !activeKey && settingsGroups.map((entry) => {
+ const groupDecorator = menuGroupsDecorators[entry.group]
+
+ return
+
+
+ {(t) => t(groupDecorator?.label ?? entry.group)}
+
+
+
+
+ {
+ entry.groupModule.map((settingsModule, index) => {
+ return
{
+ handleTabChange(settingsModule.id)
+ }}
+ >
+
+ {(t) => t(settingsModule.label)}
+
+
+ })
+ }
+
+
+ })
+ }
+
+ {
+ activeKey &&
+
+
+ }
+
+
+}
diff --git a/packages/app/src/pages/settings/index.mobile.less b/packages/app/src/pages/settings/index.mobile.less
new file mode 100644
index 00000000..4f7a2e94
--- /dev/null
+++ b/packages/app/src/pages/settings/index.mobile.less
@@ -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;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file