diff --git a/packages/core/evaluate.mjs b/packages/core/evaluate.mjs index 23cd44c1..e3e73d59 100644 --- a/packages/core/evaluate.mjs +++ b/packages/core/evaluate.mjs @@ -4,8 +4,6 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { isPattern } from './index.mjs'; - export const evalScope = async (...args) => { const results = await Promise.allSettled(args); const modules = results.filter((result) => result.status === 'fulfilled').map((r) => r.value); @@ -39,6 +37,7 @@ function safeEval(str, options = {}) { export const evaluate = async (code, transpiler, transpilerOptions) => { let meta = {}; + if (transpiler) { // transform syntactically correct js code to semantically usable code const transpiled = transpiler(code, transpilerOptions); diff --git a/website/src/components/Oven/Oven.jsx b/website/src/components/Oven/Oven.jsx index a4199b5e..eb3c8692 100644 --- a/website/src/components/Oven/Oven.jsx +++ b/website/src/components/Oven/Oven.jsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { loadFeaturedPatterns, loadPublicPatterns } from '@src/user_pattern_utils.mjs'; import { MiniRepl } from '@src/docs/MiniRepl'; -import { PatternLabel } from '@src/repl/panel/PatternsTab'; +import { PatternLabel } from '@src/repl/components/panel/PatternsTab'; function PatternList({ patterns }) { return ( diff --git a/website/src/components/Udels/UdelFrame.jsx b/website/src/components/Udels/UdelFrame.jsx new file mode 100644 index 00000000..8bbbe14e --- /dev/null +++ b/website/src/components/Udels/UdelFrame.jsx @@ -0,0 +1,32 @@ +import { useRef } from 'react'; + +export function UdelFrame({ onEvaluate, hash, instance }) { + const ref = useRef(); + window.addEventListener('message', (message) => { + const childWindow = ref?.current?.contentWindow; + if (message == null || message.source !== childWindow) { + return; // Skip message in this event listener + } + onEvaluate(message.data); + }); + + const url = new URL(window.location.origin); + url.hash = hash; + url.searchParams.append('instance', instance); + const source = url.toString(); + + return ( + + ); +} diff --git a/website/src/components/Udels/Udels.jsx b/website/src/components/Udels/Udels.jsx new file mode 100644 index 00000000..4a5f404f --- /dev/null +++ b/website/src/components/Udels/Udels.jsx @@ -0,0 +1,72 @@ +import { code2hash } from '@strudel/core'; + +import { UdelFrame } from './UdelFrame'; +import { useState } from 'react'; +import UdelsHeader from './UdelsHeader'; + +const defaultHash = 'c3RhY2soCiAgCik%3D'; + +const getHashesFromUrl = () => { + return window.location.hash?.slice(1).split(','); +}; +const updateURLHashes = (hashes) => { + const newHash = '#' + hashes.join(','); + window.location.hash = newHash; +}; +export function Udels() { + const hashes = getHashesFromUrl(); + + const [numWindows, setNumWindows] = useState(hashes?.length ?? 1); + const numWindowsOnChange = (num) => { + setNumWindows(num); + const hashes = getHashesFromUrl(); + const newHashes = []; + for (let i = 0; i < num; i++) { + newHashes[i] = hashes[i] ?? defaultHash; + } + updateURLHashes(newHashes); + }; + + const onEvaluate = (key, code) => { + const hashes = getHashesFromUrl(); + hashes[key] = code2hash(code); + + updateURLHashes(hashes); + }; + + return ( +
+ +
+ {hashes.map((hash, key) => { + return ( + { + onEvaluate(key, code); + }} + hash={hash} + key={key} + /> + ); + })} +
+
+ ); +} diff --git a/website/src/components/Udels/UdelsEditor.jsx b/website/src/components/Udels/UdelsEditor.jsx new file mode 100644 index 00000000..d0d4a956 --- /dev/null +++ b/website/src/components/Udels/UdelsEditor.jsx @@ -0,0 +1,34 @@ +import { ReplContext } from '@src/repl/util.mjs'; + +import Loader from '@src/repl/components/Loader'; +import { Panel } from '@src/repl/components/panel/Panel'; +import { Code } from '@src/repl/components/Code'; +import BigPlayButton from '@src/repl/components/BigPlayButton'; +import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage'; + +// type Props = { +// context: replcontext, +// containerRef: React.MutableRefObject, +// editorRef: React.MutableRefObject, +// error: Error +// init: () => void +// } + +export default function UdelsEditor(Props) { + const { context, containerRef, editorRef, error, init } = Props; + const { pending, started, handleTogglePlay } = context; + return ( + +
+ + {/*
*/} + +
+ +
+ + +
+
+ ); +} diff --git a/website/src/components/Udels/UdelsHeader.jsx b/website/src/components/Udels/UdelsHeader.jsx new file mode 100644 index 00000000..d56f3a1d --- /dev/null +++ b/website/src/components/Udels/UdelsHeader.jsx @@ -0,0 +1,20 @@ +import NumberInput from '@src/repl/components/NumberInput'; + +export default function UdelsHeader(Props) { + const { numWindows, setNumWindows } = Props; + + return ( + + ); +} diff --git a/website/src/layouts/MainLayout.astro b/website/src/layouts/MainLayout.astro index 49952a7e..c6f6653c 100644 --- a/website/src/layouts/MainLayout.astro +++ b/website/src/layouts/MainLayout.astro @@ -30,7 +30,7 @@ const githubEditUrl = `${CONFIG.GITHUB_EDIT_URL}/${currentFile}`; - +
diff --git a/website/src/pages/index.astro b/website/src/pages/index.astro index 760cf448..29cf2fad 100644 --- a/website/src/pages/index.astro +++ b/website/src/pages/index.astro @@ -3,12 +3,12 @@ import HeadCommon from '../components/HeadCommon.astro'; import { Repl } from '../repl/Repl'; --- - + Strudel REPL - + diff --git a/website/src/pages/udels/index.astro b/website/src/pages/udels/index.astro new file mode 100644 index 00000000..4ab699e7 --- /dev/null +++ b/website/src/pages/udels/index.astro @@ -0,0 +1,12 @@ +--- +import { Udels } from '../../components/Udels/Udels.jsx'; + + +const { BASE_URL } = import.meta.env; +const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; +--- + + + + + \ No newline at end of file diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index e20551c4..3b536c2f 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -6,7 +6,6 @@ This program is free software: you can redistribute it and/or modify it under th import { code2hash, logger, silence } from '@strudel/core'; import { getDrawContext } from '@strudel/draw'; -import cx from '@src/cx.mjs'; import { transpiler } from '@strudel/transpiler'; import { getAudioContext, @@ -29,16 +28,15 @@ import { getViewingPatternData, setViewingPatternData, } from '../user_pattern_utils.mjs'; -import { Header } from './Header'; -import Loader from './Loader'; -import { Panel } from './panel/Panel'; import { useStore } from '@nanostores/react'; import { prebake } from './prebake.mjs'; -import { getRandomTune, initCode, loadModules, shareCode, ReplContext } from './util.mjs'; -import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon'; +import { getRandomTune, initCode, loadModules, shareCode, ReplContext, isUdels } from './util.mjs'; import './Repl.css'; import { setInterval, clearInterval } from 'worker-timers'; import { getMetadata } from '../metadata_parser'; +import UdelsEditor from '@components/Udels/UdelsEditor'; + +import ReplEditor from './components/ReplEditor'; const { latestCode } = settingsMap.get(); @@ -92,6 +90,9 @@ export function Repl({ embedded = false }) { beforeEval: () => audioReady, afterEval: (all) => { const { code } = all; + //post to iframe parent (like Udels) if it exists... + window.parent?.postMessage(code); + setLatestCode(code); window.location.hash = '#' + code2hash(code); setDocumentTitle(code); @@ -234,38 +235,21 @@ export function Repl({ embedded = false }) { handleEvaluate, }; + if (isUdels()) { + return ( + + ); + } + return ( - -
- -
- {isEmbedded && !started && ( - - )} -
-
{ - containerRef.current = el; - if (!editorRef.current) { - init(); - } - }} - >
- {panelPosition === 'right' && !isEmbedded && } -
- {error && ( -
{error.message || 'Unknown Error :-/'}
- )} - {panelPosition === 'bottom' && !isEmbedded && } -
-
+ ); } diff --git a/website/src/repl/components/BigPlayButton.jsx b/website/src/repl/components/BigPlayButton.jsx new file mode 100644 index 00000000..80a4d345 --- /dev/null +++ b/website/src/repl/components/BigPlayButton.jsx @@ -0,0 +1,22 @@ +import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon'; + +// type Props = { +// started: boolean; +// handleTogglePlay: () => void; +// }; +export default function BigPlayButton(Props) { + const { started, handleTogglePlay } = Props; + if (started) { + return; + } + + return ( + + ); +} diff --git a/website/src/repl/components/Code.jsx b/website/src/repl/components/Code.jsx new file mode 100644 index 00000000..8481cc27 --- /dev/null +++ b/website/src/repl/components/Code.jsx @@ -0,0 +1,21 @@ +// type Props = { +// containerRef: React.MutableRefObject, +// editorRef: React.MutableRefObject, +// init: () => void +// } +export function Code(Props) { + const { editorRef, containerRef, init } = Props; + + return ( +
{ + containerRef.current = el; + if (!editorRef.current) { + init(); + } + }} + >
+ ); +} diff --git a/website/src/repl/Header.jsx b/website/src/repl/components/Header.jsx similarity index 98% rename from website/src/repl/Header.jsx rename to website/src/repl/components/Header.jsx index 748b84e4..288822dd 100644 --- a/website/src/repl/Header.jsx +++ b/website/src/repl/components/Header.jsx @@ -5,9 +5,9 @@ import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon'; import SparklesIcon from '@heroicons/react/20/solid/SparklesIcon'; import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon'; import cx from '@src/cx.mjs'; -import { useSettings, setIsZen } from '../settings.mjs'; +import { useSettings, setIsZen } from '../../settings.mjs'; // import { ReplContext } from './Repl'; -import './Repl.css'; +import '../Repl.css'; const { BASE_URL } = import.meta.env; const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; diff --git a/website/src/repl/Loader.jsx b/website/src/repl/components/Loader.jsx similarity index 100% rename from website/src/repl/Loader.jsx rename to website/src/repl/components/Loader.jsx diff --git a/website/src/repl/components/NumberInput.jsx b/website/src/repl/components/NumberInput.jsx new file mode 100644 index 00000000..0c3e7e53 --- /dev/null +++ b/website/src/repl/components/NumberInput.jsx @@ -0,0 +1,54 @@ +function Button(Props) { + const { children, onClick } = Props; + + return ( + + ); +} +export default function NumberInput(Props) { + const { value = 0, setValue, max, min } = Props; + + return ( +
+ + setValue(e.target.value)} + /> + +
+ ); +} diff --git a/website/src/repl/components/ReplEditor.jsx b/website/src/repl/components/ReplEditor.jsx new file mode 100644 index 00000000..5cbfbb2e --- /dev/null +++ b/website/src/repl/components/ReplEditor.jsx @@ -0,0 +1,38 @@ +import { ReplContext } from '@src/repl/util.mjs'; + +import Loader from '@src/repl/components/Loader'; +import { Panel } from '@src/repl/components/panel/Panel'; +import { Code } from '@src/repl/components/Code'; +import BigPlayButton from '@src/repl/components/BigPlayButton'; +import UserFacingErrorMessage from '@src/repl/components/UserFacingErrorMessage'; +import { Header } from './Header'; + +// type Props = { +// context: replcontext, +// containerRef: React.MutableRefObject, +// editorRef: React.MutableRefObject, +// error: Error +// init: () => void +// isEmbedded: boolean +// } + +export default function ReplEditor(Props) { + const { context, containerRef, editorRef, error, init, panelPosition } = Props; + const { pending, started, handleTogglePlay, isEmbedded } = context; + const showPanel = !isEmbedded; + return ( + +
+ +
+ {isEmbedded && } +
+ + {panelPosition === 'right' && showPanel && } +
+ + {panelPosition === 'bottom' && showPanel && } +
+
+ ); +} diff --git a/website/src/repl/components/UserFacingErrorMessage.jsx b/website/src/repl/components/UserFacingErrorMessage.jsx new file mode 100644 index 00000000..ec8d8be3 --- /dev/null +++ b/website/src/repl/components/UserFacingErrorMessage.jsx @@ -0,0 +1,8 @@ +// type Props = { error: Error | null }; +export default function UserFacingErrorMessage(Props) { + const { error } = Props; + if (error == null) { + return; + } + return
{error.message || 'Unknown Error :-/'}
; +} diff --git a/website/src/repl/panel/AudioDeviceSelector.jsx b/website/src/repl/components/panel/AudioDeviceSelector.jsx similarity index 94% rename from website/src/repl/panel/AudioDeviceSelector.jsx rename to website/src/repl/components/panel/AudioDeviceSelector.jsx index 969bf387..d1f13c22 100644 --- a/website/src/repl/panel/AudioDeviceSelector.jsx +++ b/website/src/repl/components/panel/AudioDeviceSelector.jsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { getAudioDevices, setAudioDevice } from '../util.mjs'; +import { getAudioDevices, setAudioDevice } from '../../util.mjs'; import { SelectInput } from './SelectInput'; const initdevices = new Map(); diff --git a/website/src/repl/panel/ConsoleTab.jsx b/website/src/repl/components/panel/ConsoleTab.jsx similarity index 100% rename from website/src/repl/panel/ConsoleTab.jsx rename to website/src/repl/components/panel/ConsoleTab.jsx diff --git a/website/src/repl/panel/FilesTab.jsx b/website/src/repl/components/panel/FilesTab.jsx similarity index 97% rename from website/src/repl/panel/FilesTab.jsx rename to website/src/repl/components/panel/FilesTab.jsx index d78ec1ec..2e121fb5 100644 --- a/website/src/repl/panel/FilesTab.jsx +++ b/website/src/repl/components/panel/FilesTab.jsx @@ -1,6 +1,6 @@ import { Fragment, useEffect } from 'react'; import React, { useMemo, useState } from 'react'; -import { isAudioFile, readDir, dir, playFile } from '../files.mjs'; +import { isAudioFile, readDir, dir, playFile } from '../../files.mjs'; export function FilesTab() { const [path, setPath] = useState([]); diff --git a/website/src/repl/panel/Forms.jsx b/website/src/repl/components/panel/Forms.jsx similarity index 100% rename from website/src/repl/panel/Forms.jsx rename to website/src/repl/components/panel/Forms.jsx diff --git a/website/src/repl/panel/ImportSoundsButton.jsx b/website/src/repl/components/panel/ImportSoundsButton.jsx similarity index 97% rename from website/src/repl/panel/ImportSoundsButton.jsx rename to website/src/repl/components/panel/ImportSoundsButton.jsx index da45705f..7862b766 100644 --- a/website/src/repl/panel/ImportSoundsButton.jsx +++ b/website/src/repl/components/panel/ImportSoundsButton.jsx @@ -1,5 +1,5 @@ import React, { useCallback, useState } from 'react'; -import { registerSamplesFromDB, uploadSamplesToDB, userSamplesDBConfig } from '../idbutils.mjs'; +import { registerSamplesFromDB, uploadSamplesToDB, userSamplesDBConfig } from '../../idbutils.mjs'; //choose a directory to locally import samples export default function ImportSoundsButton({ onComplete }) { diff --git a/website/src/repl/panel/Panel.jsx b/website/src/repl/components/panel/Panel.jsx similarity index 98% rename from website/src/repl/panel/Panel.jsx rename to website/src/repl/components/panel/Panel.jsx index 7d35f0e4..a7bb6a83 100644 --- a/website/src/repl/panel/Panel.jsx +++ b/website/src/repl/components/panel/Panel.jsx @@ -4,7 +4,7 @@ import useEvent from '@src/useEvent.mjs'; import cx from '@src/cx.mjs'; import { nanoid } from 'nanoid'; import { useCallback, useLayoutEffect, useEffect, useRef, useState } from 'react'; -import { setActiveFooter, useSettings } from '../../settings.mjs'; +import { setActiveFooter, useSettings } from '../../../settings.mjs'; import { ConsoleTab } from './ConsoleTab'; import { FilesTab } from './FilesTab'; import { Reference } from './Reference'; diff --git a/website/src/repl/panel/PatternsTab.jsx b/website/src/repl/components/panel/PatternsTab.jsx similarity index 94% rename from website/src/repl/panel/PatternsTab.jsx rename to website/src/repl/components/panel/PatternsTab.jsx index f6499d79..5e9119b1 100644 --- a/website/src/repl/panel/PatternsTab.jsx +++ b/website/src/repl/components/panel/PatternsTab.jsx @@ -5,13 +5,13 @@ import { useActivePattern, useViewingPatternData, userPattern, -} from '../../user_pattern_utils.mjs'; +} from '../../../user_pattern_utils.mjs'; import { useMemo } from 'react'; -import { getMetadata } from '../../metadata_parser'; -import { useExamplePatterns } from '../useExamplePatterns'; -import { parseJSON } from '../util.mjs'; +import { getMetadata } from '../../../metadata_parser.js'; +import { useExamplePatterns } from '../../useExamplePatterns.jsx'; +import { parseJSON, isUdels } from '../../util.mjs'; import { ButtonGroup } from './Forms.jsx'; -import { settingsMap, useSettings } from '../../settings.mjs'; +import { settingsMap, useSettings } from '../../../settings.mjs'; function classNames(...classes) { return classes.filter(Boolean).join(' '); @@ -99,7 +99,7 @@ export function PatternsTab({ context }) { }; const viewingPatternID = viewingPatternData?.id; - const autoResetPatternOnChange = !window.parent?.location.pathname.includes('oodles'); + const autoResetPatternOnChange = !isUdels(); return (
diff --git a/website/src/repl/panel/Reference.jsx b/website/src/repl/components/panel/Reference.jsx similarity index 98% rename from website/src/repl/panel/Reference.jsx rename to website/src/repl/components/panel/Reference.jsx index 9483e9c5..04297c0b 100644 --- a/website/src/repl/panel/Reference.jsx +++ b/website/src/repl/components/panel/Reference.jsx @@ -1,4 +1,4 @@ -import jsdocJson from '../../../../doc.json'; +import jsdocJson from '../../../../../doc.json'; const visibleFunctions = jsdocJson.docs .filter(({ name, description }) => name && !name.startsWith('_') && !!description) .sort((a, b) => /* a.meta.filename.localeCompare(b.meta.filename) + */ a.name.localeCompare(b.name)); diff --git a/website/src/repl/panel/SelectInput.jsx b/website/src/repl/components/panel/SelectInput.jsx similarity index 100% rename from website/src/repl/panel/SelectInput.jsx rename to website/src/repl/components/panel/SelectInput.jsx diff --git a/website/src/repl/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx similarity index 95% rename from website/src/repl/panel/SettingsTab.jsx rename to website/src/repl/components/panel/SettingsTab.jsx index ae290189..e1d047ea 100644 --- a/website/src/repl/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -1,12 +1,13 @@ -import { defaultSettings, settingsMap, useSettings } from '../../settings.mjs'; +import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs'; import { themes } from '@strudel/codemirror'; +import { isUdels } from '../../util.mjs'; import { ButtonGroup } from './Forms.jsx'; import { AudioDeviceSelector } from './AudioDeviceSelector.jsx'; -function Checkbox({ label, value, onChange }) { +function Checkbox({ label, value, onChange, disabled = false }) { return ( ); @@ -96,7 +97,7 @@ export function SettingsTab({ started }) { panelPosition, audioDeviceName, } = useSettings(); - + const shouldAlwaysSync = isUdels(); return (
{AudioContext.prototype.setSinkId != null && ( @@ -197,6 +198,7 @@ export function SettingsTab({ started }) { window.location.reload(); } }} + disabled={shouldAlwaysSync} value={isSyncEnabled} /> diff --git a/website/src/repl/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx similarity index 98% rename from website/src/repl/panel/SoundsTab.jsx rename to website/src/repl/components/panel/SoundsTab.jsx index 9b35d04b..8b7b36a8 100644 --- a/website/src/repl/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -2,7 +2,7 @@ import useEvent from '@src/useEvent.mjs'; import { useStore } from '@nanostores/react'; import { getAudioContext, soundMap, connectToDestination } from '@strudel/webaudio'; import React, { useMemo, useRef } from 'react'; -import { settingsMap, useSettings } from '../../settings.mjs'; +import { settingsMap, useSettings } from '../../../settings.mjs'; import { ButtonGroup } from './Forms.jsx'; import ImportSoundsButton from './ImportSoundsButton.jsx'; diff --git a/website/src/repl/panel/WelcomeTab.jsx b/website/src/repl/components/panel/WelcomeTab.jsx similarity index 100% rename from website/src/repl/panel/WelcomeTab.jsx rename to website/src/repl/components/panel/WelcomeTab.jsx diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index 26ac45d3..c43c29e6 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -134,6 +134,10 @@ export async function shareCode(codeToShare) { export const ReplContext = createContext(null); +export const isUdels = () => { + return window.parent?.location.pathname.includes('udels'); +}; + export const getAudioDevices = async () => { await navigator.mediaDevices.getUserMedia({ audio: true }); let mediaDevices = await navigator.mediaDevices.enumerateDevices(); diff --git a/website/src/settings.mjs b/website/src/settings.mjs index a70c4202..f9b9e281 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -1,6 +1,7 @@ import { persistentMap } from '@nanostores/persistent'; import { useStore } from '@nanostores/react'; import { register } from '@strudel/core'; +import { isUdels } from './repl/util.mjs'; export const defaultAudioDeviceName = 'System Standard'; @@ -29,7 +30,15 @@ export const defaultSettings = { audioDeviceName: defaultAudioDeviceName, }; -export const settingsMap = persistentMap('strudel-settings', defaultSettings); +let search = null; +if (typeof window !== 'undefined') { + search = new URLSearchParams(window.location.search); +} +// if running multiple instance in one window, it will use the settings for that instance. else default to normal +const instance = parseInt(search?.get('instance') ?? '0'); +const settings_key = `strudel-settings${instance > 0 ? instance : ''}`; + +export const settingsMap = persistentMap(settings_key, defaultSettings); const parseBoolean = (booleanlike) => ([true, 'true'].includes(booleanlike) ? true : false); @@ -54,9 +63,9 @@ export function useSettings() { isTooltipEnabled: parseBoolean(state.isTooltipEnabled), isLineWrappingEnabled: parseBoolean(state.isLineWrappingEnabled), isFlashEnabled: parseBoolean(state.isFlashEnabled), - isSyncEnabled: parseBoolean(state.isSyncEnabled), + isSyncEnabled: isUdels() ? true : parseBoolean(state.isSyncEnabled), fontSize: Number(state.fontSize), - panelPosition: state.activeFooter !== '' ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is! + panelPosition: state.activeFooter !== '' && !isUdels() ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is! userPatterns: userPatterns, }; }