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:
15
src/App.tsx
15
src/App.tsx
@ -9,12 +9,7 @@ import { DebugPanel } from './components/DebugPanel'
|
|||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import { asyncLexer } from './lang/tokeniser'
|
import { asyncLexer } from './lang/tokeniser'
|
||||||
import { abstractSyntaxTree } from './lang/abstractSyntaxTree'
|
import { abstractSyntaxTree } from './lang/abstractSyntaxTree'
|
||||||
import {
|
import { _executor } from './lang/executor'
|
||||||
_executor,
|
|
||||||
ProgramMemory,
|
|
||||||
ExtrudeGroup,
|
|
||||||
SketchGroup,
|
|
||||||
} from './lang/executor'
|
|
||||||
import CodeMirror from '@uiw/react-codemirror'
|
import CodeMirror from '@uiw/react-codemirror'
|
||||||
import { langs } from '@uiw/codemirror-extensions-langs'
|
import { langs } from '@uiw/codemirror-extensions-langs'
|
||||||
import { linter, lintGutter } from '@codemirror/lint'
|
import { linter, lintGutter } from '@codemirror/lint'
|
||||||
@ -358,7 +353,7 @@ export function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="h-screen relative flex flex-col"
|
className="h-screen overflow-hidden relative flex flex-col"
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
>
|
>
|
||||||
<AppHeader
|
<AppHeader
|
||||||
@ -371,7 +366,7 @@ export function App() {
|
|||||||
<ModalContainer />
|
<ModalContainer />
|
||||||
<Resizable
|
<Resizable
|
||||||
className={
|
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'
|
(isMouseDownInStream || onboardingStatus === 'camera'
|
||||||
? ' pointer-events-none '
|
? ' pointer-events-none '
|
||||||
: ' ') +
|
: ' ') +
|
||||||
@ -390,6 +385,7 @@ 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',
|
'hover:bg-liquid-30/40 dark:hover:bg-liquid-10/40 bg-transparent transition-colors duration-100 transition-ease-out delay-100',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div className="h-full flex flex-col justify-between">
|
||||||
<CollapsiblePanel
|
<CollapsiblePanel
|
||||||
title="Code"
|
title="Code"
|
||||||
icon={faCode}
|
icon={faCode}
|
||||||
@ -421,7 +417,7 @@ export function App() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CollapsiblePanel>
|
</CollapsiblePanel>
|
||||||
<section className="flex flex-col mt-auto">
|
<section className="flex flex-col">
|
||||||
<MemoryPanel
|
<MemoryPanel
|
||||||
theme={theme}
|
theme={theme}
|
||||||
open={openPanes.includes('variables')}
|
open={openPanes.includes('variables')}
|
||||||
@ -441,6 +437,7 @@ export function App() {
|
|||||||
iconClassNames={{ icon: 'group-open:text-destroy-30' }}
|
iconClassNames={{ icon: 'group-open:text-destroy-30' }}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
</Resizable>
|
</Resizable>
|
||||||
<Stream className="absolute inset-0 z-0" />
|
<Stream className="absolute inset-0 z-0" />
|
||||||
{debugPanel && (
|
{debugPanel && (
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
faArrowRotateBack,
|
faArrowRotateBack,
|
||||||
faCheck,
|
|
||||||
faFolder,
|
faFolder,
|
||||||
faXmark,
|
faXmark,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
@ -8,27 +7,29 @@ import { ActionButton } from '../components/ActionButton'
|
|||||||
import { AppHeader } from '../components/AppHeader'
|
import { AppHeader } from '../components/AppHeader'
|
||||||
import { open } from '@tauri-apps/api/dialog'
|
import { open } from '@tauri-apps/api/dialog'
|
||||||
import { baseUnits, useStore } from '../useStore'
|
import { baseUnits, useStore } from '../useStore'
|
||||||
import { useState } from 'react'
|
import { useRef } from 'react'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import { Toggle } from '../components/Toggle/Toggle'
|
import { Toggle } from '../components/Toggle/Toggle'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
|
|
||||||
export const Settings = () => {
|
export const Settings = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
useHotkeys('esc', () => navigate('/'))
|
||||||
const {
|
const {
|
||||||
defaultDir: ogDefaultDir,
|
defaultDir,
|
||||||
setDefaultDir: saveDefaultDir,
|
setDefaultDir,
|
||||||
defaultProjectName: ogDefaultProjectName,
|
defaultProjectName,
|
||||||
setDefaultProjectName: saveDefaultProjectName,
|
setDefaultProjectName,
|
||||||
defaultUnitSystem: ogDefaultUnitSystem,
|
defaultUnitSystem,
|
||||||
setDefaultUnitSystem: saveDefaultUnitSystem,
|
setDefaultUnitSystem,
|
||||||
defaultBaseUnit: ogDefaultBaseUnit,
|
defaultBaseUnit,
|
||||||
setDefaultBaseUnit: saveDefaultBaseUnit,
|
setDefaultBaseUnit,
|
||||||
saveDebugPanel,
|
setDebugPanel,
|
||||||
originalDebugPanel,
|
debugPanel,
|
||||||
setOnboardingStatus,
|
setOnboardingStatus,
|
||||||
theme: ogTheme,
|
theme,
|
||||||
setTheme: saveTheme,
|
setTheme,
|
||||||
} = useStore((s) => ({
|
} = useStore((s) => ({
|
||||||
defaultDir: s.defaultDir,
|
defaultDir: s.defaultDir,
|
||||||
setDefaultDir: s.setDefaultDir,
|
setDefaultDir: s.setDefaultDir,
|
||||||
@ -38,20 +39,13 @@ export const Settings = () => {
|
|||||||
setDefaultUnitSystem: s.setDefaultUnitSystem,
|
setDefaultUnitSystem: s.setDefaultUnitSystem,
|
||||||
defaultBaseUnit: s.defaultBaseUnit,
|
defaultBaseUnit: s.defaultBaseUnit,
|
||||||
setDefaultBaseUnit: s.setDefaultBaseUnit,
|
setDefaultBaseUnit: s.setDefaultBaseUnit,
|
||||||
saveDebugPanel: s.setDebugPanel,
|
setDebugPanel: s.setDebugPanel,
|
||||||
originalDebugPanel: s.debugPanel,
|
debugPanel: s.debugPanel,
|
||||||
setOnboardingStatus: s.setOnboardingStatus,
|
setOnboardingStatus: s.setOnboardingStatus,
|
||||||
theme: s.theme,
|
theme: s.theme,
|
||||||
setTheme: s.setTheme,
|
setTheme: s.setTheme,
|
||||||
}))
|
}))
|
||||||
const [defaultDir, setDefaultDir] = useState(ogDefaultDir)
|
const ogDefaultProjectName = useRef(defaultProjectName)
|
||||||
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)
|
|
||||||
|
|
||||||
async function handleDirectorySelection() {
|
async function handleDirectorySelection() {
|
||||||
const newDirectory = await open({
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppHeader showToolbar={false}>
|
<AppHeader showToolbar={false}>
|
||||||
@ -92,7 +76,7 @@ export const Settings = () => {
|
|||||||
Close
|
Close
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</AppHeader>
|
</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>
|
<h1 className="text-4xl font-bold">User Settings</h1>
|
||||||
{(window as any).__TAURI__ && (
|
{(window as any).__TAURI__ && (
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
@ -103,12 +87,13 @@ export const Settings = () => {
|
|||||||
<input
|
<input
|
||||||
className="flex-1 px-2 bg-transparent"
|
className="flex-1 px-2 bg-transparent"
|
||||||
value={defaultDir.dir}
|
value={defaultDir.dir}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
setDefaultDir({
|
setDefaultDir({
|
||||||
base: ogDefaultDir.base,
|
base: defaultDir.base,
|
||||||
dir: e.target.value,
|
dir: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
toast.success('Default directory updated')
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
Element="button"
|
Element="button"
|
||||||
@ -134,7 +119,13 @@ export const Settings = () => {
|
|||||||
<input
|
<input
|
||||||
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
|
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
|
||||||
value={defaultProjectName}
|
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>
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
@ -146,9 +137,12 @@ export const Settings = () => {
|
|||||||
onLabel="Metric"
|
onLabel="Metric"
|
||||||
name="settings-units"
|
name="settings-units"
|
||||||
checked={defaultUnitSystem === 'metric'}
|
checked={defaultUnitSystem === 'metric'}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
setDefaultUnitSystem(e.target.checked ? 'metric' : 'imperial')
|
const newUnitSystem = e.target.checked ? 'metric' : 'imperial'
|
||||||
}
|
setDefaultUnitSystem(newUnitSystem)
|
||||||
|
setDefaultBaseUnit(baseUnits[newUnitSystem][0])
|
||||||
|
toast.success('Unit system set to ' + newUnitSystem)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
@ -159,7 +153,10 @@ export const Settings = () => {
|
|||||||
id="base-unit"
|
id="base-unit"
|
||||||
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
|
className="block w-full px-3 py-1 border border-chalkboard-30 bg-transparent"
|
||||||
value={defaultBaseUnit}
|
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) => (
|
{baseUnits[defaultUnitSystem].map((unit) => (
|
||||||
<option key={unit} value={unit}>
|
<option key={unit} value={unit}>
|
||||||
@ -175,7 +172,12 @@ export const Settings = () => {
|
|||||||
<Toggle
|
<Toggle
|
||||||
name="settings-debug-panel"
|
name="settings-debug-panel"
|
||||||
checked={debugPanel}
|
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>
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
@ -187,7 +189,15 @@ export const Settings = () => {
|
|||||||
offLabel="Dark"
|
offLabel="Dark"
|
||||||
onLabel="Light"
|
onLabel="Light"
|
||||||
checked={theme === '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>
|
||||||
<SettingsSection
|
<SettingsSection
|
||||||
@ -204,19 +214,6 @@ export const Settings = () => {
|
|||||||
Replay Onboarding
|
Replay Onboarding
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</SettingsSection>
|
</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>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@ -233,7 +230,7 @@ export function SettingsSection({
|
|||||||
children,
|
children,
|
||||||
}: SettingsSectionProps) {
|
}: SettingsSectionProps) {
|
||||||
return (
|
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">
|
<div className="w-80">
|
||||||
<h2 className="text-2xl">{title}</h2>
|
<h2 className="text-2xl">{title}</h2>
|
||||||
<p className="mt-2 text-sm">{description}</p>
|
<p className="mt-2 text-sm">{description}</p>
|
||||||
|
Reference in New Issue
Block a user