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>
This commit is contained in:
88
src/lib/settings/settingsUtils.ts
Normal file
88
src/lib/settings/settingsUtils.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { type CameraSystem, cameraSystems } from '../cameraControls'
|
||||
import { Themes } from '../theme'
|
||||
import { isTauri } from '../isTauri'
|
||||
import { getInitialDefaultDir, readSettingsFile } from '../tauriFS'
|
||||
import { initialSettings } from 'lib/settings/initialSettings'
|
||||
import {
|
||||
type BaseUnit,
|
||||
baseUnitsUnion,
|
||||
type Toggle,
|
||||
type SettingsMachineContext,
|
||||
toggleAsArray,
|
||||
UnitSystem,
|
||||
} from './settingsTypes'
|
||||
import { SETTINGS_PERSIST_KEY } from '../constants'
|
||||
|
||||
export const fallbackLoadedSettings = {
|
||||
settings: initialSettings,
|
||||
errors: [] as (keyof SettingsMachineContext)[],
|
||||
}
|
||||
|
||||
function isEnumMember<T extends Record<string, unknown>>(v: unknown, e: T) {
|
||||
return Object.values(e).includes(v)
|
||||
}
|
||||
|
||||
export async function loadAndValidateSettings(): Promise<
|
||||
ReturnType<typeof validateSettings>
|
||||
> {
|
||||
const fsSettings = isTauri() ? await readSettingsFile() : {}
|
||||
const localStorageSettings = JSON.parse(
|
||||
localStorage?.getItem(SETTINGS_PERSIST_KEY) || '{}'
|
||||
)
|
||||
const mergedSettings = Object.assign({}, localStorageSettings, fsSettings)
|
||||
|
||||
return await validateSettings(mergedSettings)
|
||||
}
|
||||
|
||||
const settingsValidators: Record<
|
||||
keyof SettingsMachineContext,
|
||||
(v: unknown) => boolean
|
||||
> = {
|
||||
baseUnit: (v) => baseUnitsUnion.includes(v as BaseUnit),
|
||||
cameraControls: (v) => cameraSystems.includes(v as CameraSystem),
|
||||
defaultDirectory: (v) =>
|
||||
typeof v === 'string' && (v.length > 0 || !isTauri()),
|
||||
defaultProjectName: (v) => typeof v === 'string' && v.length > 0,
|
||||
onboardingStatus: (v) => typeof v === 'string',
|
||||
showDebugPanel: (v) => typeof v === 'boolean',
|
||||
textWrapping: (v) => toggleAsArray.includes(v as Toggle),
|
||||
theme: (v) => isEnumMember(v, Themes),
|
||||
unitSystem: (v) => isEnumMember(v, UnitSystem),
|
||||
}
|
||||
|
||||
function removeInvalidSettingsKeys(s: Record<string, unknown>) {
|
||||
const validKeys = Object.keys(initialSettings)
|
||||
for (const key in s) {
|
||||
if (!validKeys.includes(key)) {
|
||||
console.warn(`Invalid key found in settings: ${key}`)
|
||||
delete s[key]
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
export async function validateSettings(s: Record<string, unknown>) {
|
||||
let settingsNoInvalidKeys = removeInvalidSettingsKeys({ ...s })
|
||||
let errors: (keyof SettingsMachineContext)[] = []
|
||||
for (const key in settingsNoInvalidKeys) {
|
||||
const k = key as keyof SettingsMachineContext
|
||||
if (!settingsValidators[k](settingsNoInvalidKeys[k])) {
|
||||
delete settingsNoInvalidKeys[k]
|
||||
errors.push(k)
|
||||
}
|
||||
}
|
||||
|
||||
// Here's our chance to insert the fallback defaultDir
|
||||
const defaultDirectory = isTauri() ? await getInitialDefaultDir() : ''
|
||||
|
||||
const settings = Object.assign(
|
||||
initialSettings,
|
||||
{ defaultDirectory },
|
||||
settingsNoInvalidKeys
|
||||
) as SettingsMachineContext
|
||||
|
||||
return {
|
||||
settings,
|
||||
errors,
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user