diff --git a/website/src/components/Udels/UdelFrame.jsx b/website/src/components/Udels/UdelFrame.jsx new file mode 100644 index 00000000..78d28944 --- /dev/null +++ b/website/src/components/Udels/UdelFrame.jsx @@ -0,0 +1,33 @@ +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..b2d81ea9 --- /dev/null +++ b/website/src/components/Udels/Udels.jsx @@ -0,0 +1,115 @@ +import { code2hash } from '@strudel/core'; + +import { UdelFrame } from './UdelFrame'; +import { useState } from 'react'; + +function NumberInput({ value, onChange, label = '', min, max }) { + const [localState, setLocalState] = useState(value); + const [isFocused, setIsFocused] = useState(false); + + return ( + + ); +} +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); + }; + // useEffect(() => { + // prebake(); + // }, []); + + return ( +
+
+ +
+
+ {hashes.map((hash, key) => { + return ( + { + onEvaluate(key, code); + }} + hash={hash} + key={key} + /> + ); + })} +
+ {/* */} +
+ ); +} 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 9c472eb4..0c1ad91d 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -34,7 +34,7 @@ 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 { getRandomTune, initCode, loadModules, shareCode, ReplContext, isUdels } from './util.mjs'; import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon'; import './Repl.css'; import { setInterval, clearInterval } from 'worker-timers'; @@ -221,6 +221,7 @@ export function Repl({ embedded = false }) { handleEvaluate, }; + const showPanel = !isEmbedded || isUdels(); return (
@@ -246,12 +247,12 @@ export function Repl({ embedded = false }) { } }} > - {panelPosition === 'right' && !isEmbedded && } + {panelPosition === 'right' && showPanel && }
{error && (
{error.message || 'Unknown Error :-/'}
)} - {panelPosition === 'bottom' && !isEmbedded && } + {panelPosition === 'bottom' && showPanel && }
); diff --git a/website/src/repl/panel/PatternsTab.jsx b/website/src/repl/panel/PatternsTab.jsx index f6499d79..cacbe897 100644 --- a/website/src/repl/panel/PatternsTab.jsx +++ b/website/src/repl/panel/PatternsTab.jsx @@ -9,7 +9,7 @@ import { import { useMemo } from 'react'; import { getMetadata } from '../../metadata_parser'; import { useExamplePatterns } from '../useExamplePatterns'; -import { parseJSON } from '../util.mjs'; +import { parseJSON, isUdels } from '../util.mjs'; import { ButtonGroup } from './Forms.jsx'; import { settingsMap, useSettings } from '../../settings.mjs'; @@ -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/SettingsTab.jsx b/website/src/repl/panel/SettingsTab.jsx index ae290189..36ec12bd 100644 --- a/website/src/repl/panel/SettingsTab.jsx +++ b/website/src/repl/panel/SettingsTab.jsx @@ -1,12 +1,13 @@ 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/util.mjs b/website/src/repl/util.mjs index 6dba7dab..67584760 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -132,6 +132,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..3f1ca051 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'; @@ -28,8 +29,12 @@ export const defaultSettings = { userPatterns: '{}', audioDeviceName: defaultAudioDeviceName, }; +const 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('strudel-settings', defaultSettings); +export const settingsMap = persistentMap(settings_key, defaultSettings); const parseBoolean = (booleanlike) => ([true, 'true'].includes(booleanlike) ? true : false); @@ -54,7 +59,7 @@ 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! userPatterns: userPatterns,