mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 21:58:31 +00:00
Merge pull request #1132 from daslyfe/udels
Udels (MultiFrame Strudel) Revisited
This commit is contained in:
commit
61ba33043e
@ -4,8 +4,6 @@ Copyright (C) 2022 Strudel contributors - see <https://github.com/tidalcycles/st
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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);
|
||||
|
||||
@ -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 (
|
||||
|
||||
32
website/src/components/Udels/UdelFrame.jsx
Normal file
32
website/src/components/Udels/UdelFrame.jsx
Normal file
@ -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 (
|
||||
<iframe
|
||||
ref={ref}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
minWidth: '50%',
|
||||
boxSizing: 'border-box',
|
||||
border: '0px',
|
||||
}}
|
||||
title="strudel embed"
|
||||
src={source}
|
||||
></iframe>
|
||||
);
|
||||
}
|
||||
72
website/src/components/Udels/Udels.jsx
Normal file
72
website/src/components/Udels/Udels.jsx
Normal file
@ -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 (
|
||||
<div
|
||||
style={{
|
||||
margin: 0,
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
height: '100vh',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<UdelsHeader numWindows={numWindows} setNumWindows={numWindowsOnChange} />
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{hashes.map((hash, key) => {
|
||||
return (
|
||||
<UdelFrame
|
||||
instance={key}
|
||||
onEvaluate={(code) => {
|
||||
onEvaluate(key, code);
|
||||
}}
|
||||
hash={hash}
|
||||
key={key}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
34
website/src/components/Udels/UdelsEditor.jsx
Normal file
34
website/src/components/Udels/UdelsEditor.jsx
Normal file
@ -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<HTMLElement | null>,
|
||||
// editorRef: React.MutableRefObject<HTMLElement | null>,
|
||||
// error: Error
|
||||
// init: () => void
|
||||
// }
|
||||
|
||||
export default function UdelsEditor(Props) {
|
||||
const { context, containerRef, editorRef, error, init } = Props;
|
||||
const { pending, started, handleTogglePlay } = context;
|
||||
return (
|
||||
<ReplContext.Provider value={context}>
|
||||
<div className={'h-full flex w-full flex-col relative'}>
|
||||
<Loader active={pending} />
|
||||
{/* <Header context={context} /> */}
|
||||
<BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />
|
||||
<div className="grow flex relative overflow-hidden">
|
||||
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
|
||||
</div>
|
||||
<UserFacingErrorMessage error={error} />
|
||||
<Panel context={context} />
|
||||
</div>
|
||||
</ReplContext.Provider>
|
||||
);
|
||||
}
|
||||
20
website/src/components/Udels/UdelsHeader.jsx
Normal file
20
website/src/components/Udels/UdelsHeader.jsx
Normal file
@ -0,0 +1,20 @@
|
||||
import NumberInput from '@src/repl/components/NumberInput';
|
||||
|
||||
export default function UdelsHeader(Props) {
|
||||
const { numWindows, setNumWindows } = Props;
|
||||
|
||||
return (
|
||||
<header id="header" className="flex text-white z-[100] text-lg select-none bg-neutral-900">
|
||||
<div className="px-4 items-center gap-2 flex space-x-2 md:pt-0 select-none">
|
||||
<h1 onClick={() => {}} className={'text-l cursor-pointer flex gap-4'}>
|
||||
<div className={'mt-[1px] cursor-pointer'}>🌀</div>
|
||||
|
||||
<div className={'animate-pulse'}>
|
||||
<span className="">strudel</span> <span className="text-sm">-UDELS</span>
|
||||
</div>
|
||||
</h1>
|
||||
<NumberInput value={numWindows} setValue={setNumWindows} />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@ -30,7 +30,7 @@ const githubEditUrl = `${CONFIG.GITHUB_EDIT_URL}/${currentFile}`;
|
||||
</title>
|
||||
</head>
|
||||
|
||||
<body class="h-app-height text-gray-50 bg-background">
|
||||
<body class="h-app-height m-0 text-gray-50 bg-background">
|
||||
<div class="w-full h-full space-y-4 flex flex-col">
|
||||
<header class="max-w-full fixed top-0 w-full z-[100]">
|
||||
<Header currentPage={currentPage} />
|
||||
|
||||
@ -3,12 +3,12 @@ import HeadCommon from '../components/HeadCommon.astro';
|
||||
import { Repl } from '../repl/Repl';
|
||||
---
|
||||
|
||||
<html lang="en" class="dark">
|
||||
<html lang="en" class="m-0 dark">
|
||||
<head>
|
||||
<HeadCommon />
|
||||
<title>Strudel REPL</title>
|
||||
</head>
|
||||
<body class="h-app-height bg-background">
|
||||
<body class="h-app-height bg-background m-0">
|
||||
<Repl client:only="react" />
|
||||
<a rel="me" href="https://social.toplap.org/@strudel" target="_blank" class="hidden">mastodon</a>
|
||||
</body>
|
||||
|
||||
12
website/src/pages/udels/index.astro
Normal file
12
website/src/pages/udels/index.astro
Normal file
@ -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;
|
||||
---
|
||||
|
||||
|
||||
<body class="m-0">
|
||||
<Udels client:only="react" />
|
||||
</body>
|
||||
@ -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 (
|
||||
<UdelsEditor context={context} error={error} init={init} editorRef={editorRef} containerRef={containerRef} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ReplContext.Provider value={context}>
|
||||
<div className={cx('h-full flex flex-col relative')}>
|
||||
<Loader active={pending} />
|
||||
<Header context={context} />
|
||||
{isEmbedded && !started && (
|
||||
<button
|
||||
onClick={() => handleTogglePlay()}
|
||||
className="text-white text-2xl fixed left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] z-[1000] m-auto p-4 bg-black rounded-md flex items-center space-x-2"
|
||||
>
|
||||
<PlayCircleIcon className="w-6 h-6" />
|
||||
<span>play</span>
|
||||
</button>
|
||||
)}
|
||||
<div className="grow flex relative overflow-hidden">
|
||||
<section
|
||||
className={'text-gray-100 cursor-text pb-0 overflow-auto grow' + (isZen ? ' px-10' : '')}
|
||||
id="code"
|
||||
ref={(el) => {
|
||||
containerRef.current = el;
|
||||
if (!editorRef.current) {
|
||||
init();
|
||||
}
|
||||
}}
|
||||
></section>
|
||||
{panelPosition === 'right' && !isEmbedded && <Panel context={context} />}
|
||||
</div>
|
||||
{error && (
|
||||
<div className="text-red-500 p-4 bg-lineHighlight animate-pulse">{error.message || 'Unknown Error :-/'}</div>
|
||||
)}
|
||||
{panelPosition === 'bottom' && !isEmbedded && <Panel context={context} />}
|
||||
</div>
|
||||
</ReplContext.Provider>
|
||||
<ReplEditor
|
||||
panelPosition={panelPosition}
|
||||
isEmbedded={isEmbedded}
|
||||
context={context}
|
||||
error={error}
|
||||
init={init}
|
||||
editorRef={editorRef}
|
||||
containerRef={containerRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
22
website/src/repl/components/BigPlayButton.jsx
Normal file
22
website/src/repl/components/BigPlayButton.jsx
Normal file
@ -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 (
|
||||
<button
|
||||
onClick={() => handleTogglePlay()}
|
||||
className="text-white text-2xl fixed left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] z-[1000] m-auto p-4 bg-black rounded-md flex items-center space-x-2"
|
||||
>
|
||||
<PlayCircleIcon className="w-6 h-6" />
|
||||
<span>play</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
21
website/src/repl/components/Code.jsx
Normal file
21
website/src/repl/components/Code.jsx
Normal file
@ -0,0 +1,21 @@
|
||||
// type Props = {
|
||||
// containerRef: React.MutableRefObject<HTMLElement | null>,
|
||||
// editorRef: React.MutableRefObject<HTMLElement | null>,
|
||||
// init: () => void
|
||||
// }
|
||||
export function Code(Props) {
|
||||
const { editorRef, containerRef, init } = Props;
|
||||
|
||||
return (
|
||||
<section
|
||||
className={'text-gray-100 cursor-text pb-0 overflow-auto grow'}
|
||||
id="code"
|
||||
ref={(el) => {
|
||||
containerRef.current = el;
|
||||
if (!editorRef.current) {
|
||||
init();
|
||||
}
|
||||
}}
|
||||
></section>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
|
||||
54
website/src/repl/components/NumberInput.jsx
Normal file
54
website/src/repl/components/NumberInput.jsx
Normal file
@ -0,0 +1,54 @@
|
||||
function Button(Props) {
|
||||
const { children, onClick } = Props;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
data-input-counter-increment="counter-input"
|
||||
className="flex-shrink-0 bg-gray-700 hover:bg-gray-600 border-gray-600 inline-flex items-center justify-center border rounded-md h-5 w-5 focus:ring-gray-700 focus:ring-2 focus:outline-none"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
export default function NumberInput(Props) {
|
||||
const { value = 0, setValue, max, min } = Props;
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center">
|
||||
<Button onClick={() => setValue(value - 1)}>
|
||||
<svg
|
||||
className="w-2.5 h-2.5 text-white"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 2"
|
||||
>
|
||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M1 1h16" />
|
||||
</svg>
|
||||
</Button>
|
||||
<input
|
||||
min={min}
|
||||
max={max}
|
||||
type="text"
|
||||
data-input-counter
|
||||
className="flex-shrink-0 text-white border-0 bg-transparent text-sm font-normal focus:outline-none focus:ring-0 max-w-[2.5rem] text-center"
|
||||
placeholder=""
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
<Button onClick={() => setValue(value + 1)}>
|
||||
<svg
|
||||
className="w-2.5 h-2.5 text-white"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<path strokeLinecap="round" stroke="currentColor" strokeLinejoin="round" strokeWidth="2" d="M9 1v16M1 9h16" />
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
website/src/repl/components/ReplEditor.jsx
Normal file
38
website/src/repl/components/ReplEditor.jsx
Normal file
@ -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<HTMLElement | null>,
|
||||
// editorRef: React.MutableRefObject<HTMLElement | null>,
|
||||
// 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 (
|
||||
<ReplContext.Provider value={context}>
|
||||
<div className="h-full flex flex-col relative">
|
||||
<Loader active={pending} />
|
||||
<Header context={context} />
|
||||
{isEmbedded && <BigPlayButton started={started} handleTogglePlay={handleTogglePlay} />}
|
||||
<div className="grow flex relative overflow-hidden">
|
||||
<Code containerRef={containerRef} editorRef={editorRef} init={init} />
|
||||
{panelPosition === 'right' && showPanel && <Panel context={context} />}
|
||||
</div>
|
||||
<UserFacingErrorMessage error={error} />
|
||||
{panelPosition === 'bottom' && showPanel && <Panel context={context} />}
|
||||
</div>
|
||||
</ReplContext.Provider>
|
||||
);
|
||||
}
|
||||
8
website/src/repl/components/UserFacingErrorMessage.jsx
Normal file
8
website/src/repl/components/UserFacingErrorMessage.jsx
Normal file
@ -0,0 +1,8 @@
|
||||
// type Props = { error: Error | null };
|
||||
export default function UserFacingErrorMessage(Props) {
|
||||
const { error } = Props;
|
||||
if (error == null) {
|
||||
return;
|
||||
}
|
||||
return <div className="text-red-500 p-4 bg-lineHighlight animate-pulse">{error.message || 'Unknown Error :-/'}</div>;
|
||||
}
|
||||
@ -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();
|
||||
@ -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([]);
|
||||
@ -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 }) {
|
||||
@ -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';
|
||||
@ -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 (
|
||||
<div className="px-4 w-full dark:text-white text-stone-900 space-y-2 pb-4 flex flex-col overflow-hidden max-h-full">
|
||||
@ -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));
|
||||
@ -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 (
|
||||
<label>
|
||||
<input type="checkbox" checked={value} onChange={onChange} />
|
||||
<input disabled={disabled} type="checkbox" checked={value} onChange={onChange} />
|
||||
{' ' + label}
|
||||
</label>
|
||||
);
|
||||
@ -96,7 +97,7 @@ export function SettingsTab({ started }) {
|
||||
panelPosition,
|
||||
audioDeviceName,
|
||||
} = useSettings();
|
||||
|
||||
const shouldAlwaysSync = isUdels();
|
||||
return (
|
||||
<div className="text-foreground p-4 space-y-4">
|
||||
{AudioContext.prototype.setSinkId != null && (
|
||||
@ -197,6 +198,7 @@ export function SettingsTab({ started }) {
|
||||
window.location.reload();
|
||||
}
|
||||
}}
|
||||
disabled={shouldAlwaysSync}
|
||||
value={isSyncEnabled}
|
||||
/>
|
||||
</FormItem>
|
||||
@ -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';
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user