diff --git a/website/src/components/Udels/UdelsEditor.jsx b/website/src/components/Udels/UdelsEditor.jsx index d0d4a9569..2d05640f3 100644 --- a/website/src/components/Udels/UdelsEditor.jsx +++ b/website/src/components/Udels/UdelsEditor.jsx @@ -1,5 +1,3 @@ -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'; @@ -8,27 +6,21 @@ 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; + const { context } = Props; + const { containerRef, editorRef, error, init, pending, started, handleTogglePlay } = context; + return ( - -
- - {/*
*/} - -
- -
- - +
+ + +
+
- + + +
); } diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index 3b536c2f3..9032be280 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -4,252 +4,15 @@ Copyright (C) 2022 Strudel contributors - see . */ -import { code2hash, logger, silence } from '@strudel/core'; -import { getDrawContext } from '@strudel/draw'; -import { transpiler } from '@strudel/transpiler'; -import { - getAudioContext, - webaudioOutput, - resetGlobalEffects, - resetLoadedSounds, - initAudioOnFirstClick, -} from '@strudel/webaudio'; -import { defaultAudioDeviceName } from '../settings.mjs'; -import { getAudioDevices, setAudioDevice, setVersionDefaultsFrom } from './util.mjs'; -import { StrudelMirror, defaultSettings } from '@strudel/codemirror'; -import { clearHydra } from '@strudel/hydra'; -import { useCallback, useEffect, useRef, useState } from 'react'; -import { settingsMap, useSettings } from '../settings.mjs'; -import { - setActivePattern, - setLatestCode, - createPatternID, - userPattern, - getViewingPatternData, - setViewingPatternData, -} from '../user_pattern_utils.mjs'; -import { useStore } from '@nanostores/react'; -import { prebake } from './prebake.mjs'; -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 { isIframe, isUdels } from './util.mjs'; import UdelsEditor from '@components/Udels/UdelsEditor'; - import ReplEditor from './components/ReplEditor'; - -const { latestCode } = settingsMap.get(); - -let modulesLoading, presets, drawContext, clearCanvas, isIframe, audioReady; - -if (typeof window !== 'undefined') { - audioReady = initAudioOnFirstClick(); - modulesLoading = loadModules(); - presets = prebake(); - drawContext = getDrawContext(); - clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width); - isIframe = window.location !== window.parent.location; -} - -async function getModule(name) { - if (!modulesLoading) { - return; - } - const modules = await modulesLoading; - return modules.find((m) => m.packageName === name); -} +import EmbeddedReplEditor from './components/EmbeddedReplEditor'; +import { useReplContext } from './useReplContext'; export function Repl({ embedded = false }) { - const isEmbedded = embedded || isIframe; - const { panelPosition, isZen, isSyncEnabled } = useSettings(); - const init = useCallback(() => { - const drawTime = [-2, 2]; - const drawContext = getDrawContext(); - const editor = new StrudelMirror({ - sync: isSyncEnabled, - defaultOutput: webaudioOutput, - getTime: () => getAudioContext().currentTime, - setInterval, - clearInterval, - transpiler, - autodraw: false, - root: containerRef.current, - initialCode: '// LOADING', - pattern: silence, - drawTime, - drawContext, - prebake: async () => Promise.all([modulesLoading, presets]), - onUpdateState: (state) => { - setReplState({ ...state }); - }, - onToggle: (playing) => { - if (!playing) { - clearHydra(); - } - }, - 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); - const viewingPatternData = getViewingPatternData(); - setVersionDefaultsFrom(code); - const data = { ...viewingPatternData, code }; - let id = data.id; - const isExamplePattern = viewingPatternData.collection !== userPattern.collection; - - if (isExamplePattern) { - const codeHasChanged = code !== viewingPatternData.code; - if (codeHasChanged) { - // fork example - const newPattern = userPattern.duplicate(data); - id = newPattern.id; - setViewingPatternData(newPattern.data); - } - } else { - id = userPattern.isValidID(id) ? id : createPatternID(); - setViewingPatternData(userPattern.update(id, data).data); - } - setActivePattern(id); - }, - bgFill: false, - }); - window.strudelMirror = editor; - - // init settings - - initCode().then(async (decoded) => { - let code, msg; - if (decoded) { - code = decoded; - msg = `I have loaded the code from the URL.`; - } else if (latestCode) { - code = latestCode; - msg = `Your last session has been loaded!`; - } else { - const { code: randomTune, name } = await getRandomTune(); - code = randomTune; - msg = `A random code snippet named "${name}" has been loaded!`; - } - editor.setCode(code); - setDocumentTitle(code); - logger(`Welcome to Strudel! ${msg} Press play or hit ctrl+enter to run it!`, 'highlight'); - }); - - editorRef.current = editor; - }, []); - - const [replState, setReplState] = useState({}); - const { started, isDirty, error, activeCode, pending } = replState; - const editorRef = useRef(); - const containerRef = useRef(); - - // this can be simplified once SettingsTab has been refactored to change codemirrorSettings directly! - // this will be the case when the main repl is being replaced - const _settings = useStore(settingsMap, { keys: Object.keys(defaultSettings) }); - useEffect(() => { - let editorSettings = {}; - Object.keys(defaultSettings).forEach((key) => { - if (_settings.hasOwnProperty(key)) { - editorSettings[key] = _settings[key]; - } - }); - editorRef.current?.updateSettings(editorSettings); - }, [_settings]); - - // on first load, set stored audio device if possible - useEffect(() => { - const { audioDeviceName } = _settings; - if (audioDeviceName !== defaultAudioDeviceName) { - getAudioDevices().then((devices) => { - const deviceID = devices.get(audioDeviceName); - if (deviceID == null) { - return; - } - setAudioDevice(deviceID); - }); - } - }, []); - - // - // UI Actions - // - - const setDocumentTitle = (code) => { - const meta = getMetadata(code); - document.title = (meta.title ? `${meta.title} - ` : '') + 'Strudel REPL'; - }; - - const handleTogglePlay = async () => { - editorRef.current?.toggle(); - }; - - const resetEditor = async () => { - (await getModule('@strudel/tonal'))?.resetVoicings(); - resetGlobalEffects(); - clearCanvas(); - clearHydra(); - resetLoadedSounds(); - editorRef.current.repl.setCps(0.5); - await prebake(); // declare default samples - }; - - const handleUpdate = async (patternData, reset = false) => { - setViewingPatternData(patternData); - editorRef.current.setCode(patternData.code); - if (reset) { - await resetEditor(); - handleEvaluate(); - } - }; - - const handleEvaluate = () => { - editorRef.current.evaluate(); - }; - const handleShuffle = async () => { - const patternData = await getRandomTune(); - const code = patternData.code; - logger(`[repl] ✨ loading random tune "${patternData.id}"`); - setActivePattern(patternData.id); - setViewingPatternData(patternData); - await resetEditor(); - editorRef.current.setCode(code); - editorRef.current.repl.evaluate(code); - }; - - const handleShare = async () => shareCode(replState.code); - const context = { - embedded, - started, - pending, - isDirty, - activeCode, - handleTogglePlay, - handleUpdate, - handleShuffle, - handleShare, - handleEvaluate, - }; - - if (isUdels()) { - return ( - - ); - } - - return ( - - ); + const isEmbedded = embedded || isIframe(); + const Editor = isUdels() ? UdelsEditor : isEmbedded ? EmbeddedReplEditor : ReplEditor; + const context = useReplContext(); + return ; } diff --git a/website/src/repl/components/EmbeddedReplEditor.jsx b/website/src/repl/components/EmbeddedReplEditor.jsx new file mode 100644 index 000000000..06d9c2c1c --- /dev/null +++ b/website/src/repl/components/EmbeddedReplEditor.jsx @@ -0,0 +1,25 @@ +import Loader from '@src/repl/components/Loader'; +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, +// } + +export default function EmbeddedReplEditor(Props) { + const { context } = Props; + const { pending, started, handleTogglePlay, containerRef, editorRef, error, init } = context; + return ( +
+ +
+ +
+ +
+ +
+ ); +} diff --git a/website/src/repl/components/Header.jsx b/website/src/repl/components/Header.jsx index 288822dd7..c513f451d 100644 --- a/website/src/repl/components/Header.jsx +++ b/website/src/repl/components/Header.jsx @@ -6,23 +6,14 @@ 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 { ReplContext } from './Repl'; import '../Repl.css'; + const { BASE_URL } = import.meta.env; const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; -export function Header({ context }) { - const { - embedded, - started, - pending, - isDirty, - activeCode, - handleTogglePlay, - handleEvaluate, - handleShuffle, - handleShare, - } = context; +export function Header({ context, embedded = false }) { + const { started, pending, isDirty, activeCode, handleTogglePlay, handleEvaluate, handleShuffle, handleShare } = + context; const isEmbedded = typeof window !== 'undefined' && (embedded || window.location !== window.parent.location); const { isZen } = useSettings(); diff --git a/website/src/repl/components/ReplEditor.jsx b/website/src/repl/components/ReplEditor.jsx index 5cbfbb2ea..83317bf37 100644 --- a/website/src/repl/components/ReplEditor.jsx +++ b/website/src/repl/components/ReplEditor.jsx @@ -1,38 +1,30 @@ -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'; +import { useSettings } from '@src/settings.mjs'; // 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; + const { context } = Props; + const { containerRef, editorRef, error, init, pending } = context; + const settings = useSettings(); + const { panelPosition } = settings; + return ( - -
- -
- {isEmbedded && } -
- - {panelPosition === 'right' && showPanel && } -
- - {panelPosition === 'bottom' && showPanel && } +
+ +
+
+ + {panelPosition === 'right' && }
- + + {panelPosition === 'bottom' && } +
); } diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx new file mode 100644 index 000000000..d61699b6d --- /dev/null +++ b/website/src/repl/useReplContext.jsx @@ -0,0 +1,234 @@ +/* +Repl.jsx - +Copyright (C) 2022 Strudel contributors - see +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . +*/ + +import { code2hash, logger, silence } from '@strudel/core'; +import { getDrawContext } from '@strudel/draw'; +import { transpiler } from '@strudel/transpiler'; +import { + getAudioContext, + webaudioOutput, + resetGlobalEffects, + resetLoadedSounds, + initAudioOnFirstClick, +} from '@strudel/webaudio'; +import { defaultAudioDeviceName } from '../settings.mjs'; +import { getAudioDevices, setAudioDevice, setVersionDefaultsFrom } from './util.mjs'; +import { StrudelMirror, defaultSettings } from '@strudel/codemirror'; +import { clearHydra } from '@strudel/hydra'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { settingsMap, useSettings } from '../settings.mjs'; +import { + setActivePattern, + setLatestCode, + createPatternID, + userPattern, + getViewingPatternData, + setViewingPatternData, +} from '../user_pattern_utils.mjs'; +import { useStore } from '@nanostores/react'; +import { prebake } from './prebake.mjs'; +import { getRandomTune, initCode, loadModules, shareCode } from './util.mjs'; +import './Repl.css'; +import { setInterval, clearInterval } from 'worker-timers'; +import { getMetadata } from '../metadata_parser'; + +const { latestCode } = settingsMap.get(); +let modulesLoading, presets, drawContext, clearCanvas, audioReady; + +if (typeof window !== 'undefined') { + audioReady = initAudioOnFirstClick(); + modulesLoading = loadModules(); + presets = prebake(); + drawContext = getDrawContext(); + clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width); +} + +async function getModule(name) { + if (!modulesLoading) { + return; + } + const modules = await modulesLoading; + return modules.find((m) => m.packageName === name); +} + +export function useReplContext() { + const { isSyncEnabled } = useSettings(); + const init = useCallback(() => { + const drawTime = [-2, 2]; + const drawContext = getDrawContext(); + const editor = new StrudelMirror({ + sync: isSyncEnabled, + defaultOutput: webaudioOutput, + getTime: () => getAudioContext().currentTime, + setInterval, + clearInterval, + transpiler, + autodraw: false, + root: containerRef.current, + initialCode: '// LOADING', + pattern: silence, + drawTime, + drawContext, + prebake: async () => Promise.all([modulesLoading, presets]), + onUpdateState: (state) => { + setReplState({ ...state }); + }, + onToggle: (playing) => { + if (!playing) { + clearHydra(); + } + }, + 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); + const viewingPatternData = getViewingPatternData(); + setVersionDefaultsFrom(code); + const data = { ...viewingPatternData, code }; + let id = data.id; + const isExamplePattern = viewingPatternData.collection !== userPattern.collection; + + if (isExamplePattern) { + const codeHasChanged = code !== viewingPatternData.code; + if (codeHasChanged) { + // fork example + const newPattern = userPattern.duplicate(data); + id = newPattern.id; + setViewingPatternData(newPattern.data); + } + } else { + id = userPattern.isValidID(id) ? id : createPatternID(); + setViewingPatternData(userPattern.update(id, data).data); + } + setActivePattern(id); + }, + bgFill: false, + }); + window.strudelMirror = editor; + + // init settings + initCode().then(async (decoded) => { + let code, msg; + if (decoded) { + code = decoded; + msg = `I have loaded the code from the URL.`; + } else if (latestCode) { + code = latestCode; + msg = `Your last session has been loaded!`; + } else { + const { code: randomTune, name } = await getRandomTune(); + code = randomTune; + msg = `A random code snippet named "${name}" has been loaded!`; + } + editor.setCode(code); + setDocumentTitle(code); + logger(`Welcome to Strudel! ${msg} Press play or hit ctrl+enter to run it!`, 'highlight'); + }); + + editorRef.current = editor; + }, []); + + const [replState, setReplState] = useState({}); + const { started, isDirty, error, activeCode, pending } = replState; + const editorRef = useRef(); + const containerRef = useRef(); + + // this can be simplified once SettingsTab has been refactored to change codemirrorSettings directly! + // this will be the case when the main repl is being replaced + const _settings = useStore(settingsMap, { keys: Object.keys(defaultSettings) }); + useEffect(() => { + let editorSettings = {}; + Object.keys(defaultSettings).forEach((key) => { + if (Object.prototype.hasOwnProperty.call(_settings, key)) { + editorSettings[key] = _settings[key]; + } + }); + editorRef.current?.updateSettings(editorSettings); + }, [_settings]); + + // on first load, set stored audio device if possible + useEffect(() => { + const { audioDeviceName } = _settings; + if (audioDeviceName !== defaultAudioDeviceName) { + getAudioDevices().then((devices) => { + const deviceID = devices.get(audioDeviceName); + if (deviceID == null) { + return; + } + setAudioDevice(deviceID); + }); + } + }, []); + + // + // UI Actions + // + + const setDocumentTitle = (code) => { + const meta = getMetadata(code); + document.title = (meta.title ? `${meta.title} - ` : '') + 'Strudel REPL'; + }; + + const handleTogglePlay = async () => { + editorRef.current?.toggle(); + }; + + const resetEditor = async () => { + (await getModule('@strudel/tonal'))?.resetVoicings(); + resetGlobalEffects(); + clearCanvas(); + clearHydra(); + resetLoadedSounds(); + editorRef.current.repl.setCps(0.5); + await prebake(); // declare default samples + }; + + const handleUpdate = async (patternData, reset = false) => { + setViewingPatternData(patternData); + editorRef.current.setCode(patternData.code); + if (reset) { + await resetEditor(); + handleEvaluate(); + } + }; + + const handleEvaluate = () => { + editorRef.current.evaluate(); + }; + const handleShuffle = async () => { + const patternData = await getRandomTune(); + const code = patternData.code; + logger(`[repl] ✨ loading random tune "${patternData.id}"`); + setActivePattern(patternData.id); + setViewingPatternData(patternData); + await resetEditor(); + editorRef.current.setCode(code); + editorRef.current.repl.evaluate(code); + }; + + const handleShare = async () => shareCode(replState.code); + const context = { + started, + pending, + isDirty, + activeCode, + handleTogglePlay, + handleUpdate, + handleShuffle, + handleShare, + handleEvaluate, + init, + error, + editorRef, + containerRef, + }; + return context; +} diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index 8d8cfba49..4aa61fb03 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -132,14 +132,20 @@ export async function shareCode(codeToShare) { } } -export const ReplContext = createContext(null); +export const isIframe = () => window.location !== window.parent.location; +function isCrossOriginFrame() { + try { + return !window.top.location.hostname; + } catch (e) { + return true; + } +} export const isUdels = () => { - const isIframe = window.location !== window.parent.location; - if (isIframe) { + if (isCrossOriginFrame()) { return false; } - return window.parent?.location?.pathname.includes('udels'); + return window.top?.location?.pathname.includes('udels'); }; export const getAudioDevices = async () => {