/* App.js - Copyright (C) 2022 Strudel contributors - see 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 . */ // 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, useCallback } from 'react'; import './App.css'; import logo from './logo.svg'; import * as tunes from './tunes.mjs'; import { prebake } from './prebake.mjs'; import * as WebDirt from 'WebDirt'; import { resetLoadedSamples, getAudioContext } from '@strudel.cycles/webaudio'; import { controls, evalScope, logger } from '@strudel.cycles/core'; 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 AcademicCapIcon from '@heroicons/react/20/solid/AcademicCapIcon'; initAudioOnFirstClick(); // Create a single supabase client for interacting with your database const supabase = createClient( 'https://pidxdsxphlhzjnzmifth.supabase.co', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBpZHhkc3hwaGxoempuem1pZnRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTYyMzA1NTYsImV4cCI6MTk3MTgwNjU1Nn0.bqlw7802fsWRnqU5BLYtmXk_k-D1VFmbkHMywWc15NM', ); evalScope( // Tone, controls, // sadly, this cannot be exported from core direclty { WebDirt }, import('@strudel.cycles/core'), // import('@strudel.cycles/tone'), import('@strudel.cycles/tonal'), import('@strudel.cycles/mini'), import('@strudel.cycles/midi'), import('@strudel.cycles/xen'), import('@strudel.cycles/webaudio'), import('@strudel.cycles/osc'), import('@strudel.cycles/serial'), import('@strudel.cycles/soundfonts'), ); prebake(); const hideHeader = false; const pending = false; const getTime = () => getAudioContext().currentTime; async function initCode() { // load code from url hash (either short hash from database or decode long hash) try { const initialUrl = window.location.href; const hash = initialUrl.split('?')[1]?.split('#')?.[0]; const codeParam = window.location.href.split('#')[1]; // looking like https://strudel.tidalcycles.org/?J01s5i1J0200 (fixed hash length) if (codeParam) { // looking like https://strudel.tidalcycles.org/#ImMzIGUzIg%3D%3D (hash length depends on code length) return atob(decodeURIComponent(codeParam || '')); } else if (hash) { return supabase .from('code') .select('code') .eq('hash', hash) .then(({ data, error }) => { if (error) { console.warn('failed to load hash', err); } if (data.length) { console.log('load hash from database', hash); return data[0].code; } }); } } catch (err) { console.warn('failed to decode', err); } } function getRandomTune() { const allTunes = Object.entries(tunes); const randomItem = (arr) => arr[Math.floor(Math.random() * arr.length)]; const [name, code] = randomItem(allTunes); return { name, code }; } const { code: randomTune, name } = getRandomTune(); const isEmbedded = window.location !== window.parent.location; function App() { const [view, setView] = useState(); // codemirror view const [lastShared, setLastShared] = useState(); // 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 logBox = useRef(); useLayoutEffect(() => { if (logBox.current) { // scroll log box to bottom when log changes logBox.current.scrollTop = logBox.current?.scrollHeight; } }, [log]); const { code, setCode, scheduler, evaluate, activateCode, error, isDirty, activeCode, pattern, started, stop } = useStrudel({ initialCode: '// LOADING', defaultOutput: webaudioOutput, getTime, autolink: true, }); // init code useEffect(() => { initCode().then((decoded) => { 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!`, 'highlight', ); setCode(decoded || randomTune); }); }, []); 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(); } } }, [activateCode, stop, view], ), ); useHighlighting({ view, pattern, active: started && !activeCode?.includes('strudel disable-highlighting'), getTime: () => scheduler.getPhase(), }); // // UI Actions // const handleTogglePlay = async () => { await getAudioContext().resume(); // fixes no sound in ios webkit if (!started) { logger('[repl] started. tip: you can also start by pressing ctrl+enter', 'highlight'); activateCode(); } else { logger('[repl] stopped. tip: you can also stop by pressing ctrl+dot', 'highlight'); stop(); } }; const handleUpdate = () => { isDirty && activateCode(); logger('[repl] code updated! tip: you can also update the code by pressing ctrl+enter', 'highlight'); }; const handleShuffle = async () => { const { code, name } = getRandomTune(); logger(`[repl] ✨ 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
{!hideHeader && ( )}
{log.map((l, i) => { const message = linkify(l.message); return (
> {l.count ? ` (${l.count})` : ''}
); })}
); } export default App; function useEvent(name, onTrigger, useCapture = false) { useEffect(() => { document.addEventListener(name, onTrigger, useCapture); return () => { document.removeEventListener(name, onTrigger, useCapture); }; }, [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; }