From bbdda50f09576ce701ca5b17ea60b62c6e57cf5e Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Mon, 13 Feb 2023 13:28:40 +0000 Subject: [PATCH] move settings to page --- .../app/src/components/Settings/index.jsx | 492 ----------------- .../app/src/components/Settings/index.less | 163 ------ packages/app/src/pages/settings/index.jsx | 506 ++++++++++++++++++ packages/app/src/pages/settings/index.less | 152 ++++++ 4 files changed, 658 insertions(+), 655 deletions(-) delete mode 100755 packages/app/src/components/Settings/index.jsx delete mode 100755 packages/app/src/components/Settings/index.less create mode 100644 packages/app/src/pages/settings/index.jsx create mode 100644 packages/app/src/pages/settings/index.less diff --git a/packages/app/src/components/Settings/index.jsx b/packages/app/src/components/Settings/index.jsx deleted file mode 100755 index ab87bd33..00000000 --- a/packages/app/src/components/Settings/index.jsx +++ /dev/null @@ -1,492 +0,0 @@ -import React from "react" -import * as antd from "antd" -import { SliderPicker } from "react-color" -import { Translation } from "react-i18next" -import classnames from "classnames" - -import config from "config" -import { Icons, createIconRender } from "components/Icons" - -import SettingsList from "schemas/settings" -import groupsDecorator from "schemas/settingsGroupsDecorator.json" - -import "./index.less" - -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 SettingsFooter = (props) => { - const isDevMode = window.__evite?.env?.NODE_ENV !== "production" - - return
-
-
{config.app?.siteName}
-
- - v{window.app.version} - -
-
- - {isDevMode ? : } - {isDevMode ? "development" : "stable"} - -
-
-
- - - {t => t("about")} - - -
-
-} - -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) - - 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") { - const result = await item.onUpdate(updateValue).catch((error) => { - console.error(error) - antd.message.error(error.message) - return false - }) - - if (!result) { - return false - } - updateValue = result - } else { - const storagedValue = await window.app.settings.get(item.id) - - if (typeof updateValue === "undefined") { - updateValue = !storagedValue - } - } - - if (item.storaged) { - await window.app.settings.set(item.id, 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) - } - - 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.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.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 - - 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) => { - return
- - {action.title} - -
- })} -
- } -
-
-
- { - loading - ?
Loading...
- : React.createElement(SettingComponent, { - ...item.props, - ctx: { - currentValue: value, - dispatchUpdate, - onUpdateItem, - ...props.ctx, - } - })} -
- - { - delayedValue &&
- } - onClick={async () => await dispatchUpdate(value)} - > - Save - -
- } -
-
-} - -const SettingGroup = React.memo((props) => { - const { - ctx, - groupKey, - settings, - loading, - } = props - - const fromDecoratorIcon = groupsDecorator[groupKey]?.icon - const fromDecoratorTitle = groupsDecorator[groupKey]?.title - - if (loading) { - return - } - - return
-

- { - fromDecoratorIcon ? React.createElement(Icons[fromDecoratorIcon]) : null - } - - { - t => t(fromDecoratorTitle ?? groupKey) - } - -

-
- { - settings.map((item) => ) - } -
-
-}) - -const SettingTab = React.memo((props) => { - const { tab } = props - - const [loading, setLoading] = React.useState(true) - const [ctxData, setCtxData] = React.useState(null) - - let groupsSettings = {} - - if (!Array.isArray(tab.settings)) { - console.error("Cannot generate settings from non-array") - return groupsSettings - } - - tab.settings.forEach((item) => { - if (!groupsSettings[item.group]) { - groupsSettings[item.group] = [] - } - - groupsSettings[item.group].push(item) - }) - - const processCtx = async () => { - setLoading(true) - - if (typeof tab.ctxData === "function") { - const resultCtx = await tab.ctxData() - - setCtxData(resultCtx) - } - - setLoading(false) - } - - React.useEffect(() => { - processCtx() - }, []) - - return Object.keys(groupsSettings).map((groupKey) => { - return - }) -}) - -const SettingsTabs = Object.keys(SettingsList).map((settingsKey) => { - const tab = SettingsList[settingsKey] - - return { - key: settingsKey, - label: <> - {createIconRender(tab.icon ?? "Settings")} - {tab.label} - , - children: - } -}) - -export default class SettingsMenu extends React.PureComponent { - state = { - activeKey: "app" - } - - componentDidMount = async () => { - if (typeof this.props.close === "function") { - // register escape key to close settings menu - window.addEventListener("keydown", this.handleKeyDown) - } - } - - componentWillUnmount() { - if (typeof this.props.close === "function") { - window.removeEventListener("keydown", this.handleKeyDown) - } - } - - handleKeyDown = (event) => { - if (event.key === "Escape") { - this.props.close() - } - } - - onClickAppAbout = () => { - window.app.setLocation("/about") - - if (typeof this.props.close === "function") { - this.props.close() - } - } - - changeTab = (activeKey) => { - this.setState({ activeKey }) - } - - render() { - return
-
- - - -
-
- } -} \ No newline at end of file diff --git a/packages/app/src/components/Settings/index.less b/packages/app/src/components/Settings/index.less deleted file mode 100755 index f5aa84ce..00000000 --- a/packages/app/src/components/Settings/index.less +++ /dev/null @@ -1,163 +0,0 @@ -.settings_wrapper { - .settings { - display: flex; - flex-direction: column; - - >div { - margin-bottom: 25px; - } - - &.mobile { - .ant-tabs-nav-list { - width: 100%; - justify-content: space-evenly; - } - } - - .ant-tabs-nav { - height: fit-content; - position: sticky; - - top: 0; - left: 0; - - .ant-tabs-nav-wrap { - height: fit-content; - - .ant-tabs-nav-list { - height: fit-content; - - .ant-tabs-tab { - padding: 5px 0 !important; - margin-right: 10px !important; - } - } - } - } - - .group { - display: flex; - flex-direction: column; - color: var(--background-color-contrast); - - h1, - h2, - h3, - h4, - h5, - h6 { - color: var(--background-color-contrast); - } - - .content { - >div { - margin-bottom: 25px; - } - } - } - - .settingItem { - padding: 0 20px; - - >div { - margin-bottom: 10px; - } - - .header { - display: inline-flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - width: 100%; - - .title { - display: flex; - align-items: center; - color: var(--background-color-contrast); - - h1, - h2, - h3, - h4, - h5, - h6 { - margin: 0; - color: var(--background-color-contrast); - } - - p { - font-size: 11px; - color: var(--background-color-contrast); - margin: 0; - } - - >div { - margin-right: 10px; - } - } - - .extraActions { - display: inline-flex; - align-items: center; - - >div { - margin-right: 10px; - } - } - } - - .component { - display: flex; - flex-direction: column; - - --ignore-dragger: true; - padding: 0 20px; - - span { - color: var(--background-color-contrast); - } - - >div { - margin-bottom: 10px; - } - } - } - - .footer { - position: relative; - width: 100%; - - padding-top: 20px; - padding-bottom: 20px; - - display: flex; - flex-direction: column; - - justify-content: center; - align-items: center; - - >div { - margin-bottom: 10px; - - font-family: "DM Mono", monospace; - font-size: 10px; - - display: flex; - flex-direction: row; - - align-items: center; - justify-content: center; - - .ant-tag { - height: 18px; - line-height: 18px; - font-size: 10px; - } - - >div { - padding: 0 7px; - } - } - } - } -} \ No newline at end of file diff --git a/packages/app/src/pages/settings/index.jsx b/packages/app/src/pages/settings/index.jsx new file mode 100644 index 00000000..48543f89 --- /dev/null +++ b/packages/app/src/pages/settings/index.jsx @@ -0,0 +1,506 @@ +import React from "react" +import * as antd from "antd" +import { SliderPicker } from "react-color" +import { Translation } from "react-i18next" +import classnames from "classnames" + +import { Icons, createIconRender } from "components/Icons" + +import SettingsList from "schemas/settings" +import menuGroupsDecorators from "schemas/settingsMenuGroupsDecorators" +import groupsDecorators from "schemas/settingsGroupsDecorators" + +import "./index.less" + +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) + + 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.settings.get(item.id) + + if (typeof updateValue === "undefined") { + updateValue = !storagedValue + } + } + + if (item.storaged) { + await window.app.settings.set(item.id, 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) + } + + 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.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.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 + + 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) => { + return
+ + {action.title} + +
+ })} +
+ } +
+
+
+ { + loading + ?
Loading...
+ : React.createElement(SettingComponent, { + ...item.props, + ctx: { + currentValue: value, + dispatchUpdate, + onUpdateItem, + ...props.ctx, + } + })} +
+ + { + delayedValue &&
+ } + onClick={async () => await dispatchUpdate(value)} + > + Save + +
+ } +
+
+} + +const SettingGroup = React.memo((props) => { + const { + ctx, + groupKey, + settings, + loading, + } = props + + const fromDecoratorIcon = groupsDecorators[groupKey]?.icon + const fromDecoratorTitle = groupsDecorators[groupKey]?.title + + if (loading) { + return + } + + 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) + }) + + 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 children = groups[groupKey].map((item) => { + return { + key: item.id, + label: <> + {createIconRender(item.icon ?? "Settings")} + {item.label} + , + } + }) + + if (index !== groupsKeys.length - 1) { + children.push({ + type: "divider", + }) + } + + return { + key: groupKey, + label: groupKey === "bottom" ? null : <> + { + menuGroupsDecorators[groupKey]?.icon && createIconRender(menuGroupsDecorators[groupKey]?.icon ?? "Settings") + } + + { + t => t(menuGroupsDecorators[groupKey]?.label ?? groupKey) + } + + , + type: "group", + children: children + } + }) +} + +export default React.memo(() => { + const [activeKey, setActiveKey] = React.useState("general") + const [menuItems, setMenuItems] = React.useState([]) + + const onChangeTab = (event) => { + setActiveKey(event.key) + } + + React.useEffect(() => { + setMenuItems(generateMenuItems()) + }, []) + + return
+
+ +
+ +
+ { + 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 new file mode 100644 index 00000000..9ea1b615 --- /dev/null +++ b/packages/app/src/pages/settings/index.less @@ -0,0 +1,152 @@ +.settings_wrapper { + display: flex; + flex-direction: row; + + .settings_menu { + position: sticky; + + top: 0; + left: 0; + + height: 100%; + + display: flex; + flex-direction: column; + + align-items: center; + + width: 30%; + } + + .settings_content { + display: flex; + flex-direction: column; + + width: 70%; + + .group { + display: flex; + flex-direction: column; + color: var(--background-color-contrast); + + h1, + h2, + h3, + h4, + h5, + h6 { + color: var(--background-color-contrast); + } + + .content { + >div { + margin-bottom: 25px; + } + } + } + + .settingItem { + padding: 0 20px; + + >div { + margin-bottom: 10px; + } + + .header { + display: inline-flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + width: 100%; + + .title { + display: flex; + align-items: center; + color: var(--background-color-contrast); + + h1, + h2, + h3, + h4, + h5, + h6 { + margin: 0; + color: var(--background-color-contrast); + } + + p { + font-size: 11px; + color: var(--background-color-contrast); + margin: 0; + } + + >div { + margin-right: 10px; + } + } + + .extraActions { + display: inline-flex; + align-items: center; + + >div { + margin-right: 10px; + } + } + } + + .component { + display: flex; + flex-direction: column; + + --ignore-dragger: true; + padding: 0 20px; + + span { + color: var(--background-color-contrast); + } + + >div { + margin-bottom: 10px; + } + } + } + + .footer { + position: relative; + width: 100%; + + padding-top: 20px; + padding-bottom: 20px; + + display: flex; + flex-direction: column; + + justify-content: center; + align-items: center; + + >div { + margin-bottom: 10px; + + font-family: "DM Mono", monospace; + font-size: 10px; + + display: flex; + flex-direction: row; + + align-items: center; + justify-content: center; + + .ant-tag { + height: 18px; + line-height: 18px; + font-size: 10px; + } + + >div { + padding: 0 7px; + } + } + } + } +} \ No newline at end of file