Merge remote-tracking branch 'origin/main' into bettern-zen-spacing

This commit is contained in:
Felix Roos
2024-07-27 15:16:14 +02:00
39 changed files with 1282 additions and 69 deletions
+22 -38
View File
@@ -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'}
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}
/>
);
}
@@ -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
View 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;
@@ -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>
);
}
@@ -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>
);
}
@@ -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';
+4
View File
@@ -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();