Revert "File based settings (#1361)" (#1435)

This reverts commit 602e7afef6.
This commit is contained in:
Frank Noirot
2024-02-16 09:09:58 -05:00
committed by GitHub
parent 900e3b96ad
commit f00ee3a44a
32 changed files with 167 additions and 593 deletions

View File

@ -3,7 +3,6 @@ import { secrets } from './secrets'
import { getUtils } from './test-utils' import { getUtils } from './test-utils'
import waitOn from 'wait-on' import waitOn from 'wait-on'
import { Themes } from '../../src/lib/theme' import { Themes } from '../../src/lib/theme'
import { initialSettings } from '../../src/lib/settings'
/* /*
debug helper: unfortunately we do rely on exact coord mouse clicks in a few places debug helper: unfortunately we do rely on exact coord mouse clicks in a few places
@ -387,53 +386,6 @@ test('Auto complete works', async ({ page }) => {
|> xLine(5, %)`) |> xLine(5, %)`)
}) })
// Stored settings validation test
test('Stored settings are validated and fall back to defaults', async ({
page,
context,
}) => {
// Override beforeEach test setup
// with corrupted settings
await context.addInitScript(async () => {
const storedSettings = JSON.parse(
localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}'
)
// Corrupt the settings
storedSettings.baseUnit = 'invalid'
storedSettings.cameraControls = `() => alert('hack the planet')`
storedSettings.defaultDirectory = 123
storedSettings.defaultProjectName = false
localStorage.setItem('SETTINGS_PERSIST_KEY', JSON.stringify(storedSettings))
})
await page.setViewportSize({ width: 1200, height: 500 })
await page.goto('/', { waitUntil: 'domcontentloaded' })
// Check the toast appeared
await expect(
page.getByText(`Error validating persisted settings:`, { exact: false })
).toBeVisible()
// Check the settings were reset
const storedSettings = JSON.parse(
await page.evaluate(
() => localStorage.getItem('SETTINGS_PERSIST_KEY') || '{}'
)
)
await expect(storedSettings.baseUnit).toBe(initialSettings.baseUnit)
await expect(storedSettings.cameraControls).toBe(
initialSettings.cameraControls
)
await expect(storedSettings.defaultDirectory).toBe(
initialSettings.defaultDirectory
)
await expect(storedSettings.defaultProjectName).toBe(
initialSettings.defaultProjectName
)
})
// Onboarding tests // Onboarding tests
test('Onboarding redirects and code updating', async ({ page, context }) => { test('Onboarding redirects and code updating', async ({ page, context }) => {
const u = getUtils(page) const u = getUtils(page)

View File

@ -143,25 +143,6 @@ async fn get_user(
Ok(user_info) Ok(user_info)
} }
/// Open the selected path in the system file manager.
/// From this GitHub comment: https://github.com/tauri-apps/tauri/issues/4062#issuecomment-1338048169
/// But with the Linux support removed since we don't need it for now.
#[tauri::command]
fn show_in_folder(path: String) {
#[cfg(target_os = "windows")]
{
Command::new("explorer")
.args(["/select,", &path]) // The comma after select is not a typo
.spawn()
.unwrap();
}
#[cfg(target_os = "macos")]
{
Command::new("open").args(["-R", &path]).spawn().unwrap();
}
}
fn main() { fn main() {
tauri::Builder::default() tauri::Builder::default()
.setup(|_app| { .setup(|_app| {
@ -178,8 +159,7 @@ fn main() {
get_user, get_user,
login, login,
read_toml, read_toml,
read_txt_file, read_txt_file
show_in_folder,
]) ])
.plugin(tauri_plugin_fs_extra::init()) .plugin(tauri_plugin_fs_extra::init())
.run(tauri::generate_context!()) .run(tauri::generate_context!())

View File

@ -23,8 +23,7 @@
"fs": { "fs": {
"scope": [ "scope": [
"$HOME/**/*", "$HOME/**/*",
"$APPDATA/**/*", "$APPDATA/**/*"
"$DOCUMENT/**/*"
], ],
"all": true "all": true
}, },
@ -61,7 +60,7 @@
"icons/icon.icns", "icons/icon.icns",
"icons/icon.ico" "icons/icon.ico"
], ],
"identifier": "zoo-modeling-app", "identifier": "io.kittycad.modeling-app",
"longDescription": "", "longDescription": "",
"macOS": { "macOS": {
"entitlements": null, "entitlements": null,

View File

@ -22,7 +22,7 @@ import { getNormalisedCoordinates } from './lib/utils'
import { useLoaderData, useNavigate } from 'react-router-dom' import { useLoaderData, useNavigate } from 'react-router-dom'
import { type IndexLoaderData } from 'lib/types' import { type IndexLoaderData } from 'lib/types'
import { paths } from 'lib/paths' import { paths } from 'lib/paths'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { CodeMenu } from 'components/CodeMenu' import { CodeMenu } from 'components/CodeMenu'
import { TextEditor } from 'components/TextEditor' import { TextEditor } from 'components/TextEditor'
@ -53,7 +53,7 @@ export function App() {
streamDimensions: s.streamDimensions, streamDimensions: s.streamDimensions,
})) }))
const { settings } = useSettingsAuthContext() const { settings } = useGlobalStateContext()
const { showDebugPanel, onboardingStatus, theme } = settings?.context || {} const { showDebugPanel, onboardingStatus, theme } = settings?.context || {}
const { state, send } = useModelingContext() const { state, send } = useModelingContext()

View File

@ -1,9 +1,9 @@
import Loading from './components/Loading' import Loading from './components/Loading'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
// Wrapper around protected routes, used in src/Router.tsx // Wrapper around protected routes, used in src/Router.tsx
export const Auth = ({ children }: React.PropsWithChildren) => { export const Auth = ({ children }: React.PropsWithChildren) => {
const { auth } = useSettingsAuthContext() const { auth } = useGlobalStateContext()
const isLoggingIn = auth?.state.matches('checkIfLoggedIn') const isLoggingIn = auth?.state.matches('checkIfLoggedIn')
return isLoggingIn ? ( return isLoggingIn ? (

View File

@ -29,9 +29,11 @@ import {
import { metadata } from 'tauri-plugin-fs-extra-api' import { metadata } from 'tauri-plugin-fs-extra-api'
import DownloadAppBanner from './components/DownloadAppBanner' import DownloadAppBanner from './components/DownloadAppBanner'
import { WasmErrBanner } from './components/WasmErrBanner' import { WasmErrBanner } from './components/WasmErrBanner'
import { SettingsAuthStateProvider } from './components/SettingsAuthStateProvider' import { GlobalStateProvider } from './components/GlobalStateProvider'
import { settingsMachine } from './machines/settingsMachine' import {
import { SETTINGS_PERSIST_KEY } from 'lib/settings' SETTINGS_PERSIST_KEY,
settingsMachine,
} from './machines/settingsMachine'
import { ContextFrom } from 'xstate' import { ContextFrom } from 'xstate'
import CommandBarProvider from 'components/CommandBar/CommandBar' import CommandBarProvider from 'components/CommandBar/CommandBar'
import { TEST, VITE_KC_SENTRY_DSN } from './env' import { TEST, VITE_KC_SENTRY_DSN } from './env'
@ -89,9 +91,7 @@ const addGlobalContextToElements = (
...route, ...route,
element: ( element: (
<CommandBarProvider> <CommandBarProvider>
<SettingsAuthStateProvider> <GlobalStateProvider>{route.element}</GlobalStateProvider>
{route.element}
</SettingsAuthStateProvider>
</CommandBarProvider> </CommandBarProvider>
), ),
} }
@ -229,42 +229,32 @@ const router = createBrowserRouter(
const projectDir = await initializeProjectDirectory( const projectDir = await initializeProjectDirectory(
persistedSettings.defaultDirectory || '' persistedSettings.defaultDirectory || ''
) )
let newDefaultDirectory: string | undefined = undefined let newDefaultDirectory: string | undefined = undefined
if (projectDir.path) { if (projectDir !== persistedSettings.defaultDirectory) {
if (projectDir.path !== persistedSettings.defaultDirectory) { localStorage.setItem(
localStorage.setItem( SETTINGS_PERSIST_KEY,
SETTINGS_PERSIST_KEY, JSON.stringify({
JSON.stringify({ ...persistedSettings,
...persistedSettings, defaultDirectory: projectDir,
defaultDirectory: projectDir, })
})
)
newDefaultDirectory = projectDir.path
}
const projectsNoMeta = (await readDir(projectDir.path)).filter(
isProjectDirectory
)
const projects = await Promise.all(
projectsNoMeta.map(async (p: FileEntry) => ({
entrypointMetadata: await metadata(
p.path + sep + PROJECT_ENTRYPOINT
),
...p,
}))
) )
newDefaultDirectory = projectDir
}
const projectsNoMeta = (await readDir(projectDir)).filter(
isProjectDirectory
)
const projects = await Promise.all(
projectsNoMeta.map(async (p: FileEntry) => ({
entrypointMetadata: await metadata(
p.path + sep + PROJECT_ENTRYPOINT
),
...p,
}))
)
return { return {
projects, projects,
newDefaultDirectory, newDefaultDirectory,
error: projectDir.error,
}
} else {
return {
projects: [],
newDefaultDirectory,
error: projectDir.error,
}
} }
}, },
children: [ children: [

View File

@ -2,7 +2,7 @@ import { useRef, useEffect, useState } from 'react'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
import { cameraMouseDragGuards } from 'lib/cameraControls' import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useStore } from 'useStore' import { useStore } from 'useStore'
import { import {
DEBUG_SHOW_BOTH_SCENES, DEBUG_SHOW_BOTH_SCENES,
@ -38,7 +38,7 @@ export const ClientSideScene = ({
cameraControls, cameraControls,
}: { }: {
cameraControls: ReturnType< cameraControls: ReturnType<
typeof useSettingsAuthContext typeof useGlobalStateContext
>['settings']['context']['cameraControls'] >['settings']['context']['cameraControls']
}) => { }) => {
const canvasRef = useRef<HTMLDivElement>(null) const canvasRef = useRef<HTMLDivElement>(null)

View File

@ -2,7 +2,7 @@ import { Toolbar } from '../Toolbar'
import UserSidebarMenu from './UserSidebarMenu' import UserSidebarMenu from './UserSidebarMenu'
import { type IndexLoaderData } from 'lib/types' import { type IndexLoaderData } from 'lib/types'
import ProjectSidebarMenu from './ProjectSidebarMenu' import ProjectSidebarMenu from './ProjectSidebarMenu'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import styles from './AppHeader.module.css' import styles from './AppHeader.module.css'
import { NetworkHealthIndicator } from './NetworkHealthIndicator' import { NetworkHealthIndicator } from './NetworkHealthIndicator'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
@ -25,7 +25,7 @@ export const AppHeader = ({
}: AppHeaderProps) => { }: AppHeaderProps) => {
const platform = usePlatform() const platform = usePlatform()
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const { auth } = useSettingsAuthContext() const { auth } = useGlobalStateContext()
const user = auth?.context?.user const user = auth?.context?.user
return ( return (

View File

@ -6,7 +6,7 @@ import React from 'react'
import { useFormik } from 'formik' import { useFormik } from 'formik'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { engineCommandManager } from '../lang/std/engineConnection' import { engineCommandManager } from '../lang/std/engineConnection'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
type OutputFormat = Models['OutputFormat_type'] type OutputFormat = Models['OutputFormat_type']
type OutputTypeKey = OutputFormat['type'] type OutputTypeKey = OutputFormat['type']
@ -29,7 +29,7 @@ export const ExportButton = ({ children, className }: ExportButtonProps) => {
context: { baseUnit }, context: { baseUnit },
}, },
}, },
} = useSettingsAuthContext() } = useGlobalStateContext()
const defaultType = 'gltf' const defaultType = 'gltf'
const [type, setType] = React.useState<OutputTypeKey>(defaultType) const [type, setType] = React.useState<OutputTypeKey>(defaultType)

View File

@ -5,12 +5,7 @@ import { authMachine, TOKEN_PERSIST_KEY } from '../machines/authMachine'
import withBaseUrl from '../lib/withBaseURL' import withBaseUrl from '../lib/withBaseURL'
import React, { createContext, useEffect, useRef } from 'react' import React, { createContext, useEffect, useRef } from 'react'
import useStateMachineCommands from '../hooks/useStateMachineCommands' import useStateMachineCommands from '../hooks/useStateMachineCommands'
import { settingsMachine } from 'machines/settingsMachine' import { SETTINGS_PERSIST_KEY, settingsMachine } from 'machines/settingsMachine'
import {
initialSettings,
SETTINGS_PERSIST_KEY,
validateSettings,
} from 'lib/settings'
import { toast } from 'react-hot-toast' import { toast } from 'react-hot-toast'
import { setThemeClass, Themes } from 'lib/theme' import { setThemeClass, Themes } from 'lib/theme'
import { import {
@ -23,7 +18,6 @@ import {
import { isTauri } from 'lib/isTauri' import { isTauri } from 'lib/isTauri'
import { settingsCommandBarConfig } from 'lib/commandBarConfigs/settingsCommandConfig' import { settingsCommandBarConfig } from 'lib/commandBarConfigs/settingsCommandConfig'
import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig' import { authCommandBarConfig } from 'lib/commandBarConfigs/authCommandConfig'
import { initializeProjectDirectory, readSettingsFile } from 'lib/tauriFS'
type MachineContext<T extends AnyStateMachine> = { type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T> state: StateFrom<T>
@ -31,14 +25,14 @@ type MachineContext<T extends AnyStateMachine> = {
send: Prop<InterpreterFrom<T>, 'send'> send: Prop<InterpreterFrom<T>, 'send'>
} }
type SettingsAuthContext = { type GlobalContext = {
auth: MachineContext<typeof authMachine> auth: MachineContext<typeof authMachine>
settings: MachineContext<typeof settingsMachine> settings: MachineContext<typeof settingsMachine>
} }
export const SettingsAuthStateContext = createContext({} as SettingsAuthContext) export const GlobalStateContext = createContext({} as GlobalContext)
export const SettingsAuthStateProvider = ({ export const GlobalStateProvider = ({
children, children,
}: { }: {
children: React.ReactNode children: React.ReactNode
@ -46,17 +40,14 @@ export const SettingsAuthStateProvider = ({
const navigate = useNavigate() const navigate = useNavigate()
// Settings machine setup // Settings machine setup
// Load settings from local storage
// and validate them
const retrievedSettings = useRef( const retrievedSettings = useRef(
validateSettings( localStorage?.getItem(SETTINGS_PERSIST_KEY) || '{}'
JSON.parse(localStorage?.getItem(SETTINGS_PERSIST_KEY) || '{}')
)
) )
const persistedSettings = Object.assign( const persistedSettings = Object.assign(
{}, settingsMachine.initialState.context,
initialSettings, JSON.parse(retrievedSettings.current) as Partial<
retrievedSettings.current.settings (typeof settingsMachine)['context']
>
) )
const [settingsState, settingsSend] = useMachine(settingsMachine, { const [settingsState, settingsSend] = useMachine(settingsMachine, {
@ -81,75 +72,6 @@ export const SettingsAuthStateProvider = ({
}, },
}) })
// If the app is running in the Tauri context,
// try to read the settings from a file
// after doing some validation on them
useEffect(() => {
async function getFileBasedSettings() {
if (isTauri()) {
const newSettings = await readSettingsFile()
if (newSettings) {
if (newSettings.defaultDirectory) {
const newDefaultDirectory = await initializeProjectDirectory(
newSettings.defaultDirectory || ''
)
if (newDefaultDirectory.error !== null) {
toast.error(newDefaultDirectory.error.message)
}
if (newDefaultDirectory.path !== null) {
newSettings.defaultDirectory = newDefaultDirectory.path
}
}
const { settings: validatedSettings, errors: validationErrors } =
validateSettings(newSettings)
retrievedSettings.current = Object.assign(
{},
initialSettings,
retrievedSettings.current,
validatedSettings
)
settingsSend({
type: 'Set All Settings',
data: validatedSettings,
})
return validationErrors
}
} else {
// If the app is not running in the Tauri context,
// just use the settings from local storage
// after they've been validated to ensure they are correct.
settingsSend({
type: 'Set All Settings',
data: retrievedSettings.current.settings,
})
}
return []
}
// If there were validation errors either from local storage or from the file,
// log them to the console and show a toast message to the user.
void getFileBasedSettings().then((validationErrors: string[]) => {
const combinedErrors = new Set([
...retrievedSettings.current.errors,
...validationErrors,
])
if (combinedErrors.size > 0) {
const errorMessage =
'Error validating persisted settings: ' +
Array.from(combinedErrors).join(', ') +
'. Using defaults.'
console.error(errorMessage)
toast.error(errorMessage)
}
})
}, [settingsSend])
useStateMachineCommands({ useStateMachineCommands({
machineId: 'settings', machineId: 'settings',
state: settingsState, state: settingsState,
@ -197,7 +119,7 @@ export const SettingsAuthStateProvider = ({
}) })
return ( return (
<SettingsAuthStateContext.Provider <GlobalStateContext.Provider
value={{ value={{
auth: { auth: {
state: authState, state: authState,
@ -212,11 +134,11 @@ export const SettingsAuthStateProvider = ({
}} }}
> >
{children} {children}
</SettingsAuthStateContext.Provider> </GlobalStateContext.Provider>
) )
} }
export default SettingsAuthStateProvider export default GlobalStateProvider
export function logout() { export function logout() {
localStorage.removeItem(TOKEN_PERSIST_KEY) localStorage.removeItem(TOKEN_PERSIST_KEY)

View File

@ -10,7 +10,7 @@ import {
} from 'xstate' } from 'xstate'
import { SetSelections, modelingMachine } from 'machines/modelingMachine' import { SetSelections, modelingMachine } from 'machines/modelingMachine'
import { useSetupEngineManager } from 'hooks/useSetupEngineManager' import { useSetupEngineManager } from 'hooks/useSetupEngineManager'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { isCursorInSketchCommandRange } from 'lang/util' import { isCursorInSketchCommandRange } from 'lang/util'
import { engineCommandManager } from 'lang/std/engineConnection' import { engineCommandManager } from 'lang/std/engineConnection'
import { kclManager, useKclContext } from 'lang/KclSingleton' import { kclManager, useKclContext } from 'lang/KclSingleton'
@ -53,7 +53,7 @@ export const ModelingMachineProvider = ({
}: { }: {
children: React.ReactNode children: React.ReactNode
}) => { }) => {
const { auth } = useSettingsAuthContext() const { auth } = useGlobalStateContext()
const { code } = useKclContext() const { code } = useKclContext()
const token = auth?.context?.token const token = auth?.context?.token
const streamRef = useRef<HTMLDivElement>(null) const streamRef = useRef<HTMLDivElement>(null)

View File

@ -1,6 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react' import { fireEvent, render, screen } from '@testing-library/react'
import { BrowserRouter } from 'react-router-dom' import { BrowserRouter } from 'react-router-dom'
import { SettingsAuthStateProvider } from './SettingsAuthStateProvider' import { GlobalStateProvider } from './GlobalStateProvider'
import CommandBarProvider from './CommandBar/CommandBar' import CommandBarProvider from './CommandBar/CommandBar'
import { import {
NETWORK_HEALTH_TEXT, NETWORK_HEALTH_TEXT,
@ -13,7 +13,7 @@ function TestWrap({ children }: { children: React.ReactNode }) {
return ( return (
<BrowserRouter> <BrowserRouter>
<CommandBarProvider> <CommandBarProvider>
<SettingsAuthStateProvider>{children}</SettingsAuthStateProvider> <GlobalStateProvider>{children}</GlobalStateProvider>
</CommandBarProvider> </CommandBarProvider>
</BrowserRouter> </BrowserRouter>
) )

View File

@ -2,7 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react'
import { BrowserRouter } from 'react-router-dom' import { BrowserRouter } from 'react-router-dom'
import ProjectSidebarMenu from './ProjectSidebarMenu' import ProjectSidebarMenu from './ProjectSidebarMenu'
import { type ProjectWithEntryPointMetadata } from 'lib/types' import { type ProjectWithEntryPointMetadata } from 'lib/types'
import { SettingsAuthStateProvider } from './SettingsAuthStateProvider' import { GlobalStateProvider } from './GlobalStateProvider'
import CommandBarProvider from './CommandBar/CommandBar' import CommandBarProvider from './CommandBar/CommandBar'
import { APP_NAME } from 'lib/constants' import { APP_NAME } from 'lib/constants'
@ -42,9 +42,9 @@ describe('ProjectSidebarMenu tests', () => {
render( render(
<BrowserRouter> <BrowserRouter>
<CommandBarProvider> <CommandBarProvider>
<SettingsAuthStateProvider> <GlobalStateProvider>
<ProjectSidebarMenu project={projectWellFormed} /> <ProjectSidebarMenu project={projectWellFormed} />
</SettingsAuthStateProvider> </GlobalStateProvider>
</CommandBarProvider> </CommandBarProvider>
</BrowserRouter> </BrowserRouter>
) )
@ -63,9 +63,9 @@ describe('ProjectSidebarMenu tests', () => {
render( render(
<BrowserRouter> <BrowserRouter>
<CommandBarProvider> <CommandBarProvider>
<SettingsAuthStateProvider> <GlobalStateProvider>
<ProjectSidebarMenu /> <ProjectSidebarMenu />
</SettingsAuthStateProvider> </GlobalStateProvider>
</CommandBarProvider> </CommandBarProvider>
</BrowserRouter> </BrowserRouter>
) )
@ -79,12 +79,12 @@ describe('ProjectSidebarMenu tests', () => {
render( render(
<BrowserRouter> <BrowserRouter>
<CommandBarProvider> <CommandBarProvider>
<SettingsAuthStateProvider> <GlobalStateProvider>
<ProjectSidebarMenu <ProjectSidebarMenu
project={projectWellFormed} project={projectWellFormed}
renderAsLink={true} renderAsLink={true}
/> />
</SettingsAuthStateProvider> </GlobalStateProvider>
</CommandBarProvider> </CommandBarProvider>
</BrowserRouter> </BrowserRouter>
) )

View File

@ -10,7 +10,7 @@ import { useStore } from '../useStore'
import { getNormalisedCoordinates, throttle } from '../lib/utils' import { getNormalisedCoordinates, throttle } from '../lib/utils'
import Loading from './Loading' import Loading from './Loading'
import { cameraMouseDragGuards } from 'lib/cameraControls' import { cameraMouseDragGuards } from 'lib/cameraControls'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { engineCommandManager } from '../lang/std/engineConnection' import { engineCommandManager } from '../lang/std/engineConnection'
import { useModelingContext } from 'hooks/useModelingContext' import { useModelingContext } from 'hooks/useModelingContext'
@ -34,7 +34,7 @@ export const Stream = ({ className = '' }: { className?: string }) => {
setDidDragInStream: s.setDidDragInStream, setDidDragInStream: s.setDidDragInStream,
streamDimensions: s.streamDimensions, streamDimensions: s.streamDimensions,
})) }))
const { settings } = useSettingsAuthContext() const { settings } = useGlobalStateContext()
const cameraControls = settings?.context?.cameraControls const cameraControls = settings?.context?.cameraControls
const { state } = useModelingContext() const { state } = useModelingContext()
const { isExecuting } = useKclContext() const { isExecuting } = useKclContext()

View File

@ -8,7 +8,7 @@ import Server from '../editor/lsp/server'
import Client from '../editor/lsp/client' import Client from '../editor/lsp/client'
import { TEST } from 'env' import { TEST } from 'env'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useConvertToVariable } from 'hooks/useToolbarGuards' import { useConvertToVariable } from 'hooks/useToolbarGuards'
import { Themes } from 'lib/theme' import { Themes } from 'lib/theme'
import { useMemo, useRef } from 'react' import { useMemo, useRef } from 'react'
@ -72,7 +72,7 @@ export const TextEditor = ({
} = useModelingContext() } = useModelingContext()
const { settings: { context: { textWrapping } = {} } = {}, auth } = const { settings: { context: { textWrapping } = {} } = {}, auth } =
useSettingsAuthContext() useGlobalStateContext()
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const { enable: convertEnabled, handleClick: convertCallback } = const { enable: convertEnabled, handleClick: convertCallback } =
useConvertToVariable() useConvertToVariable()

View File

@ -7,7 +7,7 @@ import {
createRoutesFromElements, createRoutesFromElements,
} from 'react-router-dom' } from 'react-router-dom'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { SettingsAuthStateProvider } from './SettingsAuthStateProvider' import { GlobalStateProvider } from './GlobalStateProvider'
import CommandBarProvider from './CommandBar/CommandBar' import CommandBarProvider from './CommandBar/CommandBar'
type User = Models['User_type'] type User = Models['User_type']
@ -107,7 +107,7 @@ function TestWrap({ children }: { children: React.ReactNode }) {
path="/file/:id" path="/file/:id"
element={ element={
<CommandBarProvider> <CommandBarProvider>
<SettingsAuthStateProvider>{children}</SettingsAuthStateProvider> <GlobalStateProvider>{children}</GlobalStateProvider>
</CommandBarProvider> </CommandBarProvider>
} }
/> />

View File

@ -6,7 +6,7 @@ import { useLocation, useNavigate } from 'react-router-dom'
import { Fragment, useState } from 'react' import { Fragment, useState } from 'react'
import { paths } from 'lib/paths' import { paths } from 'lib/paths'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
type User = Models['User_type'] type User = Models['User_type']
@ -17,7 +17,7 @@ const UserSidebarMenu = ({ user }: { user?: User }) => {
const displayedName = getDisplayName(user) const displayedName = getDisplayName(user)
const [imageLoadFailed, setImageLoadFailed] = useState(false) const [imageLoadFailed, setImageLoadFailed] = useState(false)
const navigate = useNavigate() const navigate = useNavigate()
const send = useSettingsAuthContext()?.auth?.send const send = useGlobalStateContext()?.auth?.send
// Fallback logic for displaying user's "name": // Fallback logic for displaying user's "name":
// 1. user.name // 1. user.name

View File

@ -0,0 +1,6 @@
import { GlobalStateContext } from 'components/GlobalStateProvider'
import { useContext } from 'react'
export const useGlobalStateContext = () => {
return useContext(GlobalStateContext)
}

View File

@ -1,6 +0,0 @@
import { SettingsAuthStateContext } from 'components/SettingsAuthStateProvider'
import { useContext } from 'react'
export const useSettingsAuthContext = () => {
return useContext(SettingsAuthStateContext)
}

View File

@ -1,6 +1,11 @@
import { CommandSetConfig } from '../commandTypes' import { CommandSetConfig } from '../commandTypes'
import { BaseUnit, Toggle, UnitSystem, baseUnitsUnion } from 'lib/settings' import {
import { settingsMachine } from 'machines/settingsMachine' BaseUnit,
Toggle,
UnitSystem,
baseUnitsUnion,
settingsMachine,
} from 'machines/settingsMachine'
import { CameraSystem, cameraSystems } from '../cameraControls' import { CameraSystem, cameraSystems } from '../cameraControls'
import { Themes } from '../theme' import { Themes } from '../theme'

View File

@ -1,95 +0,0 @@
import { type Models } from '@kittycad/lib'
import { CameraSystem, cameraSystems } from './cameraControls'
import { Themes } from './theme'
export const DEFAULT_PROJECT_NAME = 'project-$nnn'
export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY'
export const SETTINGS_FILE_NAME = 'settings.json'
export enum UnitSystem {
Imperial = 'imperial',
Metric = 'metric',
}
export const baseUnits = {
imperial: ['in', 'ft', 'yd'],
metric: ['mm', 'cm', 'm'],
} as const
export type BaseUnit = Models['UnitLength_type']
export const baseUnitsUnion = Object.values(baseUnits).flatMap((v) => v)
export type Toggle = 'On' | 'Off'
const toggleAsArray = ['On', 'Off'] as const
export type SettingsMachineContext = {
baseUnit: BaseUnit
cameraControls: CameraSystem
defaultDirectory: string
defaultProjectName: string
onboardingStatus: string
showDebugPanel: boolean
textWrapping: Toggle
theme: Themes
unitSystem: UnitSystem
}
export const initialSettings: SettingsMachineContext = {
baseUnit: 'in' as BaseUnit,
cameraControls: 'KittyCAD' as CameraSystem,
defaultDirectory: '',
defaultProjectName: DEFAULT_PROJECT_NAME,
onboardingStatus: '',
showDebugPanel: false,
textWrapping: 'On' as Toggle,
theme: Themes.System,
unitSystem: UnitSystem.Imperial,
}
function isEnumMember<T extends Record<string, unknown>>(v: unknown, e: T) {
return Object.values(e).includes(v)
}
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',
defaultProjectName: (v) => typeof v === 'string',
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 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)
}
}
return {
settings: settingsNoInvalidKeys as Partial<SettingsMachineContext>,
errors,
}
}

View File

@ -3,16 +3,12 @@ import {
createDir, createDir,
exists, exists,
readDir, readDir,
readTextFile,
writeTextFile, writeTextFile,
} from '@tauri-apps/api/fs' } from '@tauri-apps/api/fs'
import { appConfigDir, documentDir, homeDir, sep } from '@tauri-apps/api/path' import { documentDir, homeDir, sep } from '@tauri-apps/api/path'
import { isTauri } from './isTauri' import { isTauri } from './isTauri'
import { type ProjectWithEntryPointMetadata } from 'lib/types' import { type ProjectWithEntryPointMetadata } from 'lib/types'
import { metadata } from 'tauri-plugin-fs-extra-api' import { metadata } from 'tauri-plugin-fs-extra-api'
import { settingsMachine } from 'machines/settingsMachine'
import { ContextFrom } from 'xstate'
import { SETTINGS_FILE_NAME } from 'lib/settings'
const PROJECT_FOLDER = 'zoo-modeling-app-projects' const PROJECT_FOLDER = 'zoo-modeling-app-projects'
export const FILE_EXT = '.kcl' export const FILE_EXT = '.kcl'
@ -21,101 +17,39 @@ const INDEX_IDENTIFIER = '$n' // $nn.. will pad the number with 0s
export const MAX_PADDING = 7 export const MAX_PADDING = 7
const RELEVANT_FILE_TYPES = ['kcl'] const RELEVANT_FILE_TYPES = ['kcl']
type PathWithPossibleError = {
path: string | null
error: Error | null
}
export async function getInitialDefaultDir() {
let dir
try {
dir = await documentDir()
} catch (e) {
dir = `${await homeDir()}Documents/` // for headless Linux (eg. Github Actions)
}
return dir + PROJECT_FOLDER
}
// Initializes the project directory and returns the path // Initializes the project directory and returns the path
// with any Errors that occurred export async function initializeProjectDirectory(directory: string) {
export async function initializeProjectDirectory(
directory: string
): Promise<PathWithPossibleError> {
if (!isTauri()) { if (!isTauri()) {
throw new Error( throw new Error(
'initializeProjectDirectory() can only be called from a Tauri app' 'initializeProjectDirectory() can only be called from a Tauri app'
) )
} }
let returnValue: PathWithPossibleError = {
path: null,
error: null,
}
if (directory) { if (directory) {
returnValue = await testAndCreateDir(directory, returnValue) const dirExists = await exists(directory)
} if (!dirExists) {
await createDir(directory, { recursive: true })
// If the directory from settings does not exist or could not be created,
// use the default directory
if (returnValue.path === null) {
const INITIAL_DEFAULT_DIR = await getInitialDefaultDir()
const defaultReturnValue = await testAndCreateDir(
INITIAL_DEFAULT_DIR,
returnValue,
{
exists: 'Error checking default directory.',
create: 'Error creating default directory.',
}
)
returnValue.path = defaultReturnValue.path
returnValue.error =
returnValue.error === null ? defaultReturnValue.error : returnValue.error
}
return returnValue
}
async function testAndCreateDir(
directory: string,
returnValue = {
path: null,
error: null,
} as PathWithPossibleError,
errorMessages = {
exists:
'Error checking directory at path from saved settings. Using default.',
create:
'Error creating directory at path from saved settings. Using default.',
}
): Promise<PathWithPossibleError> {
const dirExists = await exists(directory).catch((e) => {
console.error(`Error checking directory ${directory}. Original error:`, e)
return new Error(errorMessages.exists)
})
if (dirExists instanceof Error) {
returnValue.error = dirExists
} else if (dirExists && dirExists === true) {
const newDirCreated = await createDir(directory, { recursive: true }).catch(
(e) => {
console.error(
`Error creating directory ${directory}. Original error:`,
e
)
return new Error(errorMessages.create)
}
)
if (newDirCreated instanceof Error) {
returnValue.error = newDirCreated
} else {
returnValue.path = directory
} }
return directory
} }
return returnValue let docDirectory: string
try {
docDirectory = await documentDir()
} catch (e) {
console.log('error', e)
docDirectory = `${await homeDir()}Documents/` // for headless Linux (eg. Github Actions)
}
const INITIAL_DEFAULT_DIR = docDirectory + PROJECT_FOLDER
const defaultDirExists = await exists(INITIAL_DEFAULT_DIR)
if (!defaultDirExists) {
await createDir(INITIAL_DEFAULT_DIR, { recursive: true })
}
return INITIAL_DEFAULT_DIR
} }
export function isProjectDirectory(fileOrDir: Partial<FileEntry>) { export function isProjectDirectory(fileOrDir: Partial<FileEntry>) {
@ -366,44 +300,3 @@ function getPaddedIdentifierRegExp() {
const escapedIdentifier = escapeRegExpChars(INDEX_IDENTIFIER) const escapedIdentifier = escapeRegExpChars(INDEX_IDENTIFIER)
return new RegExp(`${escapedIdentifier}(${escapedIdentifier.slice(-1)}*)`) return new RegExp(`${escapedIdentifier}(${escapedIdentifier.slice(-1)}*)`)
} }
export async function getSettingsFilePath() {
const dir = await appConfigDir()
return dir + SETTINGS_FILE_NAME
}
export async function writeToSettingsFile(
settings: ContextFrom<typeof settingsMachine>
) {
return writeTextFile(
await getSettingsFilePath(),
JSON.stringify(settings, null, 2)
)
}
export async function readSettingsFile(): Promise<ContextFrom<
typeof settingsMachine
> | null> {
const dir = await appConfigDir()
const path = dir + SETTINGS_FILE_NAME
const dirExists = await exists(dir)
if (!dirExists) {
await createDir(dir, { recursive: true })
}
const settingsExist = dirExists ? await exists(path) : false
if (!settingsExist) {
console.log(`Settings file does not exist at ${path}`)
await writeToSettingsFile(settingsMachine.initialState.context)
return null
}
try {
const settings = await readTextFile(path)
return JSON.parse(settings)
} catch (e) {
console.error('Error reading settings file:', e)
return null
}
}

View File

@ -13,5 +13,4 @@ export type ProjectWithEntryPointMetadata = FileEntry & {
export type HomeLoaderData = { export type HomeLoaderData = {
projects: ProjectWithEntryPointMetadata[] projects: ProjectWithEntryPointMetadata[]
newDefaultDirectory?: string newDefaultDirectory?: string
error: Error | null
} }

View File

@ -1,43 +1,49 @@
import { assign, createMachine } from 'xstate' import { assign, createMachine } from 'xstate'
import { Themes, getSystemTheme, setThemeClass } from '../lib/theme' import { Themes, getSystemTheme, setThemeClass } from '../lib/theme'
import { CameraSystem } from 'lib/cameraControls' import { CameraSystem } from 'lib/cameraControls'
import { isTauri } from 'lib/isTauri' import { Models } from '@kittycad/lib'
import { writeToSettingsFile } from 'lib/tauriFS'
import { export const DEFAULT_PROJECT_NAME = 'project-$nnn'
BaseUnit,
DEFAULT_PROJECT_NAME, export enum UnitSystem {
SETTINGS_PERSIST_KEY, Imperial = 'imperial',
SettingsMachineContext, Metric = 'metric',
Toggle, }
UnitSystem,
initialSettings, export const baseUnits = {
} from 'lib/settings' imperial: ['in', 'ft', 'yd'],
metric: ['mm', 'cm', 'm'],
} as const
export type BaseUnit = Models['UnitLength_type']
export const baseUnitsUnion = Object.values(baseUnits).flatMap((v) => v)
export type Toggle = 'On' | 'Off'
export const SETTINGS_PERSIST_KEY = 'SETTINGS_PERSIST_KEY'
export const settingsMachine = createMachine( export const settingsMachine = createMachine(
{ {
/** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlTQAIAVACzAFswBtABgF1FQAHAe1iYsfbNxAAPRAA42+AEwB2KQFYAzGznKAnADZli1QBoQAT2kBGKfm37lOned3nzqgL6vjlLLgJFSFdCoAETAAMwBDAFdiagAFACc+ACswAGNqADlw5nYuJBB+QWFRfMkEABY5fDYa2rra83LjMwQdLWV8BXLyuxlVLU1Ld090bzxCEnJKYLComODMeLS0PniTXLFCoUwRMTK7fC1zNql7NgUjtnKjU0RlBSqpLVUVPVUda60tYZAvHHG-FNAgBVbBCKjIEywNBMDb5LbFPaILqdfRSORsS4qcxXZqIHqyK6qY4XOxsGTKco-P4+Cb+aYAIXCsDAVFBQjhvAE212pWkskUKnUml0+gUNxaqkU+EccnKF1UCnucnMcjcHl+o3+vkmZBofCgUFIMwARpEoFRYuFsGBiJyCtzEXzWrJlGxlKdVFKvfY1XiEBjyvhVOVzBdzu13pYFNStbTAQFqAB5bAmvjheIQf4QtDhNCRWD2hE7EqgfayHTEh7lHQNSxSf1Scz4cpHHFyFVujTKczuDXYPgQOBiGl4TaOktIhAAWg6X3nC4Xp39050sYw2rpYHHRUnztVhPJqmUlIGbEriv9WhrLZ6uibHcqUr7riAA */ /** @xstate-layout N4IgpgJg5mDOIC5QGUwBc0EsB2VYDpMIAbMAYlTQAIAVACzAFswBtABgF1FQAHAe1iYsfbNxAAPRAA42+AEwB2KQFYAzGznKAnADZli1QBoQAT2kBGKfm37lOned3nzqgL6vjlLLgJFSFdCoAETAAMwBDAFdiagAFACc+ACswAGNqADlw5nYuJBB+QWFRfMkEABY5fDYa2rra83LjMwQdLWV8BXLyuxlVLU1Ld090bzxCEnJKYLComODMeLS0PniTXLFCoUwRMTK7fC1zNql7NgUjtnKjU0RlBSqpLVUVPVUda60tYZAvHHG-FNAgBVbBCKjIEywNBMDb5LbFPaILqdfRSORsS4qcxXZqIHqyK6qY4XOxsGTKco-P4+Cb+aYAIXCsDAVFBQjhvAE212pWkskUKnUml0+gUNxaqkU+EccnKF1UCnucnMcjcHl+o3+vkmZBofCgUFIMwARpEoFRYuFsGBiJyCtzEXzWrJlGxlKdVFKvfY1XiEBjyvhVOVzBdzu13pYFNStbTAQFqAB5bAmvjheIQf4QtDhNCRWD2hE7EqgfayHTEh7lHQNSxSf1Scz4cpHHFyFVujTKczuDXYPgQOBiGl4TaOktIhAAWg6X3nC4Xp39050sYw2rpYHHRUnztVhPJqmUlIGbEriv9WhrLZ6uibHcqUr7riAA */
id: 'Settings', id: 'Settings',
predictableActionArguments: true, predictableActionArguments: true,
context: { ...initialSettings }, context: {
baseUnit: 'in' as BaseUnit,
cameraControls: 'KittyCAD' as CameraSystem,
defaultDirectory: '',
defaultProjectName: DEFAULT_PROJECT_NAME,
onboardingStatus: '',
showDebugPanel: false,
textWrapping: 'On' as Toggle,
theme: Themes.System,
unitSystem: UnitSystem.Imperial,
},
initial: 'idle', initial: 'idle',
states: { states: {
idle: { idle: {
entry: ['setThemeClass'], entry: ['setThemeClass'],
on: { on: {
'Set All Settings': {
actions: [
assign((context, event) => {
return {
...context,
...event.data,
}
}),
'persistSettings',
'setThemeClass',
],
target: 'idle',
internal: true,
},
'Set Base Unit': { 'Set Base Unit': {
actions: [ actions: [
assign({ assign({
@ -151,7 +157,6 @@ export const settingsMachine = createMachine(
tsTypes: {} as import('./settingsMachine.typegen').Typegen0, tsTypes: {} as import('./settingsMachine.typegen').Typegen0,
schema: { schema: {
events: {} as events: {} as
| { type: 'Set All Settings'; data: Partial<SettingsMachineContext> }
| { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } } | { type: 'Set Base Unit'; data: { baseUnit: BaseUnit } }
| { | {
type: 'Set Camera Controls' type: 'Set Camera Controls'
@ -175,11 +180,6 @@ export const settingsMachine = createMachine(
{ {
actions: { actions: {
persistSettings: (context) => { persistSettings: (context) => {
if (isTauri()) {
writeToSettingsFile(context).catch((err) => {
console.error('Error writing settings:', err)
})
}
try { try {
localStorage.setItem(SETTINGS_PERSIST_KEY, JSON.stringify(context)) localStorage.setItem(SETTINGS_PERSIST_KEY, JSON.stringify(context))
} catch (e) { } catch (e) {

View File

@ -29,9 +29,9 @@ import {
getSortIcon, getSortIcon,
} from '../lib/sorting' } from '../lib/sorting'
import useStateMachineCommands from '../hooks/useStateMachineCommands' import useStateMachineCommands from '../hooks/useStateMachineCommands'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { DEFAULT_PROJECT_NAME } from 'lib/settings' import { DEFAULT_PROJECT_NAME } from 'machines/settingsMachine'
import { sep } from '@tauri-apps/api/path' import { sep } from '@tauri-apps/api/path'
import { homeCommandBarConfig } from 'lib/commandBarConfigs/homeCommandConfig' import { homeCommandBarConfig } from 'lib/commandBarConfigs/homeCommandConfig'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
@ -42,17 +42,14 @@ import { isTauri } from 'lib/isTauri'
const Home = () => { const Home = () => {
const { commandBarSend } = useCommandsContext() const { commandBarSend } = useCommandsContext()
const navigate = useNavigate() const navigate = useNavigate()
const { const { projects: loadedProjects, newDefaultDirectory } =
projects: loadedProjects, useLoaderData() as HomeLoaderData
newDefaultDirectory,
error,
} = useLoaderData() as HomeLoaderData
const { const {
settings: { settings: {
context: { defaultDirectory, defaultProjectName }, context: { defaultDirectory, defaultProjectName },
send: sendToSettings, send: sendToSettings,
}, },
} = useSettingsAuthContext() } = useGlobalStateContext()
// Set the default directory if it's been updated // Set the default directory if it's been updated
// during the loading of the home page. This is wrapped // during the loading of the home page. This is wrapped
@ -60,17 +57,11 @@ const Home = () => {
useEffect(() => { useEffect(() => {
if (newDefaultDirectory) { if (newDefaultDirectory) {
sendToSettings({ sendToSettings({
type: 'Set All Settings', type: 'Set Default Directory',
data: { defaultDirectory: newDefaultDirectory }, data: { defaultDirectory: newDefaultDirectory },
}) })
} }
// Toast any errors that occurred during the loading process
if (error) {
toast.error(error.message)
}
}, []) }, [])
useHotkeys( useHotkeys(
isTauri() ? 'mod+,' : 'shift+mod+,', isTauri() ? 'mod+,' : 'shift+mod+,',
() => navigate(paths.HOME + paths.SETTINGS), () => navigate(paths.HOME + paths.SETTINGS),

View File

@ -2,7 +2,7 @@ import { OnboardingButtons, useDismiss, useNextClick } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { useStore } from '../../useStore' import { useStore } from '../../useStore'
import { SettingsSection } from 'routes/Settings' import { SettingsSection } from 'routes/Settings'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { import {
CameraSystem, CameraSystem,
cameraMouseDragGuards, cameraMouseDragGuards,
@ -22,7 +22,7 @@ export default function Units() {
context: { cameraControls }, context: { cameraControls },
}, },
}, },
} = useSettingsAuthContext() } = useGlobalStateContext()
return ( return (
<div className="fixed inset-0 z-50 grid items-end justify-start px-4 pointer-events-none"> <div className="fixed inset-0 z-50 grid items-end justify-start px-4 pointer-events-none">

View File

@ -5,7 +5,7 @@ import {
useNextClick, useNextClick,
} from '.' } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { Themes, getSystemTheme } from 'lib/theme' import { Themes, getSystemTheme } from 'lib/theme'
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
import { import {
@ -31,7 +31,7 @@ function OnboardingWithNewFile() {
settings: { settings: {
context: { defaultDirectory }, context: { defaultDirectory },
}, },
} = useSettingsAuthContext() } = useGlobalStateContext()
async function createAndOpenNewProject() { async function createAndOpenNewProject() {
const projects = await getProjectsInDir(defaultDirectory) const projects = await getProjectsInDir(defaultDirectory)
@ -111,7 +111,7 @@ export default function Introduction() {
context: { theme }, context: { theme },
}, },
}, },
} = useSettingsAuthContext() } = useGlobalStateContext()
const getLogoTheme = () => const getLogoTheme = () =>
theme === Themes.Light || theme === Themes.Light ||
(theme === Themes.System && getSystemTheme() === Themes.Light) (theme === Themes.System && getSystemTheme() === Themes.Light)

View File

@ -3,7 +3,7 @@ import { onboardingPaths } from 'routes/Onboarding/paths'
import { useStore } from '../../useStore' import { useStore } from '../../useStore'
import { useBackdropHighlight } from 'hooks/useBackdropHighlight' import { useBackdropHighlight } from 'hooks/useBackdropHighlight'
import { Themes, getSystemTheme } from 'lib/theme' import { Themes, getSystemTheme } from 'lib/theme'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
export default function ParametricModeling() { export default function ParametricModeling() {
const { buttonDownInStream } = useStore((s) => ({ const { buttonDownInStream } = useStore((s) => ({
@ -13,7 +13,7 @@ export default function ParametricModeling() {
settings: { settings: {
context: { theme }, context: { theme },
}, },
} = useSettingsAuthContext() } = useGlobalStateContext()
const getImageTheme = () => const getImageTheme = () =>
theme === Themes.Light || theme === Themes.Light ||
(theme === Themes.System && getSystemTheme() === Themes.Light) (theme === Themes.System && getSystemTheme() === Themes.Light)

View File

@ -1,11 +1,12 @@
import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons' import { faArrowRight, faXmark } from '@fortawesome/free-solid-svg-icons'
import { BaseUnit, baseUnits, UnitSystem } from 'lib/settings' import { BaseUnit, baseUnits } from '../../machines/settingsMachine'
import { ActionButton } from '../../components/ActionButton' import { ActionButton } from '../../components/ActionButton'
import { SettingsSection } from '../Settings' import { SettingsSection } from '../Settings'
import { Toggle } from '../../components/Toggle/Toggle' import { Toggle } from '../../components/Toggle/Toggle'
import { useDismiss, useNextClick } from '.' import { useDismiss, useNextClick } from '.'
import { onboardingPaths } from 'routes/Onboarding/paths' import { onboardingPaths } from 'routes/Onboarding/paths'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { UnitSystem } from 'machines/settingsMachine'
export default function Units() { export default function Units() {
const dismiss = useDismiss() const dismiss = useDismiss()
@ -15,7 +16,7 @@ export default function Units() {
send, send,
context: { unitSystem, baseUnit }, context: { unitSystem, baseUnit },
}, },
} = useSettingsAuthContext() } = useGlobalStateContext()
return ( return (
<div className="fixed grid place-content-center inset-0 bg-chalkboard-110/50 z-50"> <div className="fixed grid place-content-center inset-0 bg-chalkboard-110/50 z-50">

View File

@ -5,7 +5,7 @@ import Camera from './Camera'
import Sketching from './Sketching' import Sketching from './Sketching'
import { useCallback } from 'react' import { useCallback } from 'react'
import makeUrlPathRelative from '../../lib/makeUrlPathRelative' import makeUrlPathRelative from '../../lib/makeUrlPathRelative'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import Streaming from './Streaming' import Streaming from './Streaming'
import CodeEditor from './CodeEditor' import CodeEditor from './CodeEditor'
import ParametricModeling from './ParametricModeling' import ParametricModeling from './ParametricModeling'
@ -78,7 +78,7 @@ export function useNextClick(newStatus: string) {
const filePath = useAbsoluteFilePath() const filePath = useAbsoluteFilePath()
const { const {
settings: { send }, settings: { send },
} = useSettingsAuthContext() } = useGlobalStateContext()
const navigate = useNavigate() const navigate = useNavigate()
return useCallback(() => { return useCallback(() => {
@ -94,7 +94,7 @@ export function useDismiss() {
const filePath = useAbsoluteFilePath() const filePath = useAbsoluteFilePath()
const { const {
settings: { send }, settings: { send },
} = useSettingsAuthContext() } = useGlobalStateContext()
const navigate = useNavigate() const navigate = useNavigate()
return useCallback(() => { return useCallback(() => {

View File

@ -5,38 +5,31 @@ import { open } from '@tauri-apps/api/dialog'
import { import {
BaseUnit, BaseUnit,
DEFAULT_PROJECT_NAME, DEFAULT_PROJECT_NAME,
SETTINGS_PERSIST_KEY,
baseUnits, baseUnits,
initialSettings, } from '../machines/settingsMachine'
UnitSystem,
} from 'lib/settings'
import { Toggle } from '../components/Toggle/Toggle' import { Toggle } from '../components/Toggle/Toggle'
import { useLocation, useNavigate, useRouteLoaderData } from 'react-router-dom' import { useLocation, useNavigate, useRouteLoaderData } from 'react-router-dom'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import { type IndexLoaderData } from 'lib/types' import { type IndexLoaderData } from 'lib/types'
import { paths } from 'lib/paths' import { paths } from 'lib/paths'
import { Themes } from '../lib/theme' import { Themes } from '../lib/theme'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { import {
CameraSystem, CameraSystem,
cameraSystems, cameraSystems,
cameraMouseDragGuards, cameraMouseDragGuards,
} from 'lib/cameraControls' } from 'lib/cameraControls'
import { UnitSystem } from 'machines/settingsMachine'
import { useDotDotSlash } from 'hooks/useDotDotSlash' import { useDotDotSlash } from 'hooks/useDotDotSlash'
import { import {
createNewProject, createNewProject,
getNextProjectIndex, getNextProjectIndex,
getProjectsInDir, getProjectsInDir,
getSettingsFilePath,
initializeProjectDirectory,
interpolateProjectNameWithIndex, interpolateProjectNameWithIndex,
} from 'lib/tauriFS' } from 'lib/tauriFS'
import { ONBOARDING_PROJECT_NAME } from './Onboarding' import { ONBOARDING_PROJECT_NAME } from './Onboarding'
import { sep } from '@tauri-apps/api/path' import { sep } from '@tauri-apps/api/path'
import { bracket } from 'lib/exampleKcl' import { bracket } from 'lib/exampleKcl'
import { isTauri } from 'lib/isTauri'
import { invoke } from '@tauri-apps/api'
import toast from 'react-hot-toast'
export const Settings = () => { export const Settings = () => {
const APP_VERSION = import.meta.env.PACKAGE_VERSION || 'unknown' const APP_VERSION = import.meta.env.PACKAGE_VERSION || 'unknown'
@ -62,14 +55,11 @@ export const Settings = () => {
}, },
}, },
}, },
} = useSettingsAuthContext() } = useGlobalStateContext()
async function handleDirectorySelection() { async function handleDirectorySelection() {
// the `recursive` property added following
// this advice for permissions: https://github.com/tauri-apps/tauri/issues/4851#issuecomment-1210711455
const newDirectory = await open({ const newDirectory = await open({
directory: true, directory: true,
recursive: true,
defaultPath: defaultDirectory || paths.INDEX, defaultPath: defaultDirectory || paths.INDEX,
title: 'Choose a new default directory', title: 'Choose a new default directory',
}) })
@ -316,59 +306,6 @@ export const Settings = () => {
Replay Onboarding Replay Onboarding
</ActionButton> </ActionButton>
</SettingsSection> </SettingsSection>
<p className="font-mono my-6 leading-loose">
Your settings are saved in{' '}
{isTauri()
? 'a file in the app data folder for your OS.'
: "your browser's local storage."}{' '}
{isTauri() ? (
<span className="flex gap-4 flex-wrap items-center">
<button
onClick={async () =>
void invoke('show_in_folder', {
path: await getSettingsFilePath(),
})
}
className="text-base"
>
Show settings.json in folder
</button>
<button
onClick={async () => {
// We have to re-call initializeProjectDirectory
// since we can't set that in the settings machine's
// initial context due to it being async
send({
type: 'Set All Settings',
data: {
...initialSettings,
defaultDirectory:
(await initializeProjectDirectory('')).path ?? '',
},
})
toast.success('Settings restored to default')
}}
className="text-base"
>
Restore default settings
</button>
</span>
) : (
<button
onClick={() => {
localStorage.removeItem(SETTINGS_PERSIST_KEY)
send({
type: 'Set All Settings',
data: initialSettings,
})
toast.success('Settings restored to default')
}}
className="text-base"
>
Restore default settings
</button>
)}
</p>
<p className="mt-24 text-sm font-mono"> <p className="mt-24 text-sm font-mono">
{/* This uses a Vite plugin, set in vite.config.ts {/* This uses a Vite plugin, set in vite.config.ts
to inject the version from package.json */} to inject the version from package.json */}

View File

@ -4,7 +4,7 @@ import { invoke } from '@tauri-apps/api/tauri'
import { VITE_KC_SITE_BASE_URL, VITE_KC_API_BASE_URL } from '../env' import { VITE_KC_SITE_BASE_URL, VITE_KC_API_BASE_URL } from '../env'
import { Themes, getSystemTheme } from '../lib/theme' import { Themes, getSystemTheme } from '../lib/theme'
import { paths } from 'lib/paths' import { paths } from 'lib/paths'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useGlobalStateContext } from 'hooks/useGlobalStateContext'
import { APP_NAME } from 'lib/constants' import { APP_NAME } from 'lib/constants'
const SignIn = () => { const SignIn = () => {
@ -20,7 +20,7 @@ const SignIn = () => {
context: { theme }, context: { theme },
}, },
}, },
} = useSettingsAuthContext() } = useGlobalStateContext()
const signInTauri = async () => { const signInTauri = async () => {
// We want to invoke our command to login via device auth. // We want to invoke our command to login via device auth.