mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-21 20:55:12 -04:00
Prebake in settings
Update prebake without reload
This commit is contained in:
@@ -19,7 +19,7 @@ import { basicSetup } from './basicSetup.mjs';
|
||||
import { evalBlock } from './block_utilities.mjs';
|
||||
import { flash, isFlashEnabled } from './flash.mjs';
|
||||
import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
|
||||
import { keybindings } from './keybindings.mjs';
|
||||
import { keybindings, prebakeField } from './keybindings.mjs';
|
||||
import { jumpToCharacter } from './labelJump.mjs';
|
||||
import { getSliderWidgets, sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||
import { activateTheme, initTheme, theme } from './themes.mjs';
|
||||
@@ -412,6 +412,10 @@ export class StrudelMirror {
|
||||
setAutocompletionEnabled(enabled) {
|
||||
this.reconfigureExtension('isAutoCompletionEnabled', enabled);
|
||||
}
|
||||
updatePrebake(prebake) {
|
||||
logger('[repl] prebake updated');
|
||||
this.prebaked = prebake();
|
||||
}
|
||||
updateSettings(settings) {
|
||||
this.setFontSize(settings.fontSize);
|
||||
this.setFontFamily(settings.fontFamily);
|
||||
@@ -472,6 +476,85 @@ export class StrudelMirror {
|
||||
}
|
||||
}
|
||||
|
||||
export class PrebakeCodeMirror {
|
||||
constructor(initialCode, storePrebake, containerRef, editorRef, settings) {
|
||||
this.storePrebake = storePrebake;
|
||||
const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
|
||||
const initialSettings = Object.keys(compartments).map((key) =>
|
||||
compartments[key].of(extensions[key](parseBooleans(settings[key]))),
|
||||
);
|
||||
initTheme(settings.theme);
|
||||
let state = EditorState.create({
|
||||
doc: initialCode,
|
||||
extensions: [
|
||||
...initialSettings,
|
||||
basicSetup,
|
||||
javascript(),
|
||||
javascriptLanguage.data.of({
|
||||
closeBrackets: { brackets: ['(', '[', '{', "'", '"', '<'] },
|
||||
bracketMatching: { brackets: ['(', '[', '{', "'", '"', '<'] },
|
||||
}),
|
||||
syntaxHighlighting(defaultHighlightStyle),
|
||||
EditorView.updateListener.of((v) => {
|
||||
if (v.docChanged) {
|
||||
this.code = v.state.doc.toString();
|
||||
}
|
||||
}),
|
||||
drawSelection({ cursorBlinkRate: 0 }),
|
||||
Prec.highest(
|
||||
keymap.of([
|
||||
{
|
||||
key: 'Ctrl-Enter',
|
||||
mac: 'Meta-Enter',
|
||||
run: () => {
|
||||
this.savePrebake();
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'Alt-Enter',
|
||||
run: () => {
|
||||
this.savePrebake();
|
||||
},
|
||||
},
|
||||
]),
|
||||
),
|
||||
prebakeField,
|
||||
],
|
||||
});
|
||||
editorRef.current = state;
|
||||
this.code = initialCode;
|
||||
this.view = new EditorView({
|
||||
state,
|
||||
parent: containerRef.current,
|
||||
});
|
||||
}
|
||||
|
||||
async savePrebake() {
|
||||
flash(this.view);
|
||||
this.storePrebake(this.code);
|
||||
logger('[prebake] prebake saved');
|
||||
}
|
||||
|
||||
toggleComment() {
|
||||
try {
|
||||
// Honor selections; toggleLineComment handles both selections and
|
||||
// single line
|
||||
toggleLineComment(this.view);
|
||||
} catch (err) {
|
||||
console.error('Error handling repl-toggle-comment event', err);
|
||||
}
|
||||
}
|
||||
|
||||
setCode(code) {
|
||||
const changes = {
|
||||
from: 0,
|
||||
to: this.view.state.doc.length,
|
||||
insert: code,
|
||||
};
|
||||
this.view.dispatch({ changes });
|
||||
}
|
||||
}
|
||||
|
||||
export function parseBooleans(value) {
|
||||
return { true: true, false: false }[value] ?? value;
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@ export * from './flash.mjs';
|
||||
export * from './slider.mjs';
|
||||
export * from './themes.mjs';
|
||||
export * from './widget.mjs';
|
||||
export { Vim } from './keybindings.mjs';
|
||||
export { Vim, prebakeField } from './keybindings.mjs';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defaultKeymap } from '@codemirror/commands';
|
||||
import { Prec, EditorState } from '@codemirror/state';
|
||||
import { Prec, EditorState, StateField } from '@codemirror/state';
|
||||
import { keymap, ViewPlugin } from '@codemirror/view';
|
||||
// import { searchKeymap } from '@codemirror/search';
|
||||
import { emacs } from '@replit/codemirror-emacs';
|
||||
@@ -9,6 +9,15 @@ import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
||||
import { helix, commands } from 'codemirror-helix';
|
||||
import { logger } from '@strudel/core';
|
||||
|
||||
export const prebakeField = StateField.define({
|
||||
create() {
|
||||
return 'PrebakeEditor';
|
||||
},
|
||||
update(value, _tr) {
|
||||
return value;
|
||||
},
|
||||
});
|
||||
|
||||
const vscodePlugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
constructor() {}
|
||||
@@ -98,7 +107,11 @@ try {
|
||||
Vim.defineAction('strudelToggleComment', (cm) => {
|
||||
const view = cm?.view || cm;
|
||||
try {
|
||||
const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true });
|
||||
const toggleEventName =
|
||||
view?.cm6?.state?.field(prebakeField, false) !== undefined
|
||||
? 'prebake-toggle-comment'
|
||||
: 'repl-toggle-comment';
|
||||
const ev = new CustomEvent(toggleEventName, { detail: { source: 'vim', view }, cancelable: true });
|
||||
document.dispatchEvent(ev);
|
||||
} catch (e) {
|
||||
console.error('strudelToggleComment dispatch failed', e);
|
||||
@@ -119,6 +132,17 @@ try {
|
||||
// :w to evaluate
|
||||
Vim.defineEx('write', 'w', (cm) => {
|
||||
const view = cm?.view || cm; // CM6 Vim passes either an object with view or the view itself
|
||||
const isPrebake = view?.cm6?.state?.field(prebakeField, false) !== undefined;
|
||||
if (isPrebake) {
|
||||
let prebakeEventHandled = false;
|
||||
try {
|
||||
const ev = new CustomEvent('prebake-evaluate', { detail: { source: 'vim', view }, cancelable: true });
|
||||
prebakeEventHandled = document.dispatchEvent(ev) === false; // false means preventDefault was called
|
||||
return;
|
||||
} catch (e) {
|
||||
console.error('Error dispatching repl-evaluate event', e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
view?.focus?.();
|
||||
// Let the app know this came from Vim :w
|
||||
|
||||
@@ -3,3 +3,4 @@ import { transpiler } from './transpiler.mjs';
|
||||
export * from './transpiler.mjs';
|
||||
|
||||
export const evaluate = (code) => _evaluate(code, transpiler);
|
||||
export const evaluateUserPrebake = (code) => _evaluate(code, transpiler, { prebake: true });
|
||||
|
||||
@@ -25,6 +25,7 @@ export function transpiler(input, options = {}) {
|
||||
emitMiniLocations = true,
|
||||
emitWidgets = true,
|
||||
blockBased = false,
|
||||
prebake = false,
|
||||
range = [],
|
||||
} = options;
|
||||
|
||||
@@ -260,7 +261,9 @@ export function transpiler(input, options = {}) {
|
||||
// For block-based eval, add silence as the return value when block ends with declaration
|
||||
body.push(silenceExpression);
|
||||
} else {
|
||||
throw new Error('unexpected ast format without body expression');
|
||||
if (!prebake) {
|
||||
throw new Error('unexpected ast format without body expression');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +276,7 @@ export function transpiler(input, options = {}) {
|
||||
}
|
||||
|
||||
// add return to last statement
|
||||
if (addReturn) {
|
||||
if (addReturn && !prebake) {
|
||||
const { expression } = body[body.length - 1];
|
||||
body[body.length - 1] = {
|
||||
type: 'ReturnStatement',
|
||||
|
||||
@@ -17,22 +17,39 @@ export function SpecialActionButton(props) {
|
||||
);
|
||||
}
|
||||
|
||||
export function ActionInput({ label, className, ...props }) {
|
||||
return (
|
||||
<label className={cx('inline-flex items-center cursor-pointer ', className)}>
|
||||
<input {...props} className="sr-only peer" />
|
||||
export function IconButton(props) {
|
||||
const { Icon, label, children, labelIsHidden, className, ...buttonProps } = props;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cx('space-x-1 max-w-[300px] inline-flex items-center cursor-pointer hover:opacity-50', className)}
|
||||
title={label}
|
||||
{...buttonProps}
|
||||
>
|
||||
<Icon className="w-5 h-5" />
|
||||
<span className="max-w-[300px]">{label}</span>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function ActionInput({ Icon, label, className, ...props }) {
|
||||
return (
|
||||
<label className={cx('space-x-1 inline-flex items-center cursor-pointer ', className)}>
|
||||
<input {...props} className="sr-only peer" />
|
||||
<Icon className="w-5 h-5 peer-hover:opacity-50" />
|
||||
<span className="inline-flex items-center peer-hover:opacity-50">{label}</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export function SpecialActionInput({ className, ...props }) {
|
||||
export function SpecialActionInput({ Icon, className, ...props }) {
|
||||
return (
|
||||
<ActionInput
|
||||
{...props}
|
||||
Icon={Icon}
|
||||
className={className}
|
||||
label={<span className="bg-background p-2 max-w-[300px]">{props.label}</span>}
|
||||
label={<span className="max-w-[300px]">{props.label}</span>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,38 +1,52 @@
|
||||
import { errorLogger } from '@strudel/core';
|
||||
import { useSettings, storePrebakeScript } from '../../../settings.mjs';
|
||||
import { SpecialActionInput } from '../button/action-button';
|
||||
import { confirmDialog, SETTING_CHANGE_RELOAD_MSG } from '@src/repl/util.mjs';
|
||||
import { confirmDialog, PREBAKE_CHANGE_MSG } from '@src/repl/util.mjs';
|
||||
import { DocumentArrowDownIcon } from '@heroicons/react/16/solid';
|
||||
|
||||
async function importScript(script) {
|
||||
async function importScript(script, updateEditor) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(script);
|
||||
|
||||
reader.onload = () => {
|
||||
const text = reader.result;
|
||||
storePrebakeScript(text);
|
||||
updateEditor && updateEditor(text);
|
||||
};
|
||||
|
||||
reader.onerror = () => {
|
||||
errorLogger(new Error('failed to import prebake script'), 'importScript');
|
||||
};
|
||||
}
|
||||
export function ImportPrebakeScriptButton() {
|
||||
|
||||
export async function exportScript(script) {
|
||||
const blob = new Blob([script], { type: 'application/javascript' });
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.href = window.URL.createObjectURL(blob);
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
downloadLink.download = `prebake_${date}.strudel`;
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
}
|
||||
|
||||
export function ImportPrebakeScriptButton({ updateEditor }) {
|
||||
const settings = useSettings();
|
||||
|
||||
return (
|
||||
<SpecialActionInput
|
||||
Icon={DocumentArrowDownIcon}
|
||||
type="file"
|
||||
label="import prebake script"
|
||||
accept=".strudel"
|
||||
label="import"
|
||||
accept=".strudel,.js"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files[0];
|
||||
const confirmed = await confirmDialog(SETTING_CHANGE_RELOAD_MSG);
|
||||
const confirmed = await confirmDialog(PREBAKE_CHANGE_MSG);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await importScript(file);
|
||||
window.location.reload();
|
||||
await importScript(file, updateEditor);
|
||||
} catch (e) {
|
||||
errorLogger(e);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs';
|
||||
import { themes } from '@strudel/codemirror';
|
||||
import { defaultSettings, settingsMap, useSettings, storePrebakeScript, setSettingsTab } from '../../../settings.mjs';
|
||||
import { themes, codemirrorSettings, PrebakeCodeMirror } from '@strudel/codemirror';
|
||||
import { confirmAndReloadPage, isUdels } from '../../util.mjs';
|
||||
import { ButtonGroup } from './Forms.jsx';
|
||||
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
|
||||
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
|
||||
import { confirmDialog } from '../../util.mjs';
|
||||
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio';
|
||||
import { SpecialActionButton } from '../button/action-button.jsx';
|
||||
import { ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx';
|
||||
import { IconButton, SpecialActionButton } from '../button/action-button.jsx';
|
||||
import { exportScript, ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { Code } from '../Code.jsx';
|
||||
import { DocumentArrowUpIcon, DocumentCheckIcon } from '@heroicons/react/16/solid';
|
||||
|
||||
function cx(...classes) {
|
||||
// : Array<string | undefined>
|
||||
@@ -123,7 +126,7 @@ const fontFamilyOptions = {
|
||||
galactico: 'galactico',
|
||||
};
|
||||
|
||||
export function SettingsTab({ started }) {
|
||||
function MainSettingsContent({ started }) {
|
||||
const {
|
||||
theme,
|
||||
keybindings,
|
||||
@@ -155,7 +158,7 @@ export function SettingsTab({ started }) {
|
||||
const shouldAlwaysSync = isUdels();
|
||||
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
||||
return (
|
||||
<div className="p-4 text-foreground space-y-4 w-full" style={{ fontFamily }}>
|
||||
<div className="p-4 text-foreground space-y-4 w-full overflow-auto" style={{ fontFamily }}>
|
||||
{canChangeAudioDevice && (
|
||||
<FormItem label="Audio Output Device">
|
||||
<AudioDeviceSelector
|
||||
@@ -233,14 +236,6 @@ export function SettingsTab({ started }) {
|
||||
/>
|
||||
</FormItem>
|
||||
</div>
|
||||
<FormItem label="Prebake">
|
||||
<ImportPrebakeScriptButton />
|
||||
<Checkbox
|
||||
label="Include prebake script in share"
|
||||
onChange={(cbEvent) => settingsMap.setKey('includePrebakeScriptInShare', cbEvent.target.checked)}
|
||||
value={includePrebakeScriptInShare}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem label="Keybindings">
|
||||
<ButtonGroup
|
||||
@@ -362,3 +357,94 @@ export function SettingsTab({ started }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PrebakeSettingsContent() {
|
||||
const { fontFamily, includePrebakeScriptInShare, prebakeScript } = useSettings();
|
||||
const init = useCallback(() => {
|
||||
const settings = codemirrorSettings.get();
|
||||
// TODO: This instance need to be created at the top level to not create a new instance here
|
||||
const prebakeCodemirror = new PrebakeCodeMirror(
|
||||
prebakeScript,
|
||||
(code) => storePrebakeScript(code),
|
||||
containerRef,
|
||||
editorRef,
|
||||
settings,
|
||||
);
|
||||
editorRef.current = prebakeCodemirror;
|
||||
}, []);
|
||||
const editorRef = useRef();
|
||||
const containerRef = useRef();
|
||||
const handleSaveEvent = async (e) => {
|
||||
await editorRef.current?.savePrebake();
|
||||
e?.cancelable && e.preventDefault?.();
|
||||
};
|
||||
const handleToggleComment = (e) => {
|
||||
editorRef.current?.toggleComment();
|
||||
e?.cancelable && e.preventDefault?.();
|
||||
};
|
||||
const handleSetCode = (code) => {
|
||||
editorRef.current?.setCode(code);
|
||||
};
|
||||
useEffect(() => {
|
||||
document.addEventListener('prebake-evaluate', handleSaveEvent);
|
||||
document.addEventListener('prebake-toggle-comment', handleToggleComment);
|
||||
return () => {
|
||||
document.removeEventListener('prebake-evaluate', handleSaveEvent);
|
||||
document.removeEventListener('prebake-toggle-comment', handleToggleComment);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<div className="text-foreground w-full overflow-auto" style={{ fontFamily }}>
|
||||
<div className="px-4 py-1 flex flex-row items-center space-x-4 justify-between">
|
||||
<IconButton
|
||||
Icon={DocumentCheckIcon}
|
||||
onClick={async () => {
|
||||
await editorRef.current?.savePrebake();
|
||||
}}
|
||||
>
|
||||
save
|
||||
</IconButton>
|
||||
<div className="flex flex-row space-x-2">
|
||||
<ImportPrebakeScriptButton updateEditor={handleSetCode} />
|
||||
<IconButton
|
||||
Icon={DocumentArrowUpIcon}
|
||||
onClick={async () => {
|
||||
console.log('Exporting');
|
||||
await exportScript(prebakeScript);
|
||||
}}
|
||||
>
|
||||
export
|
||||
</IconButton>
|
||||
</div>
|
||||
<Checkbox
|
||||
label="Include prebake script in share"
|
||||
onChange={(cbEvent) => settingsMap.setKey('includePrebakeScriptInShare', cbEvent.target.checked)}
|
||||
value={includePrebakeScriptInShare}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col overflow-hidden h-full border-t border-muted">
|
||||
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export function SettingsTab({ started }) {
|
||||
const { settingsTab } = useSettings();
|
||||
return (
|
||||
<div className="w-full h-full text-foreground flex flex-col overflow-hidden">
|
||||
<div className="px-2 shrink-0 h-8 space-x-4 flex max-w-full overflow-x-auto border-b border-muted">
|
||||
<ButtonGroup
|
||||
wrap
|
||||
value={settingsTab}
|
||||
onChange={(value) => setSettingsTab(value)}
|
||||
items={{
|
||||
settings: 'settings',
|
||||
prebake: 'prebake',
|
||||
}}
|
||||
></ButtonGroup>
|
||||
</div>
|
||||
{settingsTab === 'settings' && <MainSettingsContent started={started} />}
|
||||
{settingsTab === 'prebake' && <PrebakeSettingsContent />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
import { code2hash, getPerformanceTimeSeconds, logger, silence } from '@strudel/core';
|
||||
import { getDrawContext } from '@strudel/draw';
|
||||
import { evaluate, transpiler } from '@strudel/transpiler';
|
||||
import { evaluate, evaluateUserPrebake, transpiler } from '@strudel/transpiler';
|
||||
import {
|
||||
getAudioContextCurrentTime,
|
||||
renderPatternAudio,
|
||||
@@ -89,7 +89,7 @@ export function useReplContext() {
|
||||
prebake: async () =>
|
||||
Promise.all([modulesLoading, presets]).then(() => {
|
||||
if (prebakeScript?.length) {
|
||||
return evaluate(prebakeScript ?? '');
|
||||
return evaluateUserPrebake(prebakeScript ?? '');
|
||||
}
|
||||
}),
|
||||
onUpdateState: (state) => {
|
||||
@@ -181,6 +181,16 @@ export function useReplContext() {
|
||||
editorRef.current?.updateSettings(editorSettings);
|
||||
}, [_settings]);
|
||||
|
||||
useEffect(() => {
|
||||
const prebake = async () =>
|
||||
Promise.all([modulesLoading, presets]).then(() => {
|
||||
if (prebakeScript?.length) {
|
||||
return evaluateUserPrebake(prebakeScript ?? '');
|
||||
}
|
||||
});
|
||||
editorRef.current?.updatePrebake(prebake);
|
||||
}, [prebakeScript]);
|
||||
|
||||
//
|
||||
// UI Actions
|
||||
//
|
||||
|
||||
@@ -108,6 +108,7 @@ export function confirmDialog(msg) {
|
||||
});
|
||||
}
|
||||
export const SETTING_CHANGE_RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?';
|
||||
export const PREBAKE_CHANGE_MSG = 'Warning: This will overwrite the current prebake.\nContinue?';
|
||||
export function confirmAndReloadPage(onSuccess) {
|
||||
confirmDialog(SETTING_CHANGE_RELOAD_MSG).then((r) => {
|
||||
if (r == true) {
|
||||
|
||||
@@ -18,6 +18,20 @@ export const soundFilterType = {
|
||||
ALL: 'all',
|
||||
};
|
||||
|
||||
const initialPrebakeScript = `// Prebake script
|
||||
//
|
||||
// This is code that is loaded before your pattern is run.
|
||||
// You can use it to define custom functions to use in any pattern.
|
||||
//
|
||||
// This is an initial example script. You can edit it to add
|
||||
// your own funtions.
|
||||
//
|
||||
// To use a script shared by some other user you can use
|
||||
// the import-button or paste the script in this editor.
|
||||
|
||||
const bigRoom = register('bigRoom', (pat) => pat.room(8).roomsize(4))
|
||||
`;
|
||||
|
||||
export const defaultSettings = {
|
||||
activeFooter: 'intro',
|
||||
keybindings: 'codemirror',
|
||||
@@ -47,13 +61,14 @@ export const defaultSettings = {
|
||||
isPanelPinned: false,
|
||||
isPanelOpen: true,
|
||||
userPatterns: '{}',
|
||||
prebakeScript: '',
|
||||
prebakeScript: initialPrebakeScript,
|
||||
audioEngineTarget: audioEngineTargets.webaudio,
|
||||
isButtonRowHidden: false,
|
||||
isCSSAnimationDisabled: false,
|
||||
maxPolyphony: 128,
|
||||
multiChannelOrbits: false,
|
||||
includePrebakeScriptInShare: true,
|
||||
settingsTab: 'settings',
|
||||
};
|
||||
|
||||
let search = null;
|
||||
@@ -115,6 +130,7 @@ export function useSettings() {
|
||||
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
|
||||
export const setPanelPinned = (bool) => settingsMap.setKey('isPanelPinned', bool);
|
||||
export const setIsPanelOpened = (bool) => settingsMap.setKey('isPanelOpen', bool);
|
||||
export const setSettingsTab = (tab) => settingsMap.setKey('settingsTab', tab);
|
||||
|
||||
export const storePrebakeScript = (script) => settingsMap.setKey('prebakeScript', script);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user