mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-16 08:08:34 +00:00
separate header + footer components
This commit is contained in:
parent
d957b19f55
commit
21503c0c38
@ -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
|
||||
389
repl/src/App.jsx
389
repl/src/App.jsx
@ -4,25 +4,24 @@ 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 { 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 }) => (
|
||||
<>
|
||||
<div
|
||||
onClick={() => 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}
|
||||
</div>
|
||||
{activeFooter === name && <>{children}</>}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
// bg-gradient-to-t from-blue-900 to-slate-900
|
||||
// bg-gradient-to-t from-green-900 to-slate-900
|
||||
<div
|
||||
className={cx(
|
||||
'h-screen flex flex-col',
|
||||
// 'bg-gradient-to-t from-green-900 to-slate-900', //
|
||||
)}
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
started,
|
||||
pending,
|
||||
isDirty,
|
||||
lastShared,
|
||||
activeCode,
|
||||
handleChangeCode,
|
||||
handleTogglePlay,
|
||||
handleUpdate,
|
||||
handleShuffle,
|
||||
handleShare,
|
||||
}}
|
||||
>
|
||||
{!hideHeader && (
|
||||
<header
|
||||
id="header"
|
||||
className={cx(
|
||||
'flex-none w-full md:flex text-black justify-between z-[100] text-lg bg-header select-none sticky top-0',
|
||||
isEmbedded ? 'h-12 md:h-8' : 'h-25 md:h-14',
|
||||
)}
|
||||
>
|
||||
<div className="px-4 flex items-center space-x-2 pt-2 md:pt-0 pointer-events-none">
|
||||
{/* <img
|
||||
src={logo}
|
||||
className={cx('Tidal-logo', isEmbedded ? 'w-8 h-8' : 'w-10 h-10', started && 'animate-pulse')} // 'bg-[#ffffff80] rounded-full'
|
||||
alt="logo"
|
||||
/> */}
|
||||
<h1
|
||||
className={cx(
|
||||
isEmbedded ? 'text-l' : 'text-xl',
|
||||
// 'bg-clip-text bg-gradient-to-r from-primary to-secondary text-transparent font-bold',
|
||||
'text-white font-bold flex space-x-2',
|
||||
)}
|
||||
>
|
||||
<div className={cx('mt-[1px]', started && 'animate-spin')}>🌀</div>
|
||||
<div className={cx(started && 'animate-pulse')}>
|
||||
<span className="">strudel</span> <span className="text-sm">REPL</span>
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex max-w-full overflow-auto text-white ">
|
||||
<button
|
||||
onClick={handleTogglePlay}
|
||||
className={cx(!isEmbedded ? 'p-2' : 'px-2', 'hover:text-tertiary', !started && 'animate-pulse')}
|
||||
>
|
||||
{!pending ? (
|
||||
<span className={cx('flex items-center space-x-1', isEmbedded ? 'w-16' : 'w-16')}>
|
||||
{started ? <StopCircleIcon className="w-5 h-5" /> : <PlayCircleIcon className="w-5 h-5" />}
|
||||
<span>{started ? 'stop' : 'play'}</span>
|
||||
</span>
|
||||
) : (
|
||||
<>loading...</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleUpdate}
|
||||
className={cx(
|
||||
'flex items-center space-x-1',
|
||||
!isEmbedded ? 'p-2' : 'px-2',
|
||||
!isDirty || !activeCode ? 'opacity-50' : 'hover:text-tertiary',
|
||||
)}
|
||||
>
|
||||
<CommandLineIcon className="w-5 h-5" />
|
||||
<span>update</span>
|
||||
</button>
|
||||
{!isEmbedded && (
|
||||
<button className="hover:text-tertiary p-2 flex items-center space-x-1" onClick={handleShuffle}>
|
||||
<SparklesIcon className="w-5 h-5" />
|
||||
<span> shuffle</span>
|
||||
</button>
|
||||
)}
|
||||
{!isEmbedded && (
|
||||
<a
|
||||
href="./tutorial"
|
||||
className={cx('hover:text-tertiary flex items-center space-x-1', !isEmbedded ? 'p-2' : 'px-2')}
|
||||
>
|
||||
<AcademicCapIcon className="w-5 h-5" />
|
||||
<span>learn</span>
|
||||
</a>
|
||||
)}
|
||||
{!isEmbedded && (
|
||||
<button
|
||||
className={cx(
|
||||
'cursor-pointer hover:text-tertiary flex items-center space-x-1',
|
||||
!isEmbedded ? 'p-2' : 'px-2',
|
||||
)}
|
||||
onClick={handleShare}
|
||||
>
|
||||
<LinkIcon className="w-5 h-5" />
|
||||
<span>share{lastShared && lastShared === (activeCode || code) ? 'd!' : ''}</span>
|
||||
</button>
|
||||
)}
|
||||
{isEmbedded && (
|
||||
<button className={cx('hover:text-tertiary px-2')}>
|
||||
<a href={window.location.href} target="_blank" rel="noopener noreferrer" title="Open in REPL">
|
||||
🚀 open
|
||||
</a>
|
||||
</button>
|
||||
)}
|
||||
{isEmbedded && (
|
||||
<button className={cx('hover:text-tertiary px-2')}>
|
||||
<a
|
||||
onClick={() => {
|
||||
window.location.href = initialUrl;
|
||||
window.location.reload();
|
||||
}}
|
||||
title="Reset"
|
||||
>
|
||||
💔 reset
|
||||
</a>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
)}
|
||||
<section className="grow flex text-gray-100 relative overflow-auto cursor-text pb-0" id="code">
|
||||
<CodeMirror value={code} onChange={handleChangeCode} onViewChanged={setView} />
|
||||
</section>
|
||||
<footer className="bg-footer z-[20]">
|
||||
<div className="flex justify-between px-2">
|
||||
<div className="flex pb-2 select-none">
|
||||
<FooterTab name="intro" />
|
||||
<FooterTab name="samples" />
|
||||
<FooterTab name="console" label={`console (${log.length})`} />
|
||||
</div>
|
||||
{activeFooter !== '' && (
|
||||
<button onClick={() => setActiveFooter('')} className="text-white">
|
||||
<XMarkIcon className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{activeFooter !== '' && (
|
||||
<div
|
||||
className="text-white font-mono text-sm h-72 max-h-[33vh] flex-none overflow-auto max-w-full px-4"
|
||||
ref={footerContent}
|
||||
>
|
||||
{activeFooter === 'intro' && (
|
||||
<div className="prose prose-invert max-w-[600px] pt-2 font-sans pb-8">
|
||||
<h3>
|
||||
<span className={cx('animate-spin inline-block select-none')}>🌀</span> welcome
|
||||
</h3>
|
||||
<p>
|
||||
You have found <span className="underline">strudel</span>, a new live coding platform to write dynamic
|
||||
music pieces in the browser! It is free and open-source and made for beginners and experts alike. To
|
||||
get started:
|
||||
<br />
|
||||
<br />
|
||||
<span className="underline">1. hit play</span> -{' '}
|
||||
<span className="underline">2. change something</span> -{' '}
|
||||
<span className="underline">3. hit update</span>
|
||||
<br />
|
||||
If you don't like what you hear, try <span className="underline">shuffle</span>!
|
||||
</p>
|
||||
<p>
|
||||
To learn more about what this all means, check out the{' '}
|
||||
<a href="https://strudel.tidalcycles.org/tutorial" target="_blank">
|
||||
interactive tutorial
|
||||
</a>
|
||||
. Also feel free to join the{' '}
|
||||
<a href="https://discord.com/invite/HGEdXmRkzT" target="_blank">
|
||||
tidalcycles discord channel
|
||||
</a>{' '}
|
||||
to ask any questions, give feedback or just say hello.
|
||||
</p>
|
||||
<h3>about</h3>
|
||||
<p>
|
||||
strudel is a JavaScript version of{' '}
|
||||
<a href="tidalcycles.org/" target="_blank">
|
||||
tidalcycles
|
||||
</a>
|
||||
, which is a popular live coding language for music, written in Haskell. You can find the source code
|
||||
at{' '}
|
||||
<a href="https://github.com/tidalcycles/strudel" target="_blank">
|
||||
github
|
||||
</a>
|
||||
. Please consider to{' '}
|
||||
<a href="https://opencollective.com/tidalcycles" target="_blank">
|
||||
support this project
|
||||
</a>{' '}
|
||||
to ensure ongoing development 💖
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{activeFooter === 'console' && (
|
||||
<div className="break-all">
|
||||
{log.map((l, i) => {
|
||||
const message = linkify(l.message);
|
||||
return (
|
||||
<div
|
||||
key={l.id}
|
||||
className={cx(l.type === 'error' && 'text-red-500', l.type === 'highlight' && 'text-highlight')}
|
||||
>
|
||||
<span dangerouslySetInnerHTML={{ __html: message }} />
|
||||
{l.count ? ` (${l.count})` : ''}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{activeFooter === 'samples' && (
|
||||
<div className="break-normal w-full">
|
||||
<span className="text-white">{loadedSamples.length} banks loaded:</span>
|
||||
{loadedSamples.map(([name, samples]) => (
|
||||
<span key={name} className="cursor-pointer hover:text-tertiary" onClick={() => {}}>
|
||||
{' '}
|
||||
{name}(
|
||||
{Array.isArray(samples)
|
||||
? samples.length
|
||||
: typeof samples === 'object'
|
||||
? Object.values(samples).length
|
||||
: 1}
|
||||
){' '}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cx(
|
||||
'h-screen flex flex-col',
|
||||
// 'bg-gradient-to-t from-green-900 to-slate-900', //
|
||||
)}
|
||||
</footer>
|
||||
</div>
|
||||
>
|
||||
{!hideHeader && <Header />}
|
||||
<section className="grow flex text-gray-100 relative overflow-auto cursor-text pb-0" id="code">
|
||||
<CodeMirror value={code} onChange={handleChangeCode} onViewChanged={setView} />
|
||||
</section>
|
||||
<Footer />
|
||||
</div>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
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, '<a class="underline" href="$1" target="_blank">$1</a>');
|
||||
|
||||
//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<a class="underline" href="http://$2" target="_blank">$2</a>',
|
||||
);
|
||||
|
||||
//Change email addresses to mailto:: links.
|
||||
replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
|
||||
replacedText = replacedText.replace(replacePattern3, '<a class="underline" href="mailto:$1">$1</a>');
|
||||
|
||||
return replacedText;
|
||||
}
|
||||
|
||||
192
repl/src/Footer.jsx
Normal file
192
repl/src/Footer.jsx
Normal file
@ -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 }) => (
|
||||
<>
|
||||
<div
|
||||
onClick={() => 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}
|
||||
</div>
|
||||
{activeFooter === name && <>{children}</>}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<footer className="bg-footer z-[20]">
|
||||
<div className="flex justify-between px-2">
|
||||
<div className="flex pb-2 select-none">
|
||||
<FooterTab name="intro" />
|
||||
<FooterTab name="samples" />
|
||||
<FooterTab name="console" label={`console (${log.length})`} />
|
||||
</div>
|
||||
{activeFooter !== '' && (
|
||||
<button onClick={() => setActiveFooter('')} className="text-white">
|
||||
<XMarkIcon className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{activeFooter !== '' && (
|
||||
<div
|
||||
className="text-white font-mono text-sm h-72 max-h-[33vh] flex-none overflow-auto max-w-full px-4"
|
||||
ref={footerContent}
|
||||
>
|
||||
{activeFooter === 'intro' && (
|
||||
<div className="prose prose-invert max-w-[600px] pt-2 font-sans pb-8">
|
||||
<h3>
|
||||
<span className={cx('animate-spin inline-block select-none')}>🌀</span> welcome
|
||||
</h3>
|
||||
<p>
|
||||
You have found <span className="underline">strudel</span>, a new live coding platform to write dynamic
|
||||
music pieces in the browser! It is free and open-source and made for beginners and experts alike. To get
|
||||
started:
|
||||
<br />
|
||||
<br />
|
||||
<span className="underline">1. hit play</span> - <span className="underline">2. change something</span>{' '}
|
||||
- <span className="underline">3. hit update</span>
|
||||
<br />
|
||||
If you don't like what you hear, try <span className="underline">shuffle</span>!
|
||||
</p>
|
||||
<p>
|
||||
To learn more about what this all means, check out the{' '}
|
||||
<a href="https://strudel.tidalcycles.org/tutorial" target="_blank">
|
||||
interactive tutorial
|
||||
</a>
|
||||
. Also feel free to join the{' '}
|
||||
<a href="https://discord.com/invite/HGEdXmRkzT" target="_blank">
|
||||
tidalcycles discord channel
|
||||
</a>{' '}
|
||||
to ask any questions, give feedback or just say hello.
|
||||
</p>
|
||||
<h3>about</h3>
|
||||
<p>
|
||||
strudel is a JavaScript version of{' '}
|
||||
<a href="tidalcycles.org/" target="_blank">
|
||||
tidalcycles
|
||||
</a>
|
||||
, which is a popular live coding language for music, written in Haskell. You can find the source code at{' '}
|
||||
<a href="https://github.com/tidalcycles/strudel" target="_blank">
|
||||
github
|
||||
</a>
|
||||
. Please consider to{' '}
|
||||
<a href="https://opencollective.com/tidalcycles" target="_blank">
|
||||
support this project
|
||||
</a>{' '}
|
||||
to ensure ongoing development 💖
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{activeFooter === 'console' && (
|
||||
<div className="break-all">
|
||||
{log.map((l, i) => {
|
||||
const message = linkify(l.message);
|
||||
return (
|
||||
<div
|
||||
key={l.id}
|
||||
className={cx(l.type === 'error' && 'text-red-500', l.type === 'highlight' && 'text-highlight')}
|
||||
>
|
||||
<span dangerouslySetInnerHTML={{ __html: message }} />
|
||||
{l.count ? ` (${l.count})` : ''}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{activeFooter === 'samples' && (
|
||||
<div className="break-normal w-full">
|
||||
<span className="text-white">{loadedSamples.length} banks loaded:</span>
|
||||
{loadedSamples.map(([name, samples]) => (
|
||||
<span key={name} className="cursor-pointer hover:text-tertiary" onClick={() => {}}>
|
||||
{' '}
|
||||
{name}(
|
||||
{Array.isArray(samples)
|
||||
? samples.length
|
||||
: typeof samples === 'object'
|
||||
? Object.values(samples).length
|
||||
: 1}
|
||||
){' '}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
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, '<a class="underline" href="$1" target="_blank">$1</a>');
|
||||
|
||||
//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<a class="underline" href="http://$2" target="_blank">$2</a>',
|
||||
);
|
||||
|
||||
//Change email addresses to mailto:: links.
|
||||
replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim;
|
||||
replacedText = replacedText.replace(replacePattern3, '<a class="underline" href="mailto:$1">$1</a>');
|
||||
|
||||
return replacedText;
|
||||
}
|
||||
128
repl/src/Header.jsx
Normal file
128
repl/src/Header.jsx
Normal file
@ -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 (
|
||||
<header
|
||||
id="header"
|
||||
className={cx(
|
||||
'flex-none w-full md:flex text-black justify-between z-[100] text-lg bg-header select-none sticky top-0',
|
||||
isEmbedded ? 'h-12 md:h-8' : 'h-25 md:h-14',
|
||||
)}
|
||||
>
|
||||
<div className="px-4 flex items-center space-x-2 pt-2 md:pt-0 pointer-events-none">
|
||||
{/* <img
|
||||
src={logo}
|
||||
className={cx('Tidal-logo', isEmbedded ? 'w-8 h-8' : 'w-10 h-10', started && 'animate-pulse')} // 'bg-[#ffffff80] rounded-full'
|
||||
alt="logo"
|
||||
/> */}
|
||||
<h1
|
||||
className={cx(
|
||||
isEmbedded ? 'text-l' : 'text-xl',
|
||||
// 'bg-clip-text bg-gradient-to-r from-primary to-secondary text-transparent font-bold',
|
||||
'text-white font-bold flex space-x-2',
|
||||
)}
|
||||
>
|
||||
<div className={cx('mt-[1px]', started && 'animate-spin')}>🌀</div>
|
||||
<div className={cx(started && 'animate-pulse')}>
|
||||
<span className="">strudel</span> <span className="text-sm">REPL</span>
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex max-w-full overflow-auto text-white ">
|
||||
<button
|
||||
onClick={handleTogglePlay}
|
||||
className={cx(!isEmbedded ? 'p-2' : 'px-2', 'hover:text-tertiary', !started && 'animate-pulse')}
|
||||
>
|
||||
{!pending ? (
|
||||
<span className={cx('flex items-center space-x-1', isEmbedded ? 'w-16' : 'w-16')}>
|
||||
{started ? <StopCircleIcon className="w-5 h-5" /> : <PlayCircleIcon className="w-5 h-5" />}
|
||||
<span>{started ? 'stop' : 'play'}</span>
|
||||
</span>
|
||||
) : (
|
||||
<>loading...</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleUpdate}
|
||||
className={cx(
|
||||
'flex items-center space-x-1',
|
||||
!isEmbedded ? 'p-2' : 'px-2',
|
||||
!isDirty || !activeCode ? 'opacity-50' : 'hover:text-tertiary',
|
||||
)}
|
||||
>
|
||||
<CommandLineIcon className="w-5 h-5" />
|
||||
<span>update</span>
|
||||
</button>
|
||||
{!isEmbedded && (
|
||||
<button className="hover:text-tertiary p-2 flex items-center space-x-1" onClick={handleShuffle}>
|
||||
<SparklesIcon className="w-5 h-5" />
|
||||
<span> shuffle</span>
|
||||
</button>
|
||||
)}
|
||||
{!isEmbedded && (
|
||||
<a
|
||||
href="./tutorial"
|
||||
className={cx('hover:text-tertiary flex items-center space-x-1', !isEmbedded ? 'p-2' : 'px-2')}
|
||||
>
|
||||
<AcademicCapIcon className="w-5 h-5" />
|
||||
<span>learn</span>
|
||||
</a>
|
||||
)}
|
||||
{!isEmbedded && (
|
||||
<button
|
||||
className={cx(
|
||||
'cursor-pointer hover:text-tertiary flex items-center space-x-1',
|
||||
!isEmbedded ? 'p-2' : 'px-2',
|
||||
)}
|
||||
onClick={handleShare}
|
||||
>
|
||||
<LinkIcon className="w-5 h-5" />
|
||||
<span>share{lastShared && lastShared === (activeCode || code) ? 'd!' : ''}</span>
|
||||
</button>
|
||||
)}
|
||||
{isEmbedded && (
|
||||
<button className={cx('hover:text-tertiary px-2')}>
|
||||
<a href={window.location.href} target="_blank" rel="noopener noreferrer" title="Open in REPL">
|
||||
🚀 open
|
||||
</a>
|
||||
</button>
|
||||
)}
|
||||
{isEmbedded && (
|
||||
<button className={cx('hover:text-tertiary px-2')}>
|
||||
<a
|
||||
onClick={() => {
|
||||
window.location.href = initialUrl;
|
||||
window.location.reload();
|
||||
}}
|
||||
title="Reset"
|
||||
>
|
||||
💔 reset
|
||||
</a>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user