diff --git a/packages/preload/src/modules/config.ts b/packages/preload/src/modules/config.ts index 219edfe..66941fd 100644 --- a/packages/preload/src/modules/config.ts +++ b/packages/preload/src/modules/config.ts @@ -3,19 +3,25 @@ import path from 'path'; import { produce, type WritableDraft } from 'immer'; import type { Config } from '/shared/types/config'; +import { DEFAULT_DEPENDENCY_UPDATE_STRATEGY } from '/shared/types/settings'; import { invoke } from './invoke'; +import { getDefaultScenesPath } from './settings'; const CONFIG_FILE_NAME = 'config.json'; let config: Config | undefined; -function getDefaultConfig(): Config { +async function getDefaultConfig(): Promise { return { version: 1, workspace: { paths: [], }, + settings: { + scenesPath: await getDefaultScenesPath(), + dependencyUpdateStrategy: DEFAULT_DEPENDENCY_UPDATE_STRATEGY, + }, }; } @@ -28,7 +34,7 @@ export async function getConfigPath(): Promise { const appHome = await invoke('electron.getAppHome'); try { await fs.stat(appHome); - } catch (error) { + } catch (_) { await fs.mkdir(appHome); } return path.join(appHome, CONFIG_FILE_NAME); @@ -48,7 +54,7 @@ export async function getConfig(): Promise> { config = JSON.parse(await fs.readFile(configPath, 'utf-8')) as Config; } catch (_) { try { - await writeConfig(getDefaultConfig()); + await writeConfig(await getDefaultConfig()); } catch (e) { console.error('[Preload] Failed initializing config file', e); } diff --git a/packages/preload/src/modules/settings.ts b/packages/preload/src/modules/settings.ts index 45c5fc5..8d14394 100644 --- a/packages/preload/src/modules/settings.ts +++ b/packages/preload/src/modules/settings.ts @@ -2,31 +2,33 @@ import { DEPENDENCY_UPDATE_STRATEGY, DEFAULT_DEPENDENCY_UPDATE_STRATEGY, } from '/shared/types/settings'; +import type { AppSettings } from '/shared/types/settings'; + import { invoke } from './invoke'; import { getConfig, setConfig } from './config'; -export async function getScenesPath() { - const config = await getConfig(); - return config.scenesPath ?? (await invoke('electron.getAppHome')); +export function getDefaultScenesPath() { + return invoke('electron.getAppHome'); } -export async function setScenesPath(path: string) { - await setConfig(config => (config.scenesPath = path)); +export async function getScenesPath() { + const config = await getConfig(); + return config.settings.scenesPath ?? (await getDefaultScenesPath()); } -// Helper to check if a value is part of the enum -function isValidUpdateStrategy(value?: string): value is DEPENDENCY_UPDATE_STRATEGY { +export function isValidUpdateStrategy(value?: string): value is DEPENDENCY_UPDATE_STRATEGY { return Object.values(DEPENDENCY_UPDATE_STRATEGY).includes(value as DEPENDENCY_UPDATE_STRATEGY); } export async function getUpdateDependenciesStrategy() { - const { updateDependenciesStrategy } = await getConfig(); - if (isValidUpdateStrategy(updateDependenciesStrategy)) return updateDependenciesStrategy; + const { dependencyUpdateStrategy } = (await getConfig()).settings; + if (isValidUpdateStrategy(dependencyUpdateStrategy)) return dependencyUpdateStrategy; return DEFAULT_DEPENDENCY_UPDATE_STRATEGY; } -export async function setUpdateDependenciesStrategy(strategy: DEPENDENCY_UPDATE_STRATEGY) { - await setConfig(config => (config.updateDependenciesStrategy = strategy)); +export async function updateAppSettings(settings: AppSettings) { + // update app settings on config file + await setConfig(config => (config.settings = settings)); } export async function selectSceneFolder(): Promise { diff --git a/packages/preload/src/modules/workspace.ts b/packages/preload/src/modules/workspace.ts index 936e242..6828e72 100644 --- a/packages/preload/src/modules/workspace.ts +++ b/packages/preload/src/modules/workspace.ts @@ -6,9 +6,10 @@ import { shell } from 'electron'; import equal from 'fast-deep-equal'; import { type DependencyState, SortBy, type Project } from '/shared/types/projects'; -import type { Template, Workspace } from '/shared/types/workspace'; -import { FileSystemStorage } from '/shared/types/storage'; import { PACKAGES_LIST } from '/shared/types/pkg'; +import { DEFAULT_DEPENDENCY_UPDATE_STRATEGY } from '/shared/types/settings'; +import { FileSystemStorage } from '/shared/types/storage'; +import type { Template, Workspace } from '/shared/types/workspace'; import { getConfig, setConfig } from './config'; import { exists, writeFile as deepWriteFile } from './fs'; @@ -17,7 +18,7 @@ import { getRowsAndCols, parseCoords } from './scene'; import { getEditorHome } from './editor'; import { invoke } from './invoke'; import { getOudatedDeps } from './npm'; -import { getUpdateDependenciesStrategy, getScenesPath } from './settings'; +import { getDefaultScenesPath, getScenesPath } from './settings'; import { DEFAULT_THUMBNAIL, NEW_SCENE_NAME, EMPTY_SCENE_TEMPLATE_REPO } from './constants'; @@ -202,9 +203,10 @@ export async function getTemplates(): Promise { */ export async function getWorkspace(): Promise { const config = await getConfig(); - const [projects, missing] = await getProjects(config.workspace.paths); - const templates = await getTemplates(); - const updateStrategySetting = await getUpdateDependenciesStrategy(); + const [[projects, missing], templates] = await Promise.all([ + getProjects(config.workspace.paths), + getTemplates(), + ]); return { sortBy: SortBy.NEWEST, // TODO: read from editor config file... @@ -212,7 +214,11 @@ export async function getWorkspace(): Promise { missing, templates, settings: { - dependencyUpdateStrategy: updateStrategySetting, + ...config.settings, + // TODO: implement migrations for config file... + dependencyUpdateStrategy: + config.settings?.dependencyUpdateStrategy ?? DEFAULT_DEPENDENCY_UPDATE_STRATEGY, + scenesPath: config.settings?.scenesPath ?? (await getDefaultScenesPath()), }, }; } diff --git a/packages/renderer/src/components/Modals/AppSettings/component.tsx b/packages/renderer/src/components/Modals/AppSettings/component.tsx index 8e62abe..1151699 100644 --- a/packages/renderer/src/components/Modals/AppSettings/component.tsx +++ b/packages/renderer/src/components/Modals/AppSettings/component.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { Box, Button, @@ -14,69 +14,53 @@ import { import CloseIcon from '@mui/icons-material/Close'; import FolderIcon from '@mui/icons-material/Folder'; import { Modal } from 'decentraland-ui2/dist/components/Modal/Modal'; +import equal from 'fast-deep-equal'; + import { settings as settingsPreload } from '#preload'; + import { DEPENDENCY_UPDATE_STRATEGY } from '/shared/types/settings'; + import { t } from '/@/modules/store/translation/utils'; import { useSettings } from '/@/hooks/useSettings'; import './styles.css'; export function AppSettings({ open, onClose }: { open: boolean; onClose: () => void }) { - const settings = useSettings(); - const [scenesPath, setScenesPath] = useState(''); - const [updateDependenciesStrategy, setUpdateDependenciesStrategy] = - useState(DEPENDENCY_UPDATE_STRATEGY.NOTIFY); - const [isDirty, setIsDirty] = useState(false); + const { settings: _settings, updateAppSettings } = useSettings(); + const [settings, setSettings] = useState(_settings); useEffect(() => { - const checkIfDirty = async () => { - const scenesPathSetting = settings.scenesPath; - const updateStrategySetting = settings.updateDependenciesStrategy; - - setIsDirty( - scenesPath !== scenesPathSetting || updateDependenciesStrategy !== updateStrategySetting, - ); - }; + if (!equal(_settings, settings)) setSettings(_settings); + }, [_settings]); - checkIfDirty(); - }, [scenesPath, updateDependenciesStrategy]); - - const handleChangeSceneFolder = useCallback((event: React.ChangeEvent) => { - setScenesPath(event.target.value); - }, []); - - const handleClickChangeSceneFolder = useCallback(() => { - settings.setScenesPath(scenesPath); - onClose(); - }, [scenesPath]); + const handleChangeSceneFolder = useCallback( + (event: React.ChangeEvent) => { + setSettings({ ...settings, scenesPath: event.target.value }); + }, + [settings], + ); const handleChangeUpdateDependenciesStrategy = useCallback( (event: React.ChangeEvent) => { - const value = event.target.value as DEPENDENCY_UPDATE_STRATEGY; - setUpdateDependenciesStrategy(value); + setSettings({ + ...settings, + dependencyUpdateStrategy: event.target.value as DEPENDENCY_UPDATE_STRATEGY, + }); }, - [], + [settings], ); - const handleClickApply = useCallback(async () => { - await settings.setScenesPath(scenesPath); - await settings.setUpdateDependenciesStrategy(updateDependenciesStrategy); + const handleClickApply = useCallback(() => { + updateAppSettings(settings); onClose(); - }, [scenesPath, updateDependenciesStrategy]); + }, [settings, updateAppSettings]); const handleOpenFolder = useCallback(async () => { const folder = await settingsPreload.selectSceneFolder(); - if (folder) { - setScenesPath(folder); - } - }, []); + if (folder) setSettings({ ...settings, scenesPath: folder }); + }, [settings]); - useEffect(() => { - const path = settings.scenesPath; - const strategy = settings.updateDependenciesStrategy; - setScenesPath(path); - setUpdateDependenciesStrategy(strategy); - }, [settings.scenesPath, settings.updateDependenciesStrategy]); + const isDirty = useMemo(() => !equal(_settings, settings), [settings, _settings]); return ( v @@ -112,22 +96,13 @@ export function AppSettings({ open, onClose }: { open: boolean; onClose: () => v } /> - {t('modal.app_settings.fields.scene_editor_dependencies.label')} { const updateProject = useCallback( (updatedProject: Project) => { if (!project || !updatedProject || project.path !== updatedProject.path) return; - dispatch(workspaceActions.updateProject(updatedProject)); }, [workspaceActions.updateProject, project], diff --git a/packages/renderer/src/hooks/useSettings.ts b/packages/renderer/src/hooks/useSettings.ts index 69ab1d4..f247b5f 100644 --- a/packages/renderer/src/hooks/useSettings.ts +++ b/packages/renderer/src/hooks/useSettings.ts @@ -1,40 +1,21 @@ -import { useCallback, useEffect, useState } from 'react'; -import { settings } from '#preload'; -import { DEPENDENCY_UPDATE_STRATEGY } from '/shared/types/settings'; +import { useCallback } from 'react'; -export const useSettings = () => { - const [scenesPath, setScenesPath] = useState(''); - const [updateDependenciesStrategy, setUpdateDependenciesStrategy] = - useState(DEPENDENCY_UPDATE_STRATEGY.NOTIFY); +import { useDispatch, useSelector } from '#store'; - const handleUpdateScenesPath = useCallback(async (path: string) => { - await settings.setScenesPath(path); - setScenesPath(path); - }, []); +import { type AppSettings } from '/shared/types/settings'; - const handleUpdateDependenciesStrategy = useCallback( - async (strategy: DEPENDENCY_UPDATE_STRATEGY) => { - await settings.setUpdateDependenciesStrategy(strategy); - setUpdateDependenciesStrategy(strategy); - }, - [], - ); +import { actions as workspaceActions } from '/@/modules/store/workspace'; - useEffect(() => { - async function getAppSettings() { - const path = await settings.getScenesPath(); - const strategy = await settings.getUpdateDependenciesStrategy(); - setScenesPath(path); - setUpdateDependenciesStrategy(strategy); - } +export const useSettings = () => { + const dispatch = useDispatch(); + const { settings } = useSelector(state => state.workspace); - getAppSettings(); + const updateAppSettings = useCallback((settings: AppSettings) => { + dispatch(workspaceActions.updateSettings(settings)); }, []); return { - scenesPath, - updateDependenciesStrategy, - setScenesPath: handleUpdateScenesPath, - setUpdateDependenciesStrategy: handleUpdateDependenciesStrategy, + settings, + updateAppSettings, }; }; diff --git a/packages/renderer/src/modules/store/workspace/slice.ts b/packages/renderer/src/modules/store/workspace/slice.ts index cd7f021..f13fe0c 100644 --- a/packages/renderer/src/modules/store/workspace/slice.ts +++ b/packages/renderer/src/modules/store/workspace/slice.ts @@ -1,7 +1,7 @@ import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; import { type Project, SortBy } from '/shared/types/projects'; -import { DEPENDENCY_UPDATE_STRATEGY } from '/shared/types/settings'; +import { DEFAULT_DEPENDENCY_UPDATE_STRATEGY } from '/shared/types/settings'; import { type Workspace } from '/shared/types/workspace'; import type { Async } from '/@/modules/async'; @@ -14,7 +14,8 @@ const initialState: Async = { missing: [], templates: [], settings: { - dependencyUpdateStrategy: DEPENDENCY_UPDATE_STRATEGY.NOTIFY, + scenesPath: '', + dependencyUpdateStrategy: DEFAULT_DEPENDENCY_UPDATE_STRATEGY, }, status: 'idle', error: null, @@ -164,7 +165,10 @@ export const slice = createSlice({ state.projects[projectIdx] = project; } }, - ); + ) + .addCase(thunks.updateSettings.fulfilled, (state, { meta }) => { + state.settings = meta.arg; + }); }, }); diff --git a/packages/renderer/src/modules/store/workspace/thunks.ts b/packages/renderer/src/modules/store/workspace/thunks.ts index 8146aff..99a4713 100644 --- a/packages/renderer/src/modules/store/workspace/thunks.ts +++ b/packages/renderer/src/modules/store/workspace/thunks.ts @@ -1,9 +1,9 @@ -import { fs, npm, workspace } from '#preload'; +import { fs, npm, settings, workspace } from '#preload'; import { createAsyncThunk } from '/@/modules/store/thunk'; import { type Project } from '/shared/types/projects'; -import { type DEPENDENCY_UPDATE_STRATEGY } from '/shared/types/settings'; +import type { DEPENDENCY_UPDATE_STRATEGY } from '/shared/types/settings'; import { WorkspaceError } from '/shared/types/workspace'; import { actions } from './index'; @@ -99,3 +99,4 @@ export const runProject = createAsyncThunk( return updatedProject; }, ); +export const updateSettings = createAsyncThunk('config/updateSettings', settings.updateAppSettings); diff --git a/packages/shared/types/config.ts b/packages/shared/types/config.ts index ef544a1..553016f 100644 --- a/packages/shared/types/config.ts +++ b/packages/shared/types/config.ts @@ -1,13 +1,9 @@ -import { type DEPENDENCY_UPDATE_STRATEGY } from './settings'; +import { type AppSettings } from './settings'; export type Config = { version: number; workspace: { paths: string[]; }; -} & AppSettings; - -export type AppSettings = { - scenesPath?: string; - updateDependenciesStrategy?: DEPENDENCY_UPDATE_STRATEGY; + settings: AppSettings; }; diff --git a/packages/shared/types/settings.ts b/packages/shared/types/settings.ts index 9406420..b29ae7c 100644 --- a/packages/shared/types/settings.ts +++ b/packages/shared/types/settings.ts @@ -6,6 +6,7 @@ export enum DEPENDENCY_UPDATE_STRATEGY { export const DEFAULT_DEPENDENCY_UPDATE_STRATEGY = DEPENDENCY_UPDATE_STRATEGY.NOTIFY; -export type Settings = { +export type AppSettings = { + scenesPath: string; dependencyUpdateStrategy: DEPENDENCY_UPDATE_STRATEGY; }; diff --git a/packages/shared/types/workspace.ts b/packages/shared/types/workspace.ts index 6aae029..a18d152 100644 --- a/packages/shared/types/workspace.ts +++ b/packages/shared/types/workspace.ts @@ -1,13 +1,13 @@ import { ErrorBase } from './error'; import type { Project, SortBy } from './projects'; -import type { Settings } from './settings'; +import type { AppSettings } from './settings'; export type Workspace = { sortBy: SortBy; projects: Project[]; missing: string[]; templates: Template[]; - settings: Settings; + settings: AppSettings; }; export type Template = {