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