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