mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 05:38:34 +00:00
logger works now outside of react
+ dynamic sample loading logs + remove old sampler code
This commit is contained in:
parent
23e059a065
commit
0b9d59bf0e
@ -1,3 +1,18 @@
|
||||
export function logger(message) {
|
||||
console.log(`%c${message}`, 'background-color: black;color:white;padding:4px;border-radius:15px');
|
||||
export const logKey = 'strudel.log';
|
||||
|
||||
export function logger(message, type, data = {}) {
|
||||
console.log(`%c${message}`, 'background-color: black;color:white;border-radius:15px');
|
||||
if (typeof CustomEvent !== 'undefined') {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent(logKey, {
|
||||
detail: {
|
||||
message,
|
||||
type,
|
||||
data,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logger.key = logKey;
|
||||
|
||||
22
packages/react/dist/index.cjs.js
vendored
22
packages/react/dist/index.cjs.js
vendored
File diff suppressed because one or more lines are too long
414
packages/react/dist/index.es.js
vendored
414
packages/react/dist/index.es.js
vendored
File diff suppressed because one or more lines are too long
@ -63,6 +63,13 @@ function useStrudel({
|
||||
}
|
||||
}, [activateCode, evalOnMount, code]);
|
||||
|
||||
// this will stop the scheduler when hot reloading in development
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
scheduler.stop();
|
||||
};
|
||||
}, [scheduler]);
|
||||
|
||||
const togglePlay = async () => {
|
||||
if (started) {
|
||||
scheduler.pause();
|
||||
|
||||
@ -1,13 +1,36 @@
|
||||
import { logger } from '@strudel.cycles/core';
|
||||
|
||||
const bufferCache = {}; // string: Promise<ArrayBuffer>
|
||||
const loadCache = {}; // string: Promise<ArrayBuffer>
|
||||
|
||||
export const getCachedBuffer = (url) => bufferCache[url];
|
||||
|
||||
export const loadBuffer = (url, ac) => {
|
||||
function humanFileSize(bytes, si) {
|
||||
var thresh = si ? 1000 : 1024;
|
||||
if (bytes < thresh) return bytes + ' B';
|
||||
var units = si
|
||||
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||
var u = -1;
|
||||
do {
|
||||
bytes /= thresh;
|
||||
++u;
|
||||
} while (bytes >= thresh);
|
||||
return bytes.toFixed(1) + ' ' + units[u];
|
||||
}
|
||||
|
||||
export const loadBuffer = (url, ac, s, n = 0) => {
|
||||
const label = s ? `sound "${s}:${n}"` : 'sample';
|
||||
if (!loadCache[url]) {
|
||||
logger(`[sampler] load ${label}..`, 'load-sample', { url });
|
||||
const timestamp = Date.now();
|
||||
loadCache[url] = fetch(url)
|
||||
.then((res) => res.arrayBuffer())
|
||||
.then(async (res) => {
|
||||
const took = (Date.now() - timestamp);
|
||||
const size = humanFileSize(res.byteLength);
|
||||
// const downSpeed = humanFileSize(res.byteLength / took);
|
||||
logger(`[sampler] load ${label}... done! loaded ${size} in ${took}ms`, 'loaded-sample', { url });
|
||||
const decoded = await ac.decodeAudioData(res);
|
||||
bufferCache[url] = decoded;
|
||||
return decoded;
|
||||
@ -29,66 +52,7 @@ export const getLoadedBuffer = (url) => {
|
||||
return bufferCache[url];
|
||||
};
|
||||
|
||||
/* export const playBuffer = (buffer, time = ac.currentTime, destination = ac.destination) => {
|
||||
const src = ac.createBufferSource();
|
||||
src.buffer = buffer;
|
||||
src.connect(destination);
|
||||
src.start(time);
|
||||
};
|
||||
|
||||
export const playSample = async (url) => playBuffer(await loadBuffer(url)); */
|
||||
|
||||
// https://estuary.mcmaster.ca/samples/resources.json
|
||||
// Array<{ "url":string, "bank": string, "n": number}>
|
||||
// ritchse/tidal-drum-machines/tree/main/machines/AkaiLinn
|
||||
const githubCache = {};
|
||||
let sampleCache = { current: undefined };
|
||||
export const loadGithubSamples = async (path, nameFn) => {
|
||||
const storageKey = 'loadGithubSamples ' + path;
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
if (stored) {
|
||||
console.log('[sampler]: loaded sample list from localstorage', path);
|
||||
githubCache[path] = JSON.parse(stored);
|
||||
}
|
||||
if (githubCache[path]) {
|
||||
sampleCache.current = githubCache[path];
|
||||
return githubCache[path];
|
||||
}
|
||||
console.log('[sampler]: fetching sample list from github', path);
|
||||
try {
|
||||
const [user, repo, ...folders] = path.split('/');
|
||||
const baseUrl = `https://api.github.com/repos/${user}/${repo}/contents`;
|
||||
const banks = await fetch(`${baseUrl}/${folders.join('/')}`).then((res) => res.json());
|
||||
// fetch each subfolder
|
||||
githubCache[path] = (
|
||||
await Promise.all(
|
||||
banks.map(async ({ name, path }) => ({
|
||||
name,
|
||||
content: await fetch(`${baseUrl}/${path}`)
|
||||
.then((res) => res.json())
|
||||
.catch((err) => {
|
||||
console.error('could not load path', err);
|
||||
}),
|
||||
})),
|
||||
)
|
||||
)
|
||||
.filter(({ content }) => !!content)
|
||||
.reduce(
|
||||
(acc, { name, content }) => ({
|
||||
...acc,
|
||||
[nameFn?.(name) || name]: content.map(({ download_url }) => download_url),
|
||||
}),
|
||||
{},
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('[sampler]: failed to fetch sample list from github', err);
|
||||
return;
|
||||
}
|
||||
sampleCache.current = githubCache[path];
|
||||
localStorage.setItem(storageKey, JSON.stringify(sampleCache.current));
|
||||
console.log('[sampler]: loaded samples:', sampleCache.current);
|
||||
return githubCache[path];
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads a collection of samples to use with `s`
|
||||
|
||||
@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
|
||||
// import { Pattern, getFrequency, patternify2 } from '@strudel.cycles/core';
|
||||
import * as strudel from '@strudel.cycles/core';
|
||||
import { fromMidi, isNote, toMidi } from '@strudel.cycles/core';
|
||||
import { fromMidi, logger, toMidi } from '@strudel.cycles/core';
|
||||
import './feedbackdelay.mjs';
|
||||
import './reverb.mjs';
|
||||
import { loadBuffer, reverseBuffer } from './sampler.mjs';
|
||||
@ -112,9 +112,10 @@ const getSampleBufferSource = async (s, n, note, speed) => {
|
||||
const bank = samples?.[s];
|
||||
if (!bank) {
|
||||
throw new Error(
|
||||
`sample not found: "${s}", try one of ${Object.keys(samples)
|
||||
.map((s) => `"${s}"`)
|
||||
.join(', ')}.`,
|
||||
`sample not found: "${s}"`,
|
||||
// , try one of ${Object.keys(samples)
|
||||
// .map((s) => `"${s}"`)
|
||||
// .join(', ')}.
|
||||
);
|
||||
}
|
||||
if (typeof bank !== 'object') {
|
||||
@ -135,7 +136,7 @@ const getSampleBufferSource = async (s, n, note, speed) => {
|
||||
transpose = -midiDiff(closest); // semitones to repitch
|
||||
sampleUrl = bank[closest][n % bank[closest].length];
|
||||
}
|
||||
let buffer = await loadBuffer(sampleUrl, ac);
|
||||
let buffer = await loadBuffer(sampleUrl, ac, s, n);
|
||||
if (speed < 0) {
|
||||
// should this be cached?
|
||||
buffer = reverseBuffer(buffer);
|
||||
@ -337,21 +338,17 @@ export const webaudioOutput = async (hap, deadline, hapDuration) => {
|
||||
const soundfont = getSoundfontKey(s);
|
||||
let bufferSource;
|
||||
|
||||
try {
|
||||
if (soundfont) {
|
||||
// is soundfont
|
||||
bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac);
|
||||
} else {
|
||||
// is sample from loaded samples(..)
|
||||
bufferSource = await getSampleBufferSource(s, n, note, speed);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
return;
|
||||
if (soundfont) {
|
||||
// is soundfont
|
||||
bufferSource = await globalThis.getFontBufferSource(soundfont, note || n, ac);
|
||||
} else {
|
||||
// is sample from loaded samples(..)
|
||||
bufferSource = await getSampleBufferSource(s, n, note, speed);
|
||||
}
|
||||
// asny stuff above took too long?
|
||||
if (ac.currentTime > t) {
|
||||
console.warn('sample still loading:', s, n);
|
||||
logger(`[sampler] still loading sound "${s}:${n}"`, 'highlight');
|
||||
// console.warn('sample still loading:', s, n);
|
||||
return;
|
||||
}
|
||||
if (!bufferSource) {
|
||||
|
||||
323
repl/src/App.jsx
323
repl/src/App.jsx
@ -7,7 +7,7 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
// import { evaluate } from '@strudel.cycles/eval';
|
||||
import { CodeMirror, cx, flash, useHighlighting } from '@strudel.cycles/react';
|
||||
// import { cleanupDraw, cleanupUi, Tone } from '@strudel.cycles/tone';
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState, useCallback } from 'react';
|
||||
import './App.css';
|
||||
import logo from './logo.svg';
|
||||
import * as tunes from './tunes.mjs';
|
||||
@ -96,25 +96,29 @@ function getRandomTune() {
|
||||
const { code: randomTune, name } = getRandomTune();
|
||||
const isEmbedded = window.location !== window.parent.location;
|
||||
function App() {
|
||||
// const [editor, setEditor] = useState();
|
||||
const [view, setView] = useState();
|
||||
const [view, setView] = useState(); // codemirror view
|
||||
const [lastShared, setLastShared] = useState();
|
||||
|
||||
// logger
|
||||
const [log, setLog] = useState([]);
|
||||
const pushLog = (message, type) => {
|
||||
setLog((l) => {
|
||||
logger(message);
|
||||
const lastLog = l.length ? l[l.length - 1] : undefined;
|
||||
const index = (lastLog?.index ?? -1) + 1;
|
||||
if (lastLog && lastLog.message === message) {
|
||||
l = l.slice(0, -1).concat([{ message, type, count: (lastLog.count ?? 1) + 1, index }]);
|
||||
} else {
|
||||
l = l.concat([{ message, type, index }]);
|
||||
}
|
||||
return l.slice(-20);
|
||||
});
|
||||
};
|
||||
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') {
|
||||
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 logBox = useRef();
|
||||
useLayoutEffect(() => {
|
||||
if (logBox.current) {
|
||||
@ -123,20 +127,19 @@ function App() {
|
||||
}
|
||||
}, [log]);
|
||||
|
||||
// repl
|
||||
const { code, setCode, scheduler, evaluate, activateCode, error, isDirty, activeCode, pattern, started, stop } =
|
||||
useStrudel({
|
||||
initialCode: '// LOADING',
|
||||
defaultOutput: webaudioOutput,
|
||||
getTime,
|
||||
autolink: true,
|
||||
onLog: pushLog,
|
||||
onLog: logger,
|
||||
});
|
||||
|
||||
// init code
|
||||
useEffect(() => {
|
||||
initCode().then((decoded) => {
|
||||
pushLog(
|
||||
logger(
|
||||
`🌀 Welcome to Strudel! ${
|
||||
decoded ? `I have loaded the code from the URL.` : `A random code snippet named "${name}" has been loaded!`
|
||||
} Press play or hit ctrl+enter to run it!`,
|
||||
@ -146,33 +149,89 @@ function App() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
// set active pattern on ctrl+enter
|
||||
useLayoutEffect(() => {
|
||||
// TODO: make sure this is only fired when editor has focus
|
||||
const handleKeyPress = async (e) => {
|
||||
if (e.ctrlKey || e.altKey) {
|
||||
if (e.code === 'Enter') {
|
||||
e.preventDefault();
|
||||
flash(view);
|
||||
await activateCode();
|
||||
} else if (e.code === 'Period') {
|
||||
stop();
|
||||
e.preventDefault();
|
||||
useKeydown(
|
||||
useCallback(
|
||||
async (e) => {
|
||||
if (e.ctrlKey || e.altKey) {
|
||||
if (e.code === 'Enter') {
|
||||
e.preventDefault();
|
||||
flash(view);
|
||||
await activateCode();
|
||||
} else if (e.code === 'Period') {
|
||||
stop();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyPress, true);
|
||||
return () => window.removeEventListener('keydown', handleKeyPress, true);
|
||||
}, [activateCode, stop, view]);
|
||||
},
|
||||
[activateCode, stop, view],
|
||||
),
|
||||
);
|
||||
|
||||
useHighlighting({
|
||||
view,
|
||||
pattern,
|
||||
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
||||
getTime: () => scheduler.getPhase(),
|
||||
// getTime: () => Tone.getTransport().seconds,
|
||||
});
|
||||
|
||||
//
|
||||
// UI Actions
|
||||
//
|
||||
|
||||
const handleTogglePlay = async () => {
|
||||
await getAudioContext().resume(); // fixes no sound in ios webkit
|
||||
if (!started) {
|
||||
activateCode();
|
||||
} else {
|
||||
stop();
|
||||
}
|
||||
};
|
||||
const handleUpdate = () => {
|
||||
isDirty && activateCode();
|
||||
logger('Code updated! Tip: You can also update the code by pressing ctrl+enter.');
|
||||
};
|
||||
|
||||
const handleShuffle = async () => {
|
||||
const { code, name } = getRandomTune();
|
||||
logger(`✨ loading random tune "${name}"`);
|
||||
/*
|
||||
cleanupDraw();
|
||||
cleanupUi(); */
|
||||
resetLoadedSamples();
|
||||
await prebake(); // declare default samples
|
||||
await evaluate(code, false);
|
||||
};
|
||||
|
||||
const handleShare = async () => {
|
||||
const codeToShare = activeCode || code;
|
||||
if (lastShared === codeToShare) {
|
||||
logger(`Link already generated!`, 'error');
|
||||
return;
|
||||
}
|
||||
// generate uuid in the browser
|
||||
const hash = nanoid(12);
|
||||
const { data, error } = await supabase.from('code').insert([{ code: codeToShare, hash }]);
|
||||
if (!error) {
|
||||
setLastShared(activeCode || code);
|
||||
const shareUrl = window.location.origin + '?' + hash;
|
||||
// copy shareUrl to clipboard
|
||||
navigator.clipboard.writeText(shareUrl);
|
||||
const message = `Link copied to clipboard: ${shareUrl}`;
|
||||
// alert(message);
|
||||
logger(message, 'highlight');
|
||||
} else {
|
||||
console.log('error', error);
|
||||
const message = `Error: ${error.message}`;
|
||||
// alert(message);
|
||||
logger(message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangeCode = (c) => {
|
||||
setCode(c);
|
||||
started && logger('[edit] code changed. hit ctrl+enter to update');
|
||||
};
|
||||
|
||||
return (
|
||||
// bg-gradient-to-t from-blue-900 to-slate-900
|
||||
<div className="h-screen flex flex-col">
|
||||
@ -201,17 +260,7 @@ function App() {
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex max-w-full overflow-auto text-white ">
|
||||
<button
|
||||
onClick={async () => {
|
||||
await getAudioContext().resume(); // fixes no sound in ios webkit
|
||||
if (!started) {
|
||||
activateCode();
|
||||
} else {
|
||||
stop();
|
||||
}
|
||||
}}
|
||||
className={cx(!isEmbedded ? 'p-2' : 'px-2')}
|
||||
>
|
||||
<button onClick={handleTogglePlay} className={cx(!isEmbedded ? 'p-2' : 'px-2')}>
|
||||
{!pending ? (
|
||||
<span className={cx('flex items-center space-x-1 hover:text-primary', isEmbedded ? 'w-16' : 'w-16')}>
|
||||
{started ? <StopCircleIcon className="w-5 h-5" /> : <PlayCircleIcon className="w-5 h-5" />}
|
||||
@ -222,10 +271,7 @@ function App() {
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
isDirty && activateCode();
|
||||
pushLog('Code updated! Tip: You can also update the code by pressing ctrl+enter.');
|
||||
}}
|
||||
onClick={handleUpdate}
|
||||
className={cx(
|
||||
'flex items-center space-x-1',
|
||||
!isEmbedded ? 'p-2' : 'px-2',
|
||||
@ -236,22 +282,9 @@ function App() {
|
||||
<span>update</span>
|
||||
</button>
|
||||
{!isEmbedded && (
|
||||
<button
|
||||
className="hover:text-primary p-2 flex items-center space-x-1"
|
||||
onClick={async () => {
|
||||
const { code, name } = getRandomTune();
|
||||
pushLog(`✨ loading random tune "${name}"`);
|
||||
/*
|
||||
cleanupDraw();
|
||||
cleanupUi(); */
|
||||
resetLoadedSamples();
|
||||
await prebake(); // declare default samples
|
||||
await evaluate(code, false);
|
||||
}}
|
||||
>
|
||||
<button className="hover:text-primary p-2 flex items-center space-x-1" onClick={handleShuffle}>
|
||||
<SparklesIcon className="w-5 h-5" />
|
||||
<span> shuffle</span>
|
||||
{/* <MusicalNoteIcon /> <RadioIcon/> */}
|
||||
</button>
|
||||
)}
|
||||
{!isEmbedded && (
|
||||
@ -269,35 +302,10 @@ function App() {
|
||||
'cursor-pointer hover:text-primary flex items-center space-x-1',
|
||||
!isEmbedded ? 'p-2' : 'px-2',
|
||||
)}
|
||||
onClick={async () => {
|
||||
const codeToShare = activeCode || code;
|
||||
if (lastShared === codeToShare) {
|
||||
// alert('Link already generated!');
|
||||
pushLog(`Link already generated!`);
|
||||
return;
|
||||
}
|
||||
// generate uuid in the browser
|
||||
const hash = nanoid(12);
|
||||
const { data, error } = await supabase.from('code').insert([{ code: codeToShare, hash }]);
|
||||
if (!error) {
|
||||
setLastShared(activeCode || code);
|
||||
const shareUrl = window.location.origin + '?' + hash;
|
||||
// copy shareUrl to clipboard
|
||||
navigator.clipboard.writeText(shareUrl);
|
||||
const message = `Link copied to clipboard: ${shareUrl}`;
|
||||
// alert(message);
|
||||
pushLog(message);
|
||||
} else {
|
||||
console.log('error', error);
|
||||
const message = `Error: ${error.message}`;
|
||||
// alert(message);
|
||||
pushLog(message);
|
||||
}
|
||||
}}
|
||||
onClick={handleShare}
|
||||
>
|
||||
<LinkIcon className="w-5 h-5" />
|
||||
<span>share{lastShared && lastShared === (activeCode || code) ? 'd!' : ''}</span>
|
||||
{/* GlobaAlt Megaphone PaperAirplane Share */}
|
||||
</button>
|
||||
)}
|
||||
{isEmbedded && (
|
||||
@ -324,39 +332,25 @@ function App() {
|
||||
</header>
|
||||
)}
|
||||
<section className="grow flex text-gray-100 relative overflow-auto cursor-text pb-4" id="code">
|
||||
<CodeMirror
|
||||
value={code}
|
||||
onChange={(c) => {
|
||||
setCode(c);
|
||||
started && pushLog('[edit] code changed. hit ctrl+enter to update');
|
||||
}}
|
||||
onViewChanged={setView}
|
||||
/>
|
||||
<CodeMirror value={code} onChange={handleChangeCode} onViewChanged={setView} />
|
||||
</section>
|
||||
<footer className="bg-footer">
|
||||
{/* {error && (
|
||||
<div
|
||||
className={cx(
|
||||
'rounded-md pointer-events-none left-0 p-1 text-sm bg-black px-2 z-[20] max-w-screen break-all',
|
||||
'text-red-500',
|
||||
)}
|
||||
>
|
||||
{error?.message || 'unknown error'}
|
||||
</div>
|
||||
)} */}
|
||||
<div
|
||||
ref={logBox}
|
||||
className="text-white font-mono text-sm h-32 flex-none overflow-auto max-w-full break-all p-4"
|
||||
className="text-white font-mono text-sm h-64 flex-none overflow-auto max-w-full break-all p-4"
|
||||
>
|
||||
{log.map((l, i) => (
|
||||
<div
|
||||
key={l.index}
|
||||
className={cx(l.type === 'error' && 'text-red-500', l.type === 'highlight' && 'text-highlight')}
|
||||
>
|
||||
> {l.message}
|
||||
{l.count ? ` (${l.count})` : ''}
|
||||
</div>
|
||||
))}
|
||||
{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>
|
||||
</footer>
|
||||
</div>
|
||||
@ -365,59 +359,40 @@ function App() {
|
||||
|
||||
export default App;
|
||||
|
||||
function ActionButton({ children, onClick, className }) {
|
||||
return (
|
||||
<button
|
||||
className={cx(
|
||||
'bg-lineblack py-1 px-2 bottom-0 text-md whitespace-pre text-right pb-2 cursor-pointer flex items-center space-x-1 hover:text-primary',
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
function useEvent(name, onTrigger, useCapture = false) {
|
||||
useEffect(() => {
|
||||
document.addEventListener(name, onTrigger, useCapture);
|
||||
return () => {
|
||||
document.removeEventListener(name, onTrigger, useCapture);
|
||||
};
|
||||
}, [onTrigger]);
|
||||
}
|
||||
function FloatingBottomMenu() {
|
||||
{
|
||||
/* <span className="hidden md:block z-[20] bg-black py-1 px-2 text-sm absolute bottom-1 right-0 text-md whitespace-pre text-right pointer-events-none pb-2">
|
||||
{!started
|
||||
? `press ctrl+enter to play\n`
|
||||
: isDirty
|
||||
? `press ctrl+enter to update\n`
|
||||
: 'press ctrl+dot do stop\n'}
|
||||
</span>*/
|
||||
}
|
||||
return (
|
||||
<div className="flex justify-center w-full absolute bottom-0 z-[20]">
|
||||
<ActionButton
|
||||
onClick={async () => {
|
||||
await getAudioContext().resume(); // fixes no sound in ios webkit
|
||||
if (!started) {
|
||||
activateCode();
|
||||
} else {
|
||||
stop();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!pending ? (
|
||||
<span className={cx('flex items-center space-x-1 hover:text-primary', 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...</>
|
||||
)}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
onClick={() => {
|
||||
isDirty && activateCode();
|
||||
}}
|
||||
className={cx(!isDirty || !activeCode ? 'opacity-50 hover:text-inherit' : 'hover:text-primary')}
|
||||
>
|
||||
<CommandLineIcon className="w-5 h-5" />
|
||||
<span>update</span>
|
||||
</ActionButton>
|
||||
</div>
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user