Refactor Settings Component and Introduce New Settings Schema
- Removed unused imports and streamlined component structure for better readability. - Added a new settings schema to manage user and app settings, enhancing configurability. - Updated state management for settings, including draft and saved settings handling. - Improved appearance application logic to dynamically adjust theme and density based on user preferences. - Introduced utility functions for validating and picking settings, ensuring robust settings management.
This commit is contained in:
parent
da8fe8de2d
commit
9a601245b6
@ -1,10 +1,8 @@
|
||||
import { useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { Descriptions, Flex, Select, Space, Spin, Typography } from 'antd'
|
||||
import { LoadingOutlined, SettingOutlined } from '@ant-design/icons'
|
||||
import { Flex, Space } from 'antd'
|
||||
import { useThemeContext } from '../context/ThemeContext'
|
||||
import { ApiServerContext } from '../context/ApiServerContext'
|
||||
import { ElectronContext } from '../context/ElectronContext'
|
||||
import { AuthContext } from '../context/AuthContext'
|
||||
import {
|
||||
normalizeAppUpdateEngine,
|
||||
useAppUpdateContext
|
||||
@ -12,20 +10,29 @@ import {
|
||||
import { useMessageContext } from '../context/MessageContext'
|
||||
import useCollapseState from '../hooks/useCollapseState'
|
||||
import InfoCollapse from '../common/InfoCollapse'
|
||||
import ObjectInfo from '../common/ObjectInfo'
|
||||
import ViewButton from '../common/ViewButton'
|
||||
import EditButtons from '../common/EditButtons'
|
||||
import ScrollBox from '../common/ScrollBox'
|
||||
import settingsSchema, {
|
||||
APP_SETTING_NAMES,
|
||||
USER_SETTING_NAMES,
|
||||
areSettingsValid,
|
||||
getEffectiveAppearance,
|
||||
getSettingsDefaults,
|
||||
getVisibleSettingsSections,
|
||||
pickSettings
|
||||
} from '../../../database/Settings'
|
||||
|
||||
const { Text } = Typography
|
||||
const { Option } = Select
|
||||
const DEFAULT_UPDATE_BRANCH = 'main'
|
||||
const DEFAULT_UPDATE_ENGINE = 'native'
|
||||
|
||||
const engineLabel = (engine) => {
|
||||
const normalized = normalizeAppUpdateEngine(engine)
|
||||
if (normalized === 'chromium') return 'Chromium'
|
||||
if (normalized === 'native') return 'Native'
|
||||
return 'Not configured'
|
||||
}
|
||||
const LEGACY_ELECTRON_USER_KEYS = [
|
||||
'theme',
|
||||
'density',
|
||||
'showNavigationLabels',
|
||||
'dateTimeFormat',
|
||||
'timezone'
|
||||
]
|
||||
|
||||
const Settings = () => {
|
||||
const {
|
||||
@ -37,145 +44,183 @@ const Settings = () => {
|
||||
showNavigationLabels,
|
||||
setShowNavigationLabels
|
||||
} = useThemeContext()
|
||||
const { fetchAppUpdateBranches } = useContext(ApiServerContext)
|
||||
const {
|
||||
connected,
|
||||
fetchAppUpdateBranches,
|
||||
updateUserSettings,
|
||||
userSettings,
|
||||
userSettingsLoaded
|
||||
} = useContext(ApiServerContext)
|
||||
const { isElectron, getAppSettings, setAppSettings, getAppEngine } =
|
||||
useContext(ElectronContext)
|
||||
const { recheckForUpdates } = useAppUpdateContext()
|
||||
const { userProfile, setUserProfile } = useContext(AuthContext)
|
||||
const { showSuccess, showError } = useMessageContext()
|
||||
const [collapseState, updateCollapseState] = useCollapseState('Settings', {
|
||||
appearance: true,
|
||||
appUpdates: true
|
||||
})
|
||||
const visibleSections = useMemo(
|
||||
() => getVisibleSettingsSections({ isElectron }),
|
||||
[isElectron]
|
||||
)
|
||||
const [collapseState, updateCollapseState] = useCollapseState(
|
||||
'Settings',
|
||||
Object.fromEntries(
|
||||
settingsSchema.sections.map((section) => [section.key, true])
|
||||
)
|
||||
)
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [settingsLoading, setSettingsLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [appSettings, setAppSettingsState] = useState({})
|
||||
const [savedSettings, setSavedSettings] = useState({})
|
||||
const [draftSettings, setDraftSettings] = useState({})
|
||||
const [electronSettings, setElectronSettings] = useState({})
|
||||
const [branches, setBranches] = useState([])
|
||||
const [branchLoading, setBranchLoading] = useState(false)
|
||||
const [runningEngine, setRunningEngine] = useState(DEFAULT_UPDATE_ENGINE)
|
||||
|
||||
const currentThemeValue = isSystem ? 'system' : isDarkMode ? 'dark' : 'light'
|
||||
const currentDensityValue = isCompact ? 'compact' : 'comfortable'
|
||||
const userAppearance = userSettings?.appearance || {}
|
||||
|
||||
const resolvedSettings = useMemo(() => {
|
||||
const defaults = getSettingsDefaults({ isElectron })
|
||||
const stored = isEditing ? draftSettings : savedSettings
|
||||
const defaultBranch = branches.includes(DEFAULT_UPDATE_BRANCH)
|
||||
? DEFAULT_UPDATE_BRANCH
|
||||
: branches[0]
|
||||
|
||||
return {
|
||||
...defaults,
|
||||
theme: currentThemeValue,
|
||||
density: currentDensityValue,
|
||||
showNavigationLabels: showNavigationLabels ?? false,
|
||||
...(isElectron
|
||||
? {
|
||||
appTheme: stored.appTheme || currentThemeValue,
|
||||
appShowNavigationLabels:
|
||||
stored.appShowNavigationLabels ?? showNavigationLabels ?? false,
|
||||
appUpdateBranch: defaultBranch,
|
||||
appUpdateEngine:
|
||||
normalizeAppUpdateEngine(stored.appUpdateEngine) ||
|
||||
DEFAULT_UPDATE_ENGINE
|
||||
}
|
||||
: {}),
|
||||
...stored
|
||||
}
|
||||
}, [
|
||||
branches,
|
||||
currentDensityValue,
|
||||
currentThemeValue,
|
||||
draftSettings,
|
||||
isEditing,
|
||||
isElectron,
|
||||
savedSettings,
|
||||
showNavigationLabels
|
||||
])
|
||||
|
||||
const applyAppearance = (nextSettings, nextElectronSettings = electronSettings) => {
|
||||
const effective = getEffectiveAppearance({
|
||||
isElectron,
|
||||
userAppearance: nextSettings,
|
||||
electronSettings: {
|
||||
...nextElectronSettings,
|
||||
appTheme: nextSettings.appTheme,
|
||||
appShowNavigationLabels: nextSettings.appShowNavigationLabels,
|
||||
overrideTheme: nextSettings.theme === 'app'
|
||||
}
|
||||
})
|
||||
if (effective.theme) setThemeMode(effective.theme)
|
||||
if (effective.density) setDensityMode(effective.density)
|
||||
if (effective.showNavigationLabels !== undefined) {
|
||||
setShowNavigationLabels(effective.showNavigationLabels)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!userSettingsLoaded || isEditing) return
|
||||
|
||||
const loadSettings = async () => {
|
||||
setSettingsLoading(true)
|
||||
const [storedSettings, detectedEngine] = await Promise.all([
|
||||
isElectron ? getAppSettings() : Promise.resolve(userProfile?.settings || {}),
|
||||
const [storedElectronSettings, detectedEngine] = await Promise.all([
|
||||
isElectron ? getAppSettings() : Promise.resolve({}),
|
||||
isElectron ? getAppEngine() : Promise.resolve(DEFAULT_UPDATE_ENGINE)
|
||||
])
|
||||
const nextEngine =
|
||||
normalizeAppUpdateEngine(detectedEngine) || DEFAULT_UPDATE_ENGINE
|
||||
setRunningEngine(nextEngine)
|
||||
|
||||
const nextSettings = { ...(storedSettings || {}) }
|
||||
if (isElectron && !normalizeAppUpdateEngine(nextSettings.appUpdateEngine)) {
|
||||
nextSettings.appUpdateEngine = nextEngine
|
||||
const nextElectronSettings = { ...(storedElectronSettings || {}) }
|
||||
if (
|
||||
isElectron &&
|
||||
!normalizeAppUpdateEngine(nextElectronSettings.appUpdateEngine)
|
||||
) {
|
||||
nextElectronSettings.appUpdateEngine = nextEngine
|
||||
}
|
||||
|
||||
setAppSettingsState(nextSettings)
|
||||
const nextSettings = {
|
||||
...getSettingsDefaults({ isElectron }),
|
||||
...pickSettings(userAppearance, USER_SETTING_NAMES),
|
||||
...(isElectron
|
||||
? {
|
||||
...pickSettings(nextElectronSettings, APP_SETTING_NAMES),
|
||||
appTheme:
|
||||
nextElectronSettings.appTheme || nextElectronSettings.theme,
|
||||
appShowNavigationLabels:
|
||||
nextElectronSettings.appShowNavigationLabels ??
|
||||
nextElectronSettings.showNavigationLabels,
|
||||
theme: nextElectronSettings.overrideTheme
|
||||
? 'app'
|
||||
: userAppearance.theme
|
||||
}
|
||||
: {})
|
||||
}
|
||||
|
||||
setElectronSettings(nextElectronSettings)
|
||||
setSavedSettings(nextSettings)
|
||||
setDraftSettings(nextSettings)
|
||||
applyAppearance(nextSettings, nextElectronSettings)
|
||||
setSettingsLoading(false)
|
||||
}
|
||||
|
||||
loadSettings()
|
||||
}, [getAppEngine, getAppSettings, isElectron, userProfile?.settings])
|
||||
|
||||
useEffect(() => {
|
||||
if (settingsLoading || isEditing) return
|
||||
if (appSettings.theme) setThemeMode(appSettings.theme)
|
||||
if (appSettings.density) setDensityMode(appSettings.density)
|
||||
if (appSettings.showNavigationLabels !== undefined) {
|
||||
setShowNavigationLabels(appSettings.showNavigationLabels)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- reload when stored appearance/app settings change
|
||||
}, [
|
||||
appSettings.density,
|
||||
appSettings.showNavigationLabels,
|
||||
appSettings.theme,
|
||||
getAppEngine,
|
||||
getAppSettings,
|
||||
isEditing,
|
||||
setDensityMode,
|
||||
setShowNavigationLabels,
|
||||
setThemeMode,
|
||||
settingsLoading
|
||||
isElectron,
|
||||
userAppearance.dateTimeFormat,
|
||||
userAppearance.density,
|
||||
userAppearance.showNavigationLabels,
|
||||
userAppearance.theme,
|
||||
userAppearance.timezone,
|
||||
userSettingsLoaded
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isElectron) {
|
||||
setBranches([])
|
||||
setBranchLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const loadBranches = async () => {
|
||||
setBranchLoading(true)
|
||||
const availableBranches = await fetchAppUpdateBranches()
|
||||
setBranches(availableBranches)
|
||||
|
||||
setDraftSettings((previous) => {
|
||||
if (previous.appUpdateBranch) return previous
|
||||
|
||||
const defaultBranch = availableBranches.includes(DEFAULT_UPDATE_BRANCH)
|
||||
? DEFAULT_UPDATE_BRANCH
|
||||
: availableBranches[0]
|
||||
|
||||
return defaultBranch
|
||||
? { ...previous, appUpdateBranch: defaultBranch }
|
||||
: previous
|
||||
})
|
||||
|
||||
setBranchLoading(false)
|
||||
}
|
||||
|
||||
loadBranches()
|
||||
}, [fetchAppUpdateBranches, isElectron])
|
||||
|
||||
const branchOptions = useMemo(
|
||||
() =>
|
||||
branches.map((branch) => (
|
||||
<Option key={branch} value={branch}>
|
||||
{branch}
|
||||
</Option>
|
||||
)),
|
||||
[branches]
|
||||
)
|
||||
const viewItems = [
|
||||
{ key: 'appearance', label: 'Appearance Settings' },
|
||||
...(isElectron ? [{ key: 'appUpdates', label: 'App Update Settings' }] : [])
|
||||
]
|
||||
|
||||
const getCurrentThemeValue = () => {
|
||||
if (isSystem) return 'system'
|
||||
return isDarkMode ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
const currentThemeValue = getCurrentThemeValue()
|
||||
const currentDensityValue = isCompact ? 'compact' : 'comfortable'
|
||||
const currentShowNavigationLabels =
|
||||
appSettings.showNavigationLabels ?? showNavigationLabels ?? false
|
||||
const currentBranch =
|
||||
appSettings.appUpdateBranch ||
|
||||
(branches.includes(DEFAULT_UPDATE_BRANCH) ? DEFAULT_UPDATE_BRANCH : null) ||
|
||||
branches[0] ||
|
||||
'Not configured'
|
||||
const currentEngine =
|
||||
normalizeAppUpdateEngine(appSettings.appUpdateEngine) ||
|
||||
normalizeAppUpdateEngine(runningEngine) ||
|
||||
DEFAULT_UPDATE_ENGINE
|
||||
|
||||
const startEditing = () => {
|
||||
setDraftSettings({
|
||||
...appSettings,
|
||||
appUpdateBranch:
|
||||
currentBranch === 'Not configured' ? undefined : currentBranch,
|
||||
appUpdateEngine: currentEngine,
|
||||
theme: currentThemeValue,
|
||||
density: currentDensityValue,
|
||||
showNavigationLabels: currentShowNavigationLabels
|
||||
})
|
||||
setDraftSettings(resolvedSettings)
|
||||
setIsEditing(true)
|
||||
}
|
||||
|
||||
const cancelEditing = () => {
|
||||
setDraftSettings(appSettings)
|
||||
setDraftSettings(savedSettings)
|
||||
setIsEditing(false)
|
||||
}
|
||||
|
||||
@ -183,55 +228,67 @@ const Settings = () => {
|
||||
setSaving(true)
|
||||
|
||||
try {
|
||||
const useAppTheme = isElectron && draftSettings.theme === 'app'
|
||||
const nextEngine =
|
||||
normalizeAppUpdateEngine(draftSettings.appUpdateEngine) ||
|
||||
currentEngine
|
||||
DEFAULT_UPDATE_ENGINE
|
||||
const nextSettings = {
|
||||
...appSettings,
|
||||
theme: draftSettings.theme,
|
||||
density: draftSettings.density,
|
||||
...getSettingsDefaults({ isElectron }),
|
||||
...savedSettings,
|
||||
...draftSettings,
|
||||
showNavigationLabels: Boolean(draftSettings.showNavigationLabels),
|
||||
...(isElectron
|
||||
? {
|
||||
appShowNavigationLabels: Boolean(
|
||||
draftSettings.appShowNavigationLabels
|
||||
),
|
||||
appUpdateBranch: draftSettings.appUpdateBranch,
|
||||
appUpdateEngine: nextEngine
|
||||
}
|
||||
: {})
|
||||
}
|
||||
const saved = isElectron
|
||||
? await setAppSettings(nextSettings)
|
||||
: Boolean(userProfile)
|
||||
|
||||
if (!saved) {
|
||||
const userUpdates = USER_SETTING_NAMES.filter(
|
||||
(name) => !(name === 'theme' && useAppTheme)
|
||||
).map((name) => updateUserSettings('appearance', name, nextSettings[name]))
|
||||
|
||||
const userResults = await Promise.all(userUpdates)
|
||||
if (connected && userResults.some((result) => result == null)) {
|
||||
showError('Unable to save settings.')
|
||||
return
|
||||
}
|
||||
|
||||
if (!isElectron) {
|
||||
setUserProfile((previous) => ({
|
||||
...previous,
|
||||
settings: {
|
||||
...(previous?.settings || {}),
|
||||
theme: draftSettings.theme,
|
||||
density: draftSettings.density,
|
||||
showNavigationLabels: Boolean(draftSettings.showNavigationLabels)
|
||||
let nextElectronSettings = electronSettings
|
||||
if (isElectron) {
|
||||
nextElectronSettings = { ...electronSettings }
|
||||
for (const key of LEGACY_ELECTRON_USER_KEYS) {
|
||||
delete nextElectronSettings[key]
|
||||
}
|
||||
}))
|
||||
nextElectronSettings = {
|
||||
...nextElectronSettings,
|
||||
...pickSettings(nextSettings, APP_SETTING_NAMES),
|
||||
overrideTheme: useAppTheme
|
||||
}
|
||||
|
||||
setThemeMode(draftSettings.theme)
|
||||
setDensityMode(draftSettings.density)
|
||||
setShowNavigationLabels(draftSettings.showNavigationLabels)
|
||||
setAppSettingsState(nextSettings)
|
||||
const saved = await setAppSettings(nextElectronSettings)
|
||||
if (!saved) {
|
||||
showError('Unable to save settings.')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
applyAppearance(nextSettings, nextElectronSettings)
|
||||
setElectronSettings(nextElectronSettings)
|
||||
setSavedSettings(nextSettings)
|
||||
setDraftSettings(nextSettings)
|
||||
setIsEditing(false)
|
||||
showSuccess('Settings saved.')
|
||||
|
||||
const appUpdateSettingsChanged =
|
||||
isElectron &&
|
||||
(nextSettings.appUpdateBranch !== appSettings.appUpdateBranch ||
|
||||
(nextSettings.appUpdateBranch !== savedSettings.appUpdateBranch ||
|
||||
nextSettings.appUpdateEngine !==
|
||||
normalizeAppUpdateEngine(appSettings.appUpdateEngine))
|
||||
normalizeAppUpdateEngine(savedSettings.appUpdateEngine))
|
||||
|
||||
if (appUpdateSettingsChanged) {
|
||||
void recheckForUpdates()
|
||||
@ -247,7 +304,10 @@ const Settings = () => {
|
||||
<Space size='small'>
|
||||
<ViewButton
|
||||
disabled={settingsLoading}
|
||||
items={viewItems}
|
||||
items={visibleSections.map((section) => ({
|
||||
key: section.key,
|
||||
label: section.name
|
||||
}))}
|
||||
visibleState={collapseState}
|
||||
updateVisibleState={updateCollapseState}
|
||||
/>
|
||||
@ -257,163 +317,51 @@ const Settings = () => {
|
||||
handleUpdate={handleSave}
|
||||
cancelEditing={cancelEditing}
|
||||
startEditing={startEditing}
|
||||
formValid={
|
||||
Boolean(draftSettings.theme && draftSettings.density) &&
|
||||
(!isElectron ||
|
||||
(Boolean(draftSettings.appUpdateBranch) &&
|
||||
Boolean(normalizeAppUpdateEngine(draftSettings.appUpdateEngine))))
|
||||
}
|
||||
disabled={settingsLoading || (!isElectron && !userProfile)}
|
||||
formValid={areSettingsValid(draftSettings, { isElectron })}
|
||||
disabled={settingsLoading || !userSettingsLoaded}
|
||||
loading={saving}
|
||||
/>
|
||||
</Flex>
|
||||
<div style={{ height: '100%', minHeight: 0, overflowY: 'auto' }}>
|
||||
<Spin spinning={settingsLoading} indicator={<LoadingOutlined />}>
|
||||
<ScrollBox>
|
||||
<Flex vertical gap='large'>
|
||||
{visibleSections.map((section) => {
|
||||
const Icon = section.icon
|
||||
return (
|
||||
<InfoCollapse
|
||||
title='Appearance Settings'
|
||||
icon={<SettingOutlined />}
|
||||
active={collapseState.appearance}
|
||||
key={section.key}
|
||||
title={section.name}
|
||||
icon={<Icon />}
|
||||
active={collapseState[section.key]}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('appearance', expanded)
|
||||
updateCollapseState(section.key, expanded)
|
||||
}
|
||||
collapseKey='appearance'
|
||||
collapseKey={section.key}
|
||||
>
|
||||
<Descriptions
|
||||
bordered
|
||||
column={{
|
||||
xs: 1,
|
||||
sm: 1,
|
||||
md: 1,
|
||||
lg: 2,
|
||||
xl: 2,
|
||||
xxl: 2
|
||||
}}
|
||||
>
|
||||
<Descriptions.Item label='Theme'>
|
||||
{isEditing ? (
|
||||
<Select
|
||||
value={draftSettings.theme}
|
||||
onChange={(value) =>
|
||||
<ObjectInfo
|
||||
loading={settingsLoading}
|
||||
isEditing={isEditing}
|
||||
propertyDefinitions={section.properties}
|
||||
objectData={resolvedSettings}
|
||||
parentData={{ branches, isElectron }}
|
||||
onPropertyChange={
|
||||
isEditing
|
||||
? (name, value) =>
|
||||
setDraftSettings((previous) => ({
|
||||
...previous,
|
||||
theme: value
|
||||
[name]:
|
||||
value?.target && typeof value.target === 'object'
|
||||
? value.target.value
|
||||
: value
|
||||
}))
|
||||
: undefined
|
||||
}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Option value='light'>Light</Option>
|
||||
<Option value='dark'>Dark</Option>
|
||||
<Option value='system'>System</Option>
|
||||
</Select>
|
||||
) : (
|
||||
<Text>{currentThemeValue}</Text>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label='UI Density'>
|
||||
{isEditing ? (
|
||||
<Select
|
||||
value={draftSettings.density}
|
||||
onChange={(value) =>
|
||||
setDraftSettings((previous) => ({
|
||||
...previous,
|
||||
density: value
|
||||
}))
|
||||
}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Option value='comfortable'>Comfortable</Option>
|
||||
<Option value='compact'>Compact</Option>
|
||||
</Select>
|
||||
) : (
|
||||
<Text>{isCompact ? 'Compact' : 'Comfortable'}</Text>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label='Navigation Labels'>
|
||||
{isEditing ? (
|
||||
<Select
|
||||
value={
|
||||
draftSettings.showNavigationLabels ? 'show' : 'hide'
|
||||
}
|
||||
onChange={(value) =>
|
||||
setDraftSettings((previous) => ({
|
||||
...previous,
|
||||
showNavigationLabels: value === 'show'
|
||||
}))
|
||||
}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Option value='show'>Show</Option>
|
||||
<Option value='hide'>Hide</Option>
|
||||
</Select>
|
||||
) : (
|
||||
<Text>
|
||||
{currentShowNavigationLabels ? 'Show' : 'Hide'}
|
||||
</Text>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
{...(section.column ? { column: section.column } : {})}
|
||||
/>
|
||||
</InfoCollapse>
|
||||
{isElectron && (
|
||||
<InfoCollapse
|
||||
title='App Update Settings'
|
||||
icon={<SettingOutlined />}
|
||||
active={collapseState.appUpdates}
|
||||
onToggle={(expanded) =>
|
||||
updateCollapseState('appUpdates', expanded)
|
||||
}
|
||||
collapseKey='appUpdates'
|
||||
>
|
||||
<Descriptions bordered column={1}>
|
||||
<Descriptions.Item label='Branch'>
|
||||
{isEditing ? (
|
||||
<Select
|
||||
value={draftSettings.appUpdateBranch}
|
||||
onChange={(value) =>
|
||||
setDraftSettings((previous) => ({
|
||||
...previous,
|
||||
appUpdateBranch: value
|
||||
}))
|
||||
}
|
||||
style={{ width: '100%' }}
|
||||
loading={branchLoading}
|
||||
placeholder='Select a branch'
|
||||
>
|
||||
{branchOptions}
|
||||
</Select>
|
||||
) : (
|
||||
<Text>{currentBranch}</Text>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label='Engine'>
|
||||
{isEditing ? (
|
||||
<Select
|
||||
value={
|
||||
normalizeAppUpdateEngine(draftSettings.appUpdateEngine) ||
|
||||
currentEngine
|
||||
}
|
||||
onChange={(value) =>
|
||||
setDraftSettings((previous) => ({
|
||||
...previous,
|
||||
appUpdateEngine: value
|
||||
}))
|
||||
}
|
||||
style={{ width: '100%' }}
|
||||
placeholder='Select an engine'
|
||||
>
|
||||
<Option value='native'>Native</Option>
|
||||
<Option value='chromium'>Chromium</Option>
|
||||
</Select>
|
||||
) : (
|
||||
<Text>{engineLabel(currentEngine)}</Text>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</InfoCollapse>
|
||||
)}
|
||||
)
|
||||
})}
|
||||
</Flex>
|
||||
</Spin>
|
||||
</div>
|
||||
</ScrollBox>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
@ -38,7 +38,8 @@ const createEmptyUserSettings = () => ({
|
||||
sortSidebarVisibility: {},
|
||||
columnVisibility: {},
|
||||
collapseState: {},
|
||||
pageLayout: {}
|
||||
pageLayout: {},
|
||||
appearance: {}
|
||||
})
|
||||
|
||||
const normalizeUserSettingsCategory = (category) =>
|
||||
@ -76,7 +77,8 @@ const normalizeUserSettings = (settings = {}) => ({
|
||||
),
|
||||
columnVisibility: normalizeUserSettingsCategory(settings?.columnVisibility),
|
||||
collapseState: normalizeUserSettingsCategory(settings?.collapseState),
|
||||
pageLayout: normalizeUserSettingsCategory(settings?.pageLayout)
|
||||
pageLayout: normalizeUserSettingsCategory(settings?.pageLayout),
|
||||
appearance: normalizeUserSettingsCategory(settings?.appearance)
|
||||
})
|
||||
|
||||
const emitWithAcknowledgement = (socket, eventName, data) =>
|
||||
|
||||
306
src/database/Settings.js
Normal file
306
src/database/Settings.js
Normal file
@ -0,0 +1,306 @@
|
||||
import PersonIcon from '../components/Icons/PersonIcon'
|
||||
import OpenAppIcon from '../components/Icons/OpenAppIcon'
|
||||
import SoftwareUpdateIcon from '../components/Icons/SoftwareUpdateIcon'
|
||||
|
||||
export const DEFAULT_DATE_TIME_FORMAT = 'MM/DD/YYYY HH:mm:ss'
|
||||
|
||||
const DATE_FORMAT_TOKENS = [
|
||||
'YYYY',
|
||||
'YY',
|
||||
'MMMM',
|
||||
'MMM',
|
||||
'MM',
|
||||
'M',
|
||||
'dddd',
|
||||
'ddd',
|
||||
'dd',
|
||||
'd',
|
||||
'DD',
|
||||
'Do',
|
||||
'D',
|
||||
'WW',
|
||||
'W',
|
||||
'ww',
|
||||
'w',
|
||||
'Q'
|
||||
]
|
||||
const TIME_FORMAT_TOKENS = [
|
||||
'HH',
|
||||
'H',
|
||||
'hh',
|
||||
'h',
|
||||
'mm',
|
||||
'm',
|
||||
'ss',
|
||||
's',
|
||||
'SSS',
|
||||
'SS',
|
||||
'S',
|
||||
'A',
|
||||
'a',
|
||||
'ZZ',
|
||||
'Z',
|
||||
'X',
|
||||
'x'
|
||||
]
|
||||
const FORMAT_TOKEN_PATTERN = new RegExp(
|
||||
`\\[[^\\]]*\\]|${[...DATE_FORMAT_TOKENS, ...TIME_FORMAT_TOKENS]
|
||||
.sort((left, right) => right.length - left.length)
|
||||
.join('|')}`,
|
||||
'g'
|
||||
)
|
||||
const DATE_FORMAT_TOKEN_SET = new Set(DATE_FORMAT_TOKENS)
|
||||
const TIME_FORMAT_TOKEN_SET = new Set(TIME_FORMAT_TOKENS)
|
||||
|
||||
const lastPartIndex = (parts, type) => {
|
||||
for (let index = parts.length - 1; index >= 0; index -= 1) {
|
||||
if (parts[index].type === type) return index
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
const extractFormatPart = (parts, type) => {
|
||||
const first = parts.findIndex((part) => part.type === type)
|
||||
const last = lastPartIndex(parts, type)
|
||||
if (first === -1) return ''
|
||||
return parts
|
||||
.slice(first, last + 1)
|
||||
.map((part) => part.value)
|
||||
.join('')
|
||||
.trim()
|
||||
}
|
||||
|
||||
export const splitDateTimeFormat = (
|
||||
format = DEFAULT_DATE_TIME_FORMAT
|
||||
) => {
|
||||
const source =
|
||||
typeof format === 'string' && format.trim()
|
||||
? format
|
||||
: DEFAULT_DATE_TIME_FORMAT
|
||||
const parts = []
|
||||
let cursor = 0
|
||||
|
||||
for (const match of source.matchAll(FORMAT_TOKEN_PATTERN)) {
|
||||
if (match.index > cursor) {
|
||||
parts.push({ type: 'sep', value: source.slice(cursor, match.index) })
|
||||
}
|
||||
|
||||
const token = match[0]
|
||||
if (token.startsWith('[')) {
|
||||
parts.push({ type: 'sep', value: token })
|
||||
} else if (DATE_FORMAT_TOKEN_SET.has(token)) {
|
||||
parts.push({ type: 'date', value: token })
|
||||
} else if (TIME_FORMAT_TOKEN_SET.has(token)) {
|
||||
parts.push({ type: 'time', value: token })
|
||||
} else {
|
||||
parts.push({ type: 'sep', value: token })
|
||||
}
|
||||
|
||||
cursor = match.index + token.length
|
||||
}
|
||||
|
||||
if (cursor < source.length) {
|
||||
parts.push({ type: 'sep', value: source.slice(cursor) })
|
||||
}
|
||||
|
||||
return {
|
||||
dateFormat: extractFormatPart(parts, 'date') || 'MM/DD/YYYY',
|
||||
timeFormat: extractFormatPart(parts, 'time') || 'HH:mm:ss'
|
||||
}
|
||||
}
|
||||
|
||||
export const USER_SETTING_NAMES = [
|
||||
'theme',
|
||||
'density',
|
||||
'showNavigationLabels',
|
||||
'dateTimeFormat',
|
||||
'timezone'
|
||||
]
|
||||
|
||||
export const APP_SETTING_NAMES = [
|
||||
'appTheme',
|
||||
'appShowNavigationLabels',
|
||||
'appUpdateBranch',
|
||||
'appUpdateEngine'
|
||||
]
|
||||
|
||||
const themeOptions = [
|
||||
{ label: 'Light', value: 'light' },
|
||||
{ label: 'Dark', value: 'dark' },
|
||||
{ label: 'System', value: 'system' }
|
||||
]
|
||||
|
||||
const settings = {
|
||||
sections: [
|
||||
{
|
||||
key: 'appearance',
|
||||
name: 'User Settings',
|
||||
visible: true,
|
||||
icon: PersonIcon,
|
||||
properties: [
|
||||
{
|
||||
name: 'theme',
|
||||
label: 'Theme',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: (_objectData, parentData) => [
|
||||
...themeOptions,
|
||||
...(parentData?.isElectron ? [{ label: 'App', value: 'app' }] : [])
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'density',
|
||||
label: 'UI Density',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
{ label: 'Comfortable', value: 'comfortable' },
|
||||
{ label: 'Compact', value: 'compact' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'showNavigationLabels',
|
||||
label: 'Navigation Labels',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
{ label: 'Show', value: true },
|
||||
{ label: 'Hide', value: false }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'dateTimeFormat',
|
||||
label: 'Date and Time Format',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: DEFAULT_DATE_TIME_FORMAT
|
||||
},
|
||||
{
|
||||
name: 'timezone',
|
||||
label: 'Timezone',
|
||||
type: 'text',
|
||||
required: true,
|
||||
defaultValue: 'UTC'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'app',
|
||||
name: 'App Settings',
|
||||
visible: ({ isElectron } = {}) => Boolean(isElectron),
|
||||
icon: OpenAppIcon,
|
||||
properties: [
|
||||
{
|
||||
name: 'appTheme',
|
||||
label: 'Theme',
|
||||
type: 'select',
|
||||
required: true,
|
||||
defaultValue: 'system',
|
||||
options: themeOptions
|
||||
},
|
||||
{
|
||||
name: 'appShowNavigationLabels',
|
||||
label: 'Navigation Labels',
|
||||
type: 'select',
|
||||
required: true,
|
||||
defaultValue: false,
|
||||
options: [
|
||||
{ label: 'Show', value: true },
|
||||
{ label: 'Hide', value: false }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'appUpdates',
|
||||
name: 'App Update Settings',
|
||||
visible: ({ isElectron } = {}) => Boolean(isElectron),
|
||||
icon: SoftwareUpdateIcon,
|
||||
column: 1,
|
||||
properties: [
|
||||
{
|
||||
name: 'appUpdateBranch',
|
||||
label: 'Branch',
|
||||
type: 'select',
|
||||
required: true,
|
||||
defaultValue: 'main',
|
||||
options: (_objectData, parentData) =>
|
||||
(parentData?.branches || []).map((branch) => ({
|
||||
label: branch,
|
||||
value: branch
|
||||
}))
|
||||
},
|
||||
{
|
||||
name: 'appUpdateEngine',
|
||||
label: 'Engine',
|
||||
type: 'select',
|
||||
required: true,
|
||||
defaultValue: 'native',
|
||||
options: [
|
||||
{ label: 'Native', value: 'native' },
|
||||
{ label: 'Chromium', value: 'chromium' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export const getVisibleSettingsSections = (context = {}) =>
|
||||
settings.sections.filter((section) => {
|
||||
if (typeof section.visible === 'function') {
|
||||
return section.visible(context)
|
||||
}
|
||||
return section.visible !== false
|
||||
})
|
||||
|
||||
export const getSettingsDefaults = (context = {}) => {
|
||||
const defaults = {}
|
||||
for (const section of getVisibleSettingsSections(context)) {
|
||||
for (const property of section.properties || []) {
|
||||
if (property.defaultValue !== undefined) {
|
||||
defaults[property.name] = property.defaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaults
|
||||
}
|
||||
|
||||
export const areSettingsValid = (values = {}, context = {}) =>
|
||||
getVisibleSettingsSections(context).every((section) =>
|
||||
(section.properties || []).every((property) => {
|
||||
if (!property.required) return true
|
||||
const value = values[property.name]
|
||||
return value !== undefined && value !== null && value !== ''
|
||||
})
|
||||
)
|
||||
|
||||
export const pickSettings = (values = {}, names = []) =>
|
||||
names.reduce((picked, name) => {
|
||||
if (values[name] !== undefined) {
|
||||
picked[name] = values[name]
|
||||
}
|
||||
return picked
|
||||
}, {})
|
||||
|
||||
export const getEffectiveAppearance = ({
|
||||
isElectron = false,
|
||||
userAppearance = {},
|
||||
electronSettings = {}
|
||||
} = {}) => {
|
||||
const overrideTheme = Boolean(isElectron && electronSettings.overrideTheme)
|
||||
const appTheme = electronSettings.appTheme || electronSettings.theme
|
||||
const appShowNavigationLabels =
|
||||
electronSettings.appShowNavigationLabels ??
|
||||
electronSettings.showNavigationLabels
|
||||
|
||||
return {
|
||||
theme: overrideTheme ? appTheme : userAppearance.theme,
|
||||
showNavigationLabels: isElectron
|
||||
? (appShowNavigationLabels ?? userAppearance.showNavigationLabels)
|
||||
: userAppearance.showNavigationLabels,
|
||||
density: userAppearance.density
|
||||
}
|
||||
}
|
||||
|
||||
export default settings
|
||||
Loading…
x
Reference in New Issue
Block a user