Files
modeling-app/src/machines/settingsMachine.ts
Frank Noirot f40cdabfdf File based settings (#1679)
* Rename GlobalStateContext to SettingsAuthContext

* Naive initial impl of settings persistence to file system

* Update app identifier in tauri config

* Add "show in folder" tauri command

* Load from and save to file system in Tauri app

* Add documents drive to tauri permission scope

* Add recursive prop to default dir selection dialog

* Add success toast to web restore defaults action

* Add a way to validate read-in settings

* Update imports to use separate settings lib file

* Validate localStorage-loaded settings, combine error message

* Add a e2e test for validation

* Clean up state state bugs

* Reverse validation looping so new users don't error

* update settingsMachine typegen to remove conflicts

* Fmt

* Fix TS errors

* Fix import paths, etc post-merge

* Make default length units `mm` and 'metric'

* Rename to SettingsAuth*

* cargo fmt

* Revert Tauri config identifier change

* Update clientSideInfra's baseUnits from settings

* Break apart CommandBar and CommandBarProvider

* Bugfix: don't validate defaultValue when it's not configured

* Allow some TauriFS functions to no-op from browser

* Sidestep circular deps by loading context and kclManager only from React-land

* Update broken import paths

* Separate loaders from Router, load settings on every route

* Break apart settings types, utils, and constants

* Fix Jest tests by decoupling reliance on useLoaderData from SettingsAuthProvider

* Fix up Router loader data with "layout routes"
https://reactrouter.com/en/main/route/route#layout-routes

* Move settings validation and toast to custom hook so the toast renders

* fmt

* Use forks for Vitest
https://vitest.dev/guide/common-errors.html#failed-to-terminate-worker

* $APPCONFIG !== $APPDATA only on Linux
+ change the identifier back since it really doesn't seem to affect app signing

* Debugging on Linux

* Better directory validation, fix reset settings button

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* defaultDirectory can be empty in browser

* fmt

* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)

* re-trigger CI

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-03-14 15:56:45 -04:00

197 lines
6.5 KiB
TypeScript

import { assign, createMachine } from 'xstate'
import { Themes, getSystemTheme, setThemeClass } from 'lib/theme'
import { CameraSystem } from 'lib/cameraControls'
import { isTauri } from 'lib/isTauri'
import { writeToSettingsFile } from 'lib/tauriFS'
import { DEFAULT_PROJECT_NAME, SETTINGS_PERSIST_KEY } from 'lib/constants'
import {
UnitSystem,
type BaseUnit,
type SettingsMachineContext,
type Toggle,
} from 'lib/settings/settingsTypes'
export const settingsMachine = createMachine(
{
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlTQAIBBY4qyrXWAbQAYBdRUABwHtYmLP2w8QAD0TsANCACe0gL5K5THHkIlylKgCEAhrDBUAqtmEduSEAKEixNqQgCM7AJz52AJgAsAVg8AZgAOEIA2fxd3XzlFBCCXf3xfdlS0kN9vGIiVNXRmTSJSCnQqAGEDAFswACcDCtE0Wv5iNi5xO2FMUXFnWQVlVRB1Fi0S3QARMAAzAwBXYmpJzFqwAGM0flr5K07Bbt6nRH9w-HcXcPcI8PYAdgu0oLiTu+98EPdQ0-8g8N8gu53HkRgUNARijoytM5otqAAFFoAKw21AActUwHsbF0HH1EFkvOxiSTScSXLFBggrsk7r4AuEQuxAd4oiEQaMitpStQAPLYABG-AMtQgGkYaAMaHm7WsfAOeOOCEC+HCiTevlu5JcYReCBCLhSFzc3m8SWJrJcHLBY0hPKoABUwBJqAB1eq8XgabHy+w9Rygfp69jWjDg8ZQ6gOgAWYBqPtsCv9+P17Hw3juIV+Pn87kiGeeVINIXwuf8rPC4WiVZcQVDhQh3N05mEjHksDQcYTuOTSrp+Du5ZC3g8bizbkp8QCaaelwep3YTP8vnr4btDv4UCgpCo0wF8ygVHhBmwYGI3aTR0DiFupfY-giQSC3iflZfepHvnwQV8Lge93cX4qxCO4VGGbB+AgOBxE5eAcUvANJEQABaXwQj1ZCQLvUkmXpFwzStYZYIjfY-SvJDXBHLxa01Stc0yIE7j1NwKW-NUAl8a4-DuZkwKUIA */
id: 'Settings',
predictableActionArguments: true,
context: {} as SettingsMachineContext,
initial: 'idle',
states: {
idle: {
entry: ['setThemeClass', 'setClientSideSceneUnits', 'persistSettings'],
on: {
'Set All Settings': {
actions: [
assign((context, event) => {
return {
...context,
...event.data,
}
}),
'persistSettings',
'setThemeClass',
],
target: 'idle',
internal: true,
},
'Set Base Unit': {
actions: [
assign({
baseUnit: (_, event) => event.data.baseUnit,
}),
'persistSettings',
'toastSuccess',
'setClientSideSceneUnits',
'Execute AST',
],
target: 'idle',
internal: true,
},
'Set Camera Controls': {
actions: [
assign({
cameraControls: (_, event) => event.data.cameraControls,
}),
'persistSettings',
'toastSuccess',
],
target: 'idle',
internal: true,
},
'Set Default Directory': {
actions: [
assign({
defaultDirectory: (_, event) => event.data.defaultDirectory,
}),
'persistSettings',
'toastSuccess',
],
target: 'idle',
internal: true,
},
'Set Default Project Name': {
actions: [
assign({
defaultProjectName: (_, event) =>
event.data.defaultProjectName.trim() || DEFAULT_PROJECT_NAME,
}),
'persistSettings',
'toastSuccess',
],
target: 'idle',
internal: true,
},
'Set Onboarding Status': {
actions: [
assign({
onboardingStatus: (_, event) => event.data.onboardingStatus,
}),
'persistSettings',
],
target: 'idle',
internal: true,
},
'Set Text Wrapping': {
actions: [
assign({
textWrapping: (_, event) => event.data.textWrapping,
}),
'persistSettings',
'toastSuccess',
],
target: 'idle',
internal: true,
},
'Set Theme': {
actions: [
assign({
theme: (_, event) => event.data.theme,
}),
'persistSettings',
'toastSuccess',
'setThemeClass',
],
target: 'idle',
internal: true,
},
'Set Unit System': {
actions: [
assign({
unitSystem: (_, event) => event.data.unitSystem,
baseUnit: (_, event) =>
event.data.unitSystem === 'imperial' ? 'in' : 'mm',
}),
'persistSettings',
'toastSuccess',
'Execute AST',
],
target: 'idle',
internal: true,
},
'Toggle Debug Panel': {
actions: [
assign({
showDebugPanel: (context) => {
return !context.showDebugPanel
},
}),
'persistSettings',
'toastSuccess',
],
target: 'idle',
internal: true,
},
},
},
},
tsTypes: {} as import('./settingsMachine.typegen').Typegen0,
schema: {
events: {} as
| { type: 'Set All Settings'; data: Partial<SettingsMachineContext> }
| { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } }
| {
type: 'Set Camera Controls'
data: { cameraControls: CameraSystem }
}
| { type: 'Set Default Directory'; data: { defaultDirectory: string } }
| {
type: 'Set Default Project Name'
data: { defaultProjectName: string }
}
| { type: 'Set Onboarding Status'; data: { onboardingStatus: string } }
| { type: 'Set Text Wrapping'; data: { textWrapping: Toggle } }
| { type: 'Set Theme'; data: { theme: Themes } }
| {
type: 'Set Unit System'
data: { unitSystem: UnitSystem }
}
| { type: 'Toggle Debug Panel' },
},
},
{
actions: {
persistSettings: (context) => {
if (isTauri()) {
writeToSettingsFile(context).catch((err) => {
console.error('Error writing settings:', err)
})
}
try {
localStorage.setItem(SETTINGS_PERSIST_KEY, JSON.stringify(context))
} catch (e) {
console.error(e)
}
},
setThemeClass: (context, event) => {
const currentTheme =
event.type === 'Set Theme' ? event.data.theme : context.theme
setThemeClass(
currentTheme === Themes.System ? getSystemTheme() : currentTheme
)
},
},
}
)