diff --git a/repl/README.md b/repl/README.md
index 67339400..0df11d94 100644
--- a/repl/README.md
+++ b/repl/README.md
@@ -47,3 +47,4 @@ currently broken / buggy:
- [ ] find a way to display errors when console is closed / another tab selected
- [x] scheduler.getPhase is quantized to clock interval
- => draw was choppy + that also caused useHighlighting bugs
+- [ ] pianoroll keeps rolling when pressing stop
\ No newline at end of file
diff --git a/repl/src/App.jsx b/repl/src/App.jsx
index f84eec76..6fc03602 100644
--- a/repl/src/App.jsx
+++ b/repl/src/App.jsx
@@ -4,25 +4,24 @@ Copyright (C) 2022 Strudel contributors - see .
*/
-import { CodeMirror, cx, flash, useHighlighting } from '@strudel.cycles/react';
-import React, { useEffect, useLayoutEffect, useRef, useState, useCallback } from 'react';
-import './App.css';
-import * as tunes from './tunes.mjs';
-import { prebake } from './prebake.mjs';
-import * as WebDirt from 'WebDirt';
-import { resetLoadedSamples, getAudioContext, getLoadedSamples } from '@strudel.cycles/webaudio';
-import { controls, evalScope, logger, cleanupDraw, cleanupUi } from '@strudel.cycles/core';
+import { cleanupDraw, cleanupUi, controls, evalScope, logger } from '@strudel.cycles/core';
+import { CodeMirror, cx, flash, useHighlighting, useStrudel } from '@strudel.cycles/react';
+import {
+ getAudioContext,
+ getLoadedSamples,
+ initAudioOnFirstClick,
+ resetLoadedSamples,
+ webaudioOutput,
+} from '@strudel.cycles/webaudio';
import { createClient } from '@supabase/supabase-js';
import { nanoid } from 'nanoid';
-import { useStrudel } from '@strudel.cycles/react';
-import { webaudioOutput, initAudioOnFirstClick } from '@strudel.cycles/webaudio';
-import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
-import StopCircleIcon from '@heroicons/react/20/solid/StopCircleIcon';
-import CommandLineIcon from '@heroicons/react/20/solid/CommandLineIcon';
-import SparklesIcon from '@heroicons/react/20/solid/SparklesIcon';
-import LinkIcon from '@heroicons/react/20/solid/LinkIcon';
-import XMarkIcon from '@heroicons/react/20/solid/XMarkIcon';
-import AcademicCapIcon from '@heroicons/react/20/solid/AcademicCapIcon';
+import React, { createContext, useCallback, useEffect, useState } from 'react';
+import * as WebDirt from 'WebDirt';
+import './App.css';
+import { Footer } from './Footer';
+import { Header } from './Header';
+import { prebake } from './prebake.mjs';
+import * as tunes from './tunes.mjs';
initAudioOnFirstClick();
@@ -52,7 +51,7 @@ evalScope(
...modules,
);
-let loadedSamples = [];
+export let loadedSamples = [];
const presets = prebake();
Promise.all([...modules, presets]).then((data) => {
@@ -102,66 +101,25 @@ function getRandomTune() {
}
const { code: randomTune, name } = getRandomTune();
-const isEmbedded = window.location !== window.parent.location;
+
+export const AppContext = createContext();
+
function App() {
const [view, setView] = useState(); // codemirror view
const [lastShared, setLastShared] = useState();
- const [activeFooter, setActiveFooter] = useState('console');
- // logger
- const [log, setLog] = useState([]);
- useLogger(
- useCallback((e) => {
- const { message, type, data } = e.detail;
- setLog((l) => {
- const lastLog = l.length ? l[l.length - 1] : undefined;
- const id = nanoid(12);
- // if (type === 'loaded-sample' && lastLog.type === 'load-sample' && lastLog.url === data.url) {
- if (type === 'loaded-sample') {
- // const loadIndex = l.length - 1;
- const loadIndex = l.findIndex(({ data: { url }, type }) => type === 'load-sample' && url === data.url);
- l[loadIndex] = { message, type, id, data };
- } else if (lastLog && lastLog.message === message) {
- l = l.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, id, data }]);
- } else {
- l = l.concat([{ message, type, id, data }]);
- }
- return l.slice(-20);
- });
- }, []),
- );
- const footerContent = useRef();
- useLayoutEffect(() => {
- if (footerContent.current && activeFooter === 'console') {
- // scroll log box to bottom when log changes
- footerContent.current.scrollTop = footerContent.current?.scrollHeight;
- }
- }, [log, activeFooter]);
- useLayoutEffect(() => {
- if (!footerContent.current) {
- } else if (activeFooter === 'console') {
- footerContent.current.scrollTop = footerContent.current?.scrollHeight;
- } else {
- footerContent.current.scrollTop = 0;
- }
- }, [activeFooter]);
-
- const { code, setCode, scheduler, evaluate, activateCode, error, isDirty, activeCode, pattern, started, stop } =
- useStrudel({
- initialCode: '// LOADING',
- defaultOutput: webaudioOutput,
- getTime,
- autolink: true,
- onEvalError: () => {
- setActiveFooter('console');
- },
- });
+ const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop } = useStrudel({
+ initialCode: '// LOADING',
+ defaultOutput: webaudioOutput,
+ getTime,
+ autolink: true,
+ });
// init code
useEffect(() => {
initCode().then((decoded) => {
if (!decoded) {
- setActiveFooter('intro');
+ setActiveFooter('intro'); // TODO: get rid
}
logger(
`Welcome to Strudel! ${
@@ -173,6 +131,7 @@ function App() {
});
}, []);
+ // keyboard shortcuts
useKeydown(
useCallback(
async (e) => {
@@ -191,17 +150,22 @@ function App() {
),
);
+ // highlighting
useHighlighting({
view,
pattern,
active: started && !activeCode?.includes('strudel disable-highlighting'),
- getTime: () => scheduler.getPhase(), // TODO: problem: phase is quantized to clock interval...
+ getTime: () => scheduler.getPhase(),
});
//
// UI Actions
//
+ const handleChangeCode = (c) => {
+ setCode(c);
+ started && logger('[edit] code changed. hit ctrl+enter to update');
+ };
const handleTogglePlay = async () => {
await getAudioContext().resume(); // fixes no sound in ios webkit
if (!started) {
@@ -210,6 +174,7 @@ function App() {
} else {
logger('[repl] stopped. tip: you can also stop by pressing ctrl+dot', 'highlight');
stop();
+ // cleanupDraw();
}
};
const handleUpdate = () => {
@@ -254,248 +219,42 @@ function App() {
}
};
- const handleChangeCode = (c) => {
- setCode(c);
- started && logger('[edit] code changed. hit ctrl+enter to update');
- };
-
- const FooterTab = ({ children, name, label }) => (
- <>
-
setActiveFooter(name)}
- className={cx(
- 'h-8 px-2 text-white cursor-pointer hover:text-tertiary flex items-center space-x-1 border-b',
- activeFooter === name ? 'border-white hover:border-tertiary' : 'border-transparent',
- )}
- >
- {label || name}
-
- {activeFooter === name && <>{children}>}
- >
- );
-
return (
// bg-gradient-to-t from-blue-900 to-slate-900
// bg-gradient-to-t from-green-900 to-slate-900
-
- {!hideHeader && (
-
- )}
-
-
+
);
}
export default App;
-function useEvent(name, onTrigger, useCapture = false) {
+export function useEvent(name, onTrigger, useCapture = false) {
useEffect(() => {
document.addEventListener(name, onTrigger, useCapture);
return () => {
@@ -503,32 +262,6 @@ function useEvent(name, onTrigger, useCapture = false) {
};
}, [onTrigger]);
}
-
-function useLogger(onTrigger) {
- useEvent(logger.key, onTrigger);
-}
-
function useKeydown(onTrigger) {
useEvent('keydown', onTrigger, true);
}
-
-function linkify(inputText) {
- var replacedText, replacePattern1, replacePattern2, replacePattern3;
-
- //URLs starting with http://, https://, or ftp://
- replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
- replacedText = inputText.replace(replacePattern1, '$1');
-
- //URLs starting with "www." (without // before it, or it'd re-link the ones done above).
- replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
- replacedText = replacedText.replace(
- replacePattern2,
- '$1$2',
- );
-
- //Change email addresses to mailto:: links.
- replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
- replacedText = replacedText.replace(replacePattern3, '$1');
-
- return replacedText;
-}
diff --git a/repl/src/Footer.jsx b/repl/src/Footer.jsx
new file mode 100644
index 00000000..39a26697
--- /dev/null
+++ b/repl/src/Footer.jsx
@@ -0,0 +1,192 @@
+import XMarkIcon from '@heroicons/react/20/solid/XMarkIcon';
+import { logger } from '@strudel.cycles/core';
+import { cx } from '@strudel.cycles/react';
+import { nanoid } from 'nanoid';
+import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
+import { useEvent, loadedSamples } from './App';
+
+export function Footer() {
+ const [activeFooter, setActiveFooter] = useState('console');
+ const footerContent = useRef();
+ const [log, setLog] = useState([]);
+
+ useLayoutEffect(() => {
+ if (footerContent.current && activeFooter === 'console') {
+ // scroll log box to bottom when log changes
+ footerContent.current.scrollTop = footerContent.current?.scrollHeight;
+ }
+ }, [log, activeFooter]);
+ useLayoutEffect(() => {
+ if (!footerContent.current) {
+ } else if (activeFooter === 'console') {
+ footerContent.current.scrollTop = footerContent.current?.scrollHeight;
+ } else {
+ footerContent.current.scrollTop = 0;
+ }
+ }, [activeFooter]);
+
+ useLogger(
+ useCallback((e) => {
+ const { message, type, data } = e.detail;
+ if (type === 'error') {
+ setActiveFooter('console');
+ }
+ setLog((l) => {
+ const lastLog = l.length ? l[l.length - 1] : undefined;
+ const id = nanoid(12);
+ // if (type === 'loaded-sample' && lastLog.type === 'load-sample' && lastLog.url === data.url) {
+ if (type === 'loaded-sample') {
+ // const loadIndex = l.length - 1;
+ const loadIndex = l.findIndex(({ data: { url }, type }) => type === 'load-sample' && url === data.url);
+ l[loadIndex] = { message, type, id, data };
+ } else if (lastLog && lastLog.message === message) {
+ l = l.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, id, data }]);
+ } else {
+ l = l.concat([{ message, type, id, data }]);
+ }
+ return l.slice(-20);
+ });
+ }, []),
+ );
+
+ const FooterTab = ({ children, name, label }) => (
+ <>
+ setActiveFooter(name)}
+ className={cx(
+ 'h-8 px-2 text-white cursor-pointer hover:text-tertiary flex items-center space-x-1 border-b',
+ activeFooter === name ? 'border-white hover:border-tertiary' : 'border-transparent',
+ )}
+ >
+ {label || name}
+
+ {activeFooter === name && <>{children}>}
+ >
+ );
+ return (
+
+ );
+}
+
+function useLogger(onTrigger) {
+ useEvent(logger.key, onTrigger);
+}
+
+function linkify(inputText) {
+ var replacedText, replacePattern1, replacePattern2, replacePattern3;
+
+ //URLs starting with http://, https://, or ftp://
+ replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
+ replacedText = inputText.replace(replacePattern1, '$1');
+
+ //URLs starting with "www." (without // before it, or it'd re-link the ones done above).
+ replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim;
+ replacedText = replacedText.replace(
+ replacePattern2,
+ '$1$2',
+ );
+
+ //Change email addresses to mailto:: links.
+ replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
+ replacedText = replacedText.replace(replacePattern3, '$1');
+
+ return replacedText;
+}
diff --git a/repl/src/Header.jsx b/repl/src/Header.jsx
new file mode 100644
index 00000000..07de9979
--- /dev/null
+++ b/repl/src/Header.jsx
@@ -0,0 +1,128 @@
+import AcademicCapIcon from '@heroicons/react/20/solid/AcademicCapIcon';
+import CommandLineIcon from '@heroicons/react/20/solid/CommandLineIcon';
+import LinkIcon from '@heroicons/react/20/solid/LinkIcon';
+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 '@strudel.cycles/react';
+import React, { useContext } from 'react';
+import { AppContext } from './App';
+import './App.css';
+
+const isEmbedded = window.location !== window.parent.location;
+
+export function Header() {
+ const {
+ started,
+ pending,
+ isDirty,
+ lastShared,
+ activeCode,
+ handleTogglePlay,
+ handleUpdate,
+ handleShuffle,
+ handleShare,
+ } = useContext(AppContext);
+ return (
+
+ );
+}