Prebake in settings

Update prebake without reload
This commit is contained in:
John Björk 2026-01-30 20:52:15 +01:00
parent b4e171f5b4
commit 9f2237e11d
No known key found for this signature in database
GPG key ID: 303B6ADF2F569B0A
11 changed files with 292 additions and 37 deletions

View file

@ -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>}
/>
);
}

View file

@ -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);
}

View file

@ -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>
);
}

View file

@ -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
//

View file

@ -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) {

View file

@ -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);