Make settings auto-save (#242)
* Feature: settings auto-save as they are updated * Refactor: get rid of temporary settings states * Feature: add escape hotkey to settings * Style: layout tweaks * Feature: setting unit system updates base unit too
This commit is contained in:
		
							
								
								
									
										111
									
								
								src/App.tsx
									
									
									
									
									
								
							
							
						
						
									
										111
									
								
								src/App.tsx
									
									
									
									
									
								
							@ -9,12 +9,7 @@ import { DebugPanel } from './components/DebugPanel'
 | 
			
		||||
import { v4 as uuidv4 } from 'uuid'
 | 
			
		||||
import { asyncLexer } from './lang/tokeniser'
 | 
			
		||||
import { abstractSyntaxTree } from './lang/abstractSyntaxTree'
 | 
			
		||||
import {
 | 
			
		||||
  _executor,
 | 
			
		||||
  ProgramMemory,
 | 
			
		||||
  ExtrudeGroup,
 | 
			
		||||
  SketchGroup,
 | 
			
		||||
} from './lang/executor'
 | 
			
		||||
import { _executor } from './lang/executor'
 | 
			
		||||
import CodeMirror from '@uiw/react-codemirror'
 | 
			
		||||
import { langs } from '@uiw/codemirror-extensions-langs'
 | 
			
		||||
import { linter, lintGutter } from '@codemirror/lint'
 | 
			
		||||
@ -358,7 +353,7 @@ export function App() {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      className="h-screen relative flex flex-col"
 | 
			
		||||
      className="h-screen overflow-hidden relative flex flex-col"
 | 
			
		||||
      onMouseMove={handleMouseMove}
 | 
			
		||||
    >
 | 
			
		||||
      <AppHeader
 | 
			
		||||
@ -371,7 +366,7 @@ export function App() {
 | 
			
		||||
      <ModalContainer />
 | 
			
		||||
      <Resizable
 | 
			
		||||
        className={
 | 
			
		||||
          'z-10 my-5 ml-5 pr-1 flex flex-col flex-grow overflow-hidden transition-opacity transition-duration-75 ' +
 | 
			
		||||
          'h-full flex flex-col flex-1 z-10 my-5 ml-5 pr-1 transition-opacity transition-duration-75 ' +
 | 
			
		||||
          (isMouseDownInStream || onboardingStatus === 'camera'
 | 
			
		||||
            ? ' pointer-events-none '
 | 
			
		||||
            : ' ') +
 | 
			
		||||
@ -390,57 +385,59 @@ export function App() {
 | 
			
		||||
            'hover:bg-liquid-30/40 dark:hover:bg-liquid-10/40 bg-transparent transition-colors duration-100 transition-ease-out delay-100',
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <CollapsiblePanel
 | 
			
		||||
          title="Code"
 | 
			
		||||
          icon={faCode}
 | 
			
		||||
          className="open:!mb-2"
 | 
			
		||||
          open={openPanes.includes('code')}
 | 
			
		||||
        >
 | 
			
		||||
          <div className="px-2 py-1">
 | 
			
		||||
            <button
 | 
			
		||||
              // disabled={!shouldFormat}
 | 
			
		||||
              onClick={formatCode}
 | 
			
		||||
              // className={`${!shouldFormat && 'text-gray-300'}`}
 | 
			
		||||
            >
 | 
			
		||||
              format
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div id="code-mirror-override">
 | 
			
		||||
            <CodeMirror
 | 
			
		||||
              className="h-full"
 | 
			
		||||
              value={code}
 | 
			
		||||
              extensions={[
 | 
			
		||||
                langs.javascript({ jsx: true }),
 | 
			
		||||
                lineHighlightField,
 | 
			
		||||
                ...extraExtensions,
 | 
			
		||||
              ]}
 | 
			
		||||
              onChange={onChange}
 | 
			
		||||
              onUpdate={onUpdate}
 | 
			
		||||
        <div className="h-full flex flex-col justify-between">
 | 
			
		||||
          <CollapsiblePanel
 | 
			
		||||
            title="Code"
 | 
			
		||||
            icon={faCode}
 | 
			
		||||
            className="open:!mb-2"
 | 
			
		||||
            open={openPanes.includes('code')}
 | 
			
		||||
          >
 | 
			
		||||
            <div className="px-2 py-1">
 | 
			
		||||
              <button
 | 
			
		||||
                // disabled={!shouldFormat}
 | 
			
		||||
                onClick={formatCode}
 | 
			
		||||
                // className={`${!shouldFormat && 'text-gray-300'}`}
 | 
			
		||||
              >
 | 
			
		||||
                format
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div id="code-mirror-override">
 | 
			
		||||
              <CodeMirror
 | 
			
		||||
                className="h-full"
 | 
			
		||||
                value={code}
 | 
			
		||||
                extensions={[
 | 
			
		||||
                  langs.javascript({ jsx: true }),
 | 
			
		||||
                  lineHighlightField,
 | 
			
		||||
                  ...extraExtensions,
 | 
			
		||||
                ]}
 | 
			
		||||
                onChange={onChange}
 | 
			
		||||
                onUpdate={onUpdate}
 | 
			
		||||
                theme={theme}
 | 
			
		||||
                onCreateEditor={(_editorView) => setEditorView(_editorView)}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
          </CollapsiblePanel>
 | 
			
		||||
          <section className="flex flex-col">
 | 
			
		||||
            <MemoryPanel
 | 
			
		||||
              theme={theme}
 | 
			
		||||
              onCreateEditor={(_editorView) => setEditorView(_editorView)}
 | 
			
		||||
              open={openPanes.includes('variables')}
 | 
			
		||||
              title="Variables"
 | 
			
		||||
              icon={faSquareRootVariable}
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
        </CollapsiblePanel>
 | 
			
		||||
        <section className="flex flex-col mt-auto">
 | 
			
		||||
          <MemoryPanel
 | 
			
		||||
            theme={theme}
 | 
			
		||||
            open={openPanes.includes('variables')}
 | 
			
		||||
            title="Variables"
 | 
			
		||||
            icon={faSquareRootVariable}
 | 
			
		||||
          />
 | 
			
		||||
          <Logs
 | 
			
		||||
            theme={theme}
 | 
			
		||||
            open={openPanes.includes('logs')}
 | 
			
		||||
            title="Logs"
 | 
			
		||||
            icon={faCodeCommit}
 | 
			
		||||
          />
 | 
			
		||||
          <KCLErrors
 | 
			
		||||
            theme={theme}
 | 
			
		||||
            open={openPanes.includes('kclErrors')}
 | 
			
		||||
            title="KCL Errors"
 | 
			
		||||
            iconClassNames={{ icon: 'group-open:text-destroy-30' }}
 | 
			
		||||
          />
 | 
			
		||||
        </section>
 | 
			
		||||
            <Logs
 | 
			
		||||
              theme={theme}
 | 
			
		||||
              open={openPanes.includes('logs')}
 | 
			
		||||
              title="Logs"
 | 
			
		||||
              icon={faCodeCommit}
 | 
			
		||||
            />
 | 
			
		||||
            <KCLErrors
 | 
			
		||||
              theme={theme}
 | 
			
		||||
              open={openPanes.includes('kclErrors')}
 | 
			
		||||
              title="KCL Errors"
 | 
			
		||||
              iconClassNames={{ icon: 'group-open:text-destroy-30' }}
 | 
			
		||||
            />
 | 
			
		||||
          </section>
 | 
			
		||||
        </div>
 | 
			
		||||
      </Resizable>
 | 
			
		||||
      <Stream className="absolute inset-0 z-0" />
 | 
			
		||||
      {debugPanel && (
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,5 @@
 | 
			
		||||
import {
 | 
			
		||||
  faArrowRotateBack,
 | 
			
		||||
  faCheck,
 | 
			
		||||
  faFolder,
 | 
			
		||||
  faXmark,
 | 
			
		||||
} from '@fortawesome/free-solid-svg-icons'
 | 
			
		||||
@ -8,27 +7,29 @@ import { ActionButton } from '../components/ActionButton'
 | 
			
		||||
import { AppHeader } from '../components/AppHeader'
 | 
			
		||||
import { open } from '@tauri-apps/api/dialog'
 | 
			
		||||
import { baseUnits, useStore } from '../useStore'
 | 
			
		||||
import { useState } from 'react'
 | 
			
		||||
import { useRef } from 'react'
 | 
			
		||||
import { toast } from 'react-hot-toast'
 | 
			
		||||
import { Toggle } from '../components/Toggle/Toggle'
 | 
			
		||||
import { useNavigate } from 'react-router-dom'
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
 | 
			
		||||
export const Settings = () => {
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
  useHotkeys('esc', () => navigate('/'))
 | 
			
		||||
  const {
 | 
			
		||||
    defaultDir: ogDefaultDir,
 | 
			
		||||
    setDefaultDir: saveDefaultDir,
 | 
			
		||||
    defaultProjectName: ogDefaultProjectName,
 | 
			
		||||
    setDefaultProjectName: saveDefaultProjectName,
 | 
			
		||||
    defaultUnitSystem: ogDefaultUnitSystem,
 | 
			
		||||
    setDefaultUnitSystem: saveDefaultUnitSystem,
 | 
			
		||||
    defaultBaseUnit: ogDefaultBaseUnit,
 | 
			
		||||
    setDefaultBaseUnit: saveDefaultBaseUnit,
 | 
			
		||||
    saveDebugPanel,
 | 
			
		||||
    originalDebugPanel,
 | 
			
		||||
    defaultDir,
 | 
			
		||||
    setDefaultDir,
 | 
			
		||||
    defaultProjectName,
 | 
			
		||||
    setDefaultProjectName,
 | 
			
		||||
    defaultUnitSystem,
 | 
			
		||||
    setDefaultUnitSystem,
 | 
			
		||||
    defaultBaseUnit,
 | 
			
		||||
    setDefaultBaseUnit,
 | 
			
		||||
    setDebugPanel,
 | 
			
		||||
    debugPanel,
 | 
			
		||||
    setOnboardingStatus,
 | 
			
		||||
    theme: ogTheme,
 | 
			
		||||
    setTheme: saveTheme,
 | 
			
		||||
    theme,
 | 
			
		||||
    setTheme,
 | 
			
		||||
  } = useStore((s) => ({
 | 
			
		||||
    defaultDir: s.defaultDir,
 | 
			
		||||
    setDefaultDir: s.setDefaultDir,
 | 
			
		||||
@ -38,20 +39,13 @@ export const Settings = () => {
 | 
			
		||||
    setDefaultUnitSystem: s.setDefaultUnitSystem,
 | 
			
		||||
    defaultBaseUnit: s.defaultBaseUnit,
 | 
			
		||||
    setDefaultBaseUnit: s.setDefaultBaseUnit,
 | 
			
		||||
    saveDebugPanel: s.setDebugPanel,
 | 
			
		||||
    originalDebugPanel: s.debugPanel,
 | 
			
		||||
    setDebugPanel: s.setDebugPanel,
 | 
			
		||||
    debugPanel: s.debugPanel,
 | 
			
		||||
    setOnboardingStatus: s.setOnboardingStatus,
 | 
			
		||||
    theme: s.theme,
 | 
			
		||||
    setTheme: s.setTheme,
 | 
			
		||||
  }))
 | 
			
		||||
  const [defaultDir, setDefaultDir] = useState(ogDefaultDir)
 | 
			
		||||
  const [defaultProjectName, setDefaultProjectName] =
 | 
			
		||||
    useState(ogDefaultProjectName)
 | 
			
		||||
  const [defaultUnitSystem, setDefaultUnitSystem] =
 | 
			
		||||
    useState(ogDefaultUnitSystem)
 | 
			
		||||
  const [defaultBaseUnit, setDefaultBaseUnit] = useState(ogDefaultBaseUnit)
 | 
			
		||||
  const [debugPanel, setDebugPanel] = useState(originalDebugPanel)
 | 
			
		||||
  const [theme, setTheme] = useState(ogTheme)
 | 
			
		||||
  const ogDefaultProjectName = useRef(defaultProjectName)
 | 
			
		||||
 | 
			
		||||
  async function handleDirectorySelection() {
 | 
			
		||||
    const newDirectory = await open({
 | 
			
		||||
@ -65,16 +59,6 @@ export const Settings = () => {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleSaveClick = () => {
 | 
			
		||||
    saveDefaultDir(defaultDir)
 | 
			
		||||
    saveDefaultProjectName(defaultProjectName)
 | 
			
		||||
    saveDefaultUnitSystem(defaultUnitSystem)
 | 
			
		||||
    saveDefaultBaseUnit(defaultBaseUnit)
 | 
			
		||||
    saveDebugPanel(debugPanel)
 | 
			
		||||
    saveTheme(theme)
 | 
			
		||||
    toast.success('Settings saved!')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <AppHeader showToolbar={false}>
 | 
			
		||||
@ -92,7 +76,7 @@ export const Settings = () => {
 | 
			
		||||
          Close
 | 
			
		||||
        </ActionButton>
 | 
			
		||||
      </AppHeader>
 | 
			
		||||
      <div className="mt-24 max-w-5xl mx-auto">
 | 
			
		||||
      <div className="mt-16 max-w-3xl mx-auto">
 | 
			
		||||
        <h1 className="text-4xl font-bold">User Settings</h1>
 | 
			
		||||
        {(window as any).__TAURI__ && (
 | 
			
		||||
          <SettingsSection
 | 
			
		||||
@ -103,12 +87,13 @@ export const Settings = () => {
 | 
			
		||||
              <input
 | 
			
		||||
                className="flex-1 px-2 bg-transparent"
 | 
			
		||||
                value={defaultDir.dir}
 | 
			
		||||
                onChange={(e) =>
 | 
			
		||||
                onChange={(e) => {
 | 
			
		||||
                  setDefaultDir({
 | 
			
		||||
                    base: ogDefaultDir.base,
 | 
			
		||||
                    base: defaultDir.base,
 | 
			
		||||
                    dir: e.target.value,
 | 
			
		||||
                  })
 | 
			
		||||
                }
 | 
			
		||||
                  toast.success('Default directory updated')
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
              <ActionButton
 | 
			
		||||
                Element="button"
 | 
			
		||||
@ -134,7 +119,13 @@ export const Settings = () => {
 | 
			
		||||
          <input
 | 
			
		||||
            className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
 | 
			
		||||
            value={defaultProjectName}
 | 
			
		||||
            onChange={(e) => setDefaultProjectName(e.target.value)}
 | 
			
		||||
            onChange={(e) => {
 | 
			
		||||
              setDefaultProjectName(e.target.value)
 | 
			
		||||
            }}
 | 
			
		||||
            onBlur={() => {
 | 
			
		||||
              ogDefaultProjectName.current !== defaultProjectName &&
 | 
			
		||||
                toast.success('Default project name updated')
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </SettingsSection>
 | 
			
		||||
        <SettingsSection
 | 
			
		||||
@ -146,9 +137,12 @@ export const Settings = () => {
 | 
			
		||||
            onLabel="Metric"
 | 
			
		||||
            name="settings-units"
 | 
			
		||||
            checked={defaultUnitSystem === 'metric'}
 | 
			
		||||
            onChange={(e) =>
 | 
			
		||||
              setDefaultUnitSystem(e.target.checked ? 'metric' : 'imperial')
 | 
			
		||||
            }
 | 
			
		||||
            onChange={(e) => {
 | 
			
		||||
              const newUnitSystem = e.target.checked ? 'metric' : 'imperial'
 | 
			
		||||
              setDefaultUnitSystem(newUnitSystem)
 | 
			
		||||
              setDefaultBaseUnit(baseUnits[newUnitSystem][0])
 | 
			
		||||
              toast.success('Unit system set to ' + newUnitSystem)
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </SettingsSection>
 | 
			
		||||
        <SettingsSection
 | 
			
		||||
@ -159,7 +153,10 @@ export const Settings = () => {
 | 
			
		||||
            id="base-unit"
 | 
			
		||||
            className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
 | 
			
		||||
            value={defaultBaseUnit}
 | 
			
		||||
            onChange={(e) => setDefaultBaseUnit(e.target.value)}
 | 
			
		||||
            onChange={(e) => {
 | 
			
		||||
              setDefaultBaseUnit(e.target.value)
 | 
			
		||||
              toast.success('Base unit changed to ' + e.target.value)
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            {baseUnits[defaultUnitSystem].map((unit) => (
 | 
			
		||||
              <option key={unit} value={unit}>
 | 
			
		||||
@ -175,7 +172,12 @@ export const Settings = () => {
 | 
			
		||||
          <Toggle
 | 
			
		||||
            name="settings-debug-panel"
 | 
			
		||||
            checked={debugPanel}
 | 
			
		||||
            onChange={(e) => setDebugPanel(e.target.checked)}
 | 
			
		||||
            onChange={(e) => {
 | 
			
		||||
              setDebugPanel(e.target.checked)
 | 
			
		||||
              toast.success(
 | 
			
		||||
                'Debug panel toggled ' + (e.target.checked ? 'on' : 'off')
 | 
			
		||||
              )
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </SettingsSection>
 | 
			
		||||
        <SettingsSection
 | 
			
		||||
@ -187,7 +189,15 @@ export const Settings = () => {
 | 
			
		||||
            offLabel="Dark"
 | 
			
		||||
            onLabel="Light"
 | 
			
		||||
            checked={theme === 'light'}
 | 
			
		||||
            onChange={(e) => setTheme(e.target.checked ? 'light' : 'dark')}
 | 
			
		||||
            onChange={(e) => {
 | 
			
		||||
              const newTheme = e.target.checked ? 'light' : 'dark'
 | 
			
		||||
              setTheme(newTheme)
 | 
			
		||||
              toast.success(
 | 
			
		||||
                newTheme.slice(0, 1).toLocaleUpperCase() +
 | 
			
		||||
                  newTheme.slice(1) +
 | 
			
		||||
                  ' mode activated'
 | 
			
		||||
              )
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </SettingsSection>
 | 
			
		||||
        <SettingsSection
 | 
			
		||||
@ -204,19 +214,6 @@ export const Settings = () => {
 | 
			
		||||
            Replay Onboarding
 | 
			
		||||
          </ActionButton>
 | 
			
		||||
        </SettingsSection>
 | 
			
		||||
        <ActionButton
 | 
			
		||||
          className="hover:border-succeed-50"
 | 
			
		||||
          onClick={handleSaveClick}
 | 
			
		||||
          icon={{
 | 
			
		||||
            icon: faCheck,
 | 
			
		||||
            bgClassName:
 | 
			
		||||
              'bg-succeed-80 group-hover:bg-succeed-70 hover:bg-succeed-70',
 | 
			
		||||
            iconClassName:
 | 
			
		||||
              'text-succeed-20 group-hover:text-succeed-10 hover:text-succeed-10',
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          Save Settings
 | 
			
		||||
        </ActionButton>
 | 
			
		||||
      </div>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
@ -233,7 +230,7 @@ export function SettingsSection({
 | 
			
		||||
  children,
 | 
			
		||||
}: SettingsSectionProps) {
 | 
			
		||||
  return (
 | 
			
		||||
    <section className="my-8 first-of-type:mt-16 last-of-type:mb-16 flex gap-12 items-start">
 | 
			
		||||
    <section className="my-16 last-of-type:mb-24 grid grid-cols-2 gap-12 items-start">
 | 
			
		||||
      <div className="w-80">
 | 
			
		||||
        <h2 className="text-2xl">{title}</h2>
 | 
			
		||||
        <p className="mt-2 text-sm">{description}</p>
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user