mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-10 02:54:15 +00:00
Merge pull request #104 from ragestudio/rewrite-settings
Rewrite settings
This commit is contained in:
commit
2511bdc4fb
@ -8,7 +8,7 @@
|
||||
"longPressDelay": 600,
|
||||
"autoCollapseDelay": 500,
|
||||
"autoCollapseDelayEnabled": true,
|
||||
"haptic_feedback": false,
|
||||
"haptic_feedback": true,
|
||||
"collapseOnLooseFocus": true,
|
||||
"style.auto_darkMode": true,
|
||||
"feed_max_fetch": 20,
|
||||
|
@ -21,6 +21,7 @@ export default {
|
||||
group: "layout",
|
||||
emitEvent: "app.softReload",
|
||||
storaged: true,
|
||||
mobile: false,
|
||||
},
|
||||
{
|
||||
id: "style.reduceAnimations",
|
||||
|
@ -1,6 +1,4 @@
|
||||
import React from "react"
|
||||
import config from "config"
|
||||
import { Select } from "antd"
|
||||
|
||||
export default {
|
||||
id: "general",
|
||||
@ -10,38 +8,42 @@ export default {
|
||||
order: 0,
|
||||
settings: [
|
||||
{
|
||||
"id": "language",
|
||||
"storaged": true,
|
||||
"group": "general",
|
||||
"component": "Select",
|
||||
"icon": "MdTranslate",
|
||||
"title": "Language",
|
||||
"description": "Choose a language for the application",
|
||||
"props": {
|
||||
children: config.i18n.languages.map((language) => {
|
||||
return <Select.Option value={language.locale}>{language.name}</Select.Option>
|
||||
id: "language",
|
||||
storaged: true,
|
||||
group: "general",
|
||||
component: "Select",
|
||||
icon: "MdTranslate",
|
||||
title: "Language",
|
||||
description: "Choose a language for the application",
|
||||
props: {
|
||||
options: config.i18n.languages.map((language) => {
|
||||
return {
|
||||
label: language.name,
|
||||
value: language.locale
|
||||
}
|
||||
})
|
||||
},
|
||||
"emitEvent": "changeLanguage"
|
||||
emitEvent: "changeLanguage"
|
||||
},
|
||||
{
|
||||
"id": "haptic_feedback",
|
||||
"storaged": true,
|
||||
"group": "general",
|
||||
"component": "Switch",
|
||||
"icon": "MdVibration",
|
||||
"title": "Haptic Feedback",
|
||||
"description": "Enable haptic feedback on touch events.",
|
||||
id: "haptic_feedback",
|
||||
storaged: true,
|
||||
group: "general",
|
||||
component: "Switch",
|
||||
icon: "MdVibration",
|
||||
title: "Haptic Feedback",
|
||||
description: "Enable haptic feedback on touch events.",
|
||||
desktop: false
|
||||
},
|
||||
{
|
||||
"id": "longPressDelay",
|
||||
"storaged": true,
|
||||
"group": "general",
|
||||
"component": "Slider",
|
||||
"icon": "MdTimer",
|
||||
"title": "Long press delay",
|
||||
"description": "Set the delay before long press trigger is activated.",
|
||||
"props": {
|
||||
id: "longPressDelay",
|
||||
storaged: true,
|
||||
group: "general",
|
||||
component: "Slider",
|
||||
icon: "MdTimer",
|
||||
title: "Long press delay",
|
||||
description: "Set the delay before long press trigger is activated.",
|
||||
props: {
|
||||
min: 300,
|
||||
max: 2000,
|
||||
step: 100,
|
||||
@ -55,24 +57,29 @@ export default {
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "clear_internal_storage",
|
||||
"storaged": false,
|
||||
"group": "general",
|
||||
"component": "Button",
|
||||
"icon": "MdDelete",
|
||||
"title": "Clear internal storage",
|
||||
"description": "Clear all the data stored in the internal storage, including your current session. It will not affect the data stored in the cloud.",
|
||||
"emitEvent": "app.clearInternalStorage"
|
||||
id: "clear_internal_storage",
|
||||
storaged: false,
|
||||
group: "general",
|
||||
component: "Button",
|
||||
icon: "MdDelete",
|
||||
title: "Clear internal storage",
|
||||
description: "Clear all the data stored in the internal storage, including your current session. It will not affect the data stored in the cloud.",
|
||||
emitEvent: "app.clearInternalStorage",
|
||||
props: {
|
||||
danger: true,
|
||||
children: "Clear"
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "low_performance_mode",
|
||||
"storaged": true,
|
||||
"group": "general",
|
||||
"component": "Switch",
|
||||
"icon": "MdSlowMotionVideo",
|
||||
"title": "Low performance mode",
|
||||
"description": "Enable low performance mode to reduce the memory usage and improve the performance in low-end devices. This will disable some animations and other decorative features.",
|
||||
"emitEvent": "app.lowPerformanceMode",
|
||||
id: "low_performance_mode",
|
||||
storaged: true,
|
||||
group: "general",
|
||||
component: "Switch",
|
||||
icon: "MdSlowMotionVideo",
|
||||
title: "Low performance mode",
|
||||
description: "Enable low performance mode to reduce the memory usage and improve the performance in low-end devices. This will disable some animations and other decorative features.",
|
||||
emitEvent: "app.lowPerformanceMode",
|
||||
experimental: true,
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
@ -83,6 +90,7 @@ export default {
|
||||
icon: "MdVolumeUp",
|
||||
title: "UI effects",
|
||||
description: "Enable the UI effects.",
|
||||
mobile: false,
|
||||
},
|
||||
{
|
||||
id: "ui.general_volume",
|
||||
@ -100,64 +108,68 @@ export default {
|
||||
max: 100,
|
||||
step: 10,
|
||||
},
|
||||
emitEvent: "change:app.general_ui_volume"
|
||||
emitEvent: "change:app.general_ui_volume",
|
||||
mobile: false,
|
||||
},
|
||||
{
|
||||
"id": "notifications_sound",
|
||||
"storaged": true,
|
||||
"group": "notifications",
|
||||
"component": "Switch",
|
||||
"icon": "MdVolumeUp",
|
||||
"title": "Notifications Sound",
|
||||
"description": "Play a sound when a notification is received.",
|
||||
id: "notifications_sound",
|
||||
storaged: true,
|
||||
group: "notifications",
|
||||
component: "Switch",
|
||||
icon: "MdVolumeUp",
|
||||
title: "Notifications Sound",
|
||||
description: "Play a sound when a notification is received.",
|
||||
},
|
||||
{
|
||||
"id": "notifications_vibrate",
|
||||
"storaged": true,
|
||||
"group": "notifications",
|
||||
"component": "Switch",
|
||||
"icon": "MdVibration",
|
||||
"title": "Vibration",
|
||||
"description": "Vibrate the device when a notification is received.",
|
||||
"emitEvent": "changeNotificationsVibrate"
|
||||
id: "notifications_vibrate",
|
||||
storaged: true,
|
||||
group: "notifications",
|
||||
component: "Switch",
|
||||
icon: "MdVibration",
|
||||
title: "Vibration",
|
||||
description: "Vibrate the device when a notification is received.",
|
||||
emitEvent: "changeNotificationsVibrate",
|
||||
desktop: false,
|
||||
},
|
||||
{
|
||||
"id": "notifications_sound_volume",
|
||||
"storaged": true,
|
||||
"group": "notifications",
|
||||
"component": "Slider",
|
||||
"icon": "MdVolumeUp",
|
||||
"title": "Sound Volume",
|
||||
"description": "Set the volume of the sound when a notification is received.",
|
||||
"props": {
|
||||
id: "notifications_sound_volume",
|
||||
storaged: true,
|
||||
group: "notifications",
|
||||
component: "Slider",
|
||||
icon: "MdVolumeUp",
|
||||
title: "Sound Volume",
|
||||
description: "Set the volume of the sound when a notification is received.",
|
||||
props: {
|
||||
tipFormatter: (value) => {
|
||||
return `${value}%`
|
||||
}
|
||||
},
|
||||
"emitEvent": "changeNotificationsSoundVolume"
|
||||
emitEvent: "changeNotificationsSoundVolume",
|
||||
mobile: false,
|
||||
},
|
||||
{
|
||||
"id": "collapseOnLooseFocus",
|
||||
"storaged": true,
|
||||
"group": "sidebar",
|
||||
"component": "Switch",
|
||||
"icon": "Columns",
|
||||
"title": "Auto Collapse",
|
||||
"description": "Collapse the sidebar when loose focus",
|
||||
"emitEvent": "settingChanged.sidebar_collapse",
|
||||
id: "collapseOnLooseFocus",
|
||||
storaged: true,
|
||||
group: "sidebar",
|
||||
component: "Switch",
|
||||
icon: "Columns",
|
||||
title: "Auto Collapse",
|
||||
description: "Collapse the sidebar when loose focus",
|
||||
emitEvent: "settingChanged.sidebar_collapse",
|
||||
mobile: false,
|
||||
},
|
||||
{
|
||||
"id": "autoCollapseDelay",
|
||||
"storaged": true,
|
||||
"group": "sidebar",
|
||||
"component": "Slider",
|
||||
"icon": "MdTimer",
|
||||
"dependsOn": {
|
||||
id: "autoCollapseDelay",
|
||||
storaged: true,
|
||||
group: "sidebar",
|
||||
component: "Slider",
|
||||
icon: "MdTimer",
|
||||
dependsOn: {
|
||||
"collapseOnLooseFocus": true
|
||||
},
|
||||
"title": "Auto Collapse timeout",
|
||||
"description": "Set the delay before the sidebar is collapsed",
|
||||
"props": {
|
||||
title: "Auto Collapse timeout",
|
||||
description: "Set the delay before the sidebar is collapsed",
|
||||
props: {
|
||||
min: 0,
|
||||
max: 2000,
|
||||
step: 100,
|
||||
@ -168,41 +180,21 @@ export default {
|
||||
1500: "1.5s",
|
||||
2000: "2s",
|
||||
}
|
||||
}
|
||||
},
|
||||
mobile: false,
|
||||
},
|
||||
{
|
||||
"id": "feed_max_fetch",
|
||||
"title": "Fetch max items",
|
||||
"description": "Set the maximum number of items to load per fetch in the feed list",
|
||||
"component": "Slider",
|
||||
"icon": "MdFormatListNumbered",
|
||||
"group": "posts",
|
||||
"props": {
|
||||
id: "feed_max_fetch",
|
||||
title: "Fetch max items",
|
||||
description: "Set the maximum number of items to load per fetch in the feed list",
|
||||
component: "Slider",
|
||||
icon: "MdFormatListNumbered",
|
||||
group: "posts",
|
||||
props: {
|
||||
min: 5,
|
||||
max: 50,
|
||||
},
|
||||
"storaged": true,
|
||||
},
|
||||
{
|
||||
"id": "postCard_carrusel_auto",
|
||||
"title": "Post autoplay",
|
||||
"description": "Automatically play the post medias when the post has multiple medias",
|
||||
"component": "Switch",
|
||||
"icon": "MdPhotoCameraBack",
|
||||
"group": "posts",
|
||||
"storaged": true,
|
||||
"emitEvent": "router.forceUpdate",
|
||||
"disabled": true
|
||||
},
|
||||
{
|
||||
"id": "postCard_expansible_actions",
|
||||
"title": "Expansible actions",
|
||||
"description": "Automatically show or hide the actions bar",
|
||||
"component": "Switch",
|
||||
"icon": "MdCallToAction",
|
||||
"group": "posts",
|
||||
"storaged": true,
|
||||
"emitEvent": "router.forceUpdate"
|
||||
storaged: true,
|
||||
},
|
||||
]
|
||||
}
|
@ -1,26 +1,139 @@
|
||||
const settingsPaths = import.meta.glob("/constants/settings/*/index.jsx")
|
||||
async function composeSettingsByGroups() {
|
||||
console.time("load settings")
|
||||
|
||||
export default async () => {
|
||||
const settings = {}
|
||||
/* @vite-ignore */
|
||||
let _settings = import.meta.glob("/constants/settings/*/index.jsx")
|
||||
|
||||
for (const [key, value] of Object.entries(settingsPaths)) {
|
||||
const path = key.split("/").slice(-2)
|
||||
_settings = Object.entries(_settings).map(([route, moduleFile]) => {
|
||||
const path = route.split("/").slice(-2)
|
||||
const name = path[0]
|
||||
|
||||
if (name === "components" || name === "index") {
|
||||
continue
|
||||
return null
|
||||
}
|
||||
|
||||
if (!settings[name]) {
|
||||
settings[name] = {}
|
||||
return moduleFile
|
||||
})
|
||||
|
||||
_settings = _settings.filter((moduleFile) => moduleFile)
|
||||
|
||||
_settings = await Promise.all(_settings.map((moduleFile) => moduleFile()))
|
||||
|
||||
_settings = _settings.map((moduleFile) => {
|
||||
return moduleFile.default || moduleFile
|
||||
})
|
||||
|
||||
_settings = _settings.sort((a, b) => {
|
||||
if (a.group === "bottom") {
|
||||
return 1
|
||||
}
|
||||
|
||||
let setting = await value()
|
||||
if (b.group === "bottom") {
|
||||
return -1
|
||||
}
|
||||
|
||||
setting = setting.default || setting
|
||||
return 0
|
||||
})
|
||||
|
||||
settings[name] = setting
|
||||
_settings = _settings.reduce((acc, settingModule) => {
|
||||
if (typeof acc[settingModule.group] !== "object") {
|
||||
acc[settingModule.group] = []
|
||||
}
|
||||
|
||||
acc[settingModule.group].push(settingModule)
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
_settings = Object.entries(_settings).map(([group, groupModule]) => {
|
||||
// filter setting by platform
|
||||
groupModule = groupModule.map((subGroup) => {
|
||||
if (Array.isArray(subGroup.settings)) {
|
||||
subGroup.settings = subGroup.settings.filter((setting) => {
|
||||
if (!app.isMobile && setting.desktop === false) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (app.isMobile && setting.mobile === false) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return subGroup
|
||||
})
|
||||
|
||||
return {
|
||||
group,
|
||||
groupModule: groupModule
|
||||
}
|
||||
})
|
||||
|
||||
// order groups
|
||||
_settings = _settings.map((group) => {
|
||||
group.groupModule = group.groupModule.sort((a, b) => {
|
||||
if (typeof a.order === undefined) {
|
||||
// find index
|
||||
a.order = group.groupModule.indexOf(a)
|
||||
}
|
||||
|
||||
if (typeof b.order === undefined) {
|
||||
// find index
|
||||
b.order = group.groupModule.indexOf(b)
|
||||
}
|
||||
|
||||
return a.order - b.order
|
||||
})
|
||||
|
||||
return group
|
||||
})
|
||||
|
||||
console.timeEnd("load settings")
|
||||
|
||||
return _settings
|
||||
}
|
||||
|
||||
function composeTabsFromGroups(settingsGroups) {
|
||||
return settingsGroups.reduce((acc, entry) => {
|
||||
entry.groupModule.forEach((item) => {
|
||||
if (item.id) {
|
||||
acc[item.id] = item
|
||||
}
|
||||
})
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
function composeGroupsFromSettingsTab(settings) {
|
||||
if (!Array.isArray(settings)) {
|
||||
console.error("settings is not an array")
|
||||
return []
|
||||
}
|
||||
|
||||
return settings
|
||||
return settings.reduce((acc, setting) => {
|
||||
if (setting.group) {
|
||||
if (typeof acc[setting.group] === "undefined") {
|
||||
acc[setting.group] = []
|
||||
}
|
||||
|
||||
acc[setting.group].push(setting)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
const composedSettingsByGroups = await composeSettingsByGroups()
|
||||
const composedTabs = composeTabsFromGroups(composedSettingsByGroups)
|
||||
|
||||
export {
|
||||
composedSettingsByGroups,
|
||||
composedTabs,
|
||||
|
||||
composeSettingsByGroups,
|
||||
composeTabsFromGroups,
|
||||
composeGroupsFromSettingsTab,
|
||||
}
|
@ -92,6 +92,33 @@ export default {
|
||||
"player.compressor": true
|
||||
},
|
||||
storaged: false,
|
||||
},
|
||||
{
|
||||
id: "player.gain",
|
||||
title: "Gain",
|
||||
icon: "MdGraphicEq",
|
||||
group: "general",
|
||||
description: "Adjust gain for audio output",
|
||||
component: "Slider",
|
||||
props: {
|
||||
min: 1,
|
||||
max: 2,
|
||||
step: 0.1,
|
||||
marks: {
|
||||
1: "Off",
|
||||
1.5: "50%",
|
||||
2: "100%"
|
||||
}
|
||||
},
|
||||
defaultValue: () => {
|
||||
return app.cores.player.gain.values().gain
|
||||
},
|
||||
onUpdate: (value) => {
|
||||
app.cores.player.gain.modifyValues({
|
||||
gain: value
|
||||
})
|
||||
},
|
||||
storaged: false,
|
||||
}
|
||||
]
|
||||
}
|
@ -51,7 +51,7 @@ export default {
|
||||
})
|
||||
|
||||
if (result) {
|
||||
return result
|
||||
return value
|
||||
}
|
||||
},
|
||||
extraActions: [
|
||||
@ -89,7 +89,7 @@ export default {
|
||||
})
|
||||
|
||||
if (result) {
|
||||
return result
|
||||
return value
|
||||
}
|
||||
},
|
||||
"debounced": true,
|
||||
@ -105,6 +105,7 @@ export default {
|
||||
UploadButton
|
||||
],
|
||||
"defaultValue": (ctx) => {
|
||||
console.log(ctx)
|
||||
return ctx.userData.avatar
|
||||
},
|
||||
"onUpdate": async (value) => {
|
||||
@ -114,7 +115,7 @@ export default {
|
||||
|
||||
if (result) {
|
||||
app.message.success("Avatar updated")
|
||||
return result
|
||||
return value
|
||||
}
|
||||
},
|
||||
"debounced": true,
|
||||
@ -139,7 +140,7 @@ export default {
|
||||
|
||||
if (result) {
|
||||
app.message.success("Cover updated")
|
||||
return result
|
||||
return value
|
||||
}
|
||||
},
|
||||
"debounced": true,
|
||||
@ -166,7 +167,7 @@ export default {
|
||||
})
|
||||
|
||||
if (result) {
|
||||
return result
|
||||
return value
|
||||
}
|
||||
},
|
||||
"debounced": true,
|
||||
|
@ -34,17 +34,6 @@ export default {
|
||||
"icon": "Monitor",
|
||||
"component": loadable(() => import("../components/sessions")),
|
||||
"storaged": false
|
||||
},
|
||||
{
|
||||
"id": "logout",
|
||||
"group": "security.other",
|
||||
"component": "Button",
|
||||
"icon": "LogOut",
|
||||
"title": "Logout",
|
||||
"description": "Logout from your account",
|
||||
onUpdate: async () => {
|
||||
await AuthModel.logout()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -339,6 +339,15 @@ class ComtyApp extends React.Component {
|
||||
app.eventBus.emit("layout.forceUpdate")
|
||||
app.eventBus.emit("router.forceUpdate")
|
||||
},
|
||||
"app.logout_request": () => {
|
||||
antd.Modal.confirm({
|
||||
title: "Logout",
|
||||
content: "Are you sure you want to logout?",
|
||||
onOk: () => {
|
||||
AuthModel.logout()
|
||||
},
|
||||
})
|
||||
},
|
||||
"app.no_session": async () => {
|
||||
const location = window.location.pathname
|
||||
|
||||
@ -452,7 +461,7 @@ class ComtyApp extends React.Component {
|
||||
app.eventBus.emit("statusTap")
|
||||
})
|
||||
|
||||
StatusBar.setOverlaysWebView({ overlay: true })
|
||||
StatusBar.setOverlaysWebView({ overlay: false })
|
||||
|
||||
CapacitorApp.addListener("backButton", ({ canGoBack }) => {
|
||||
if (!canGoBack) {
|
||||
|
@ -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