From 95a9ab6182f5d156eb14fa002ec0ef1bc962ccf1 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Thu, 2 Nov 2023 22:29:27 +0100 Subject: [PATCH] revert Repl.jsx --- website/src/repl/Repl.jsx | 275 +++++++++++++++++++++++++++++++++----- 1 file changed, 238 insertions(+), 37 deletions(-) diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index 49c0940cb..d9eb7001e 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -4,52 +4,244 @@ Copyright (C) 2022 Strudel contributors - see . */ -import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon'; -import { getDrawContext, logger, $replstate, $repldirty } from '@strudel.cycles/core'; -import { cx } from '@strudel.cycles/react'; -import { getAudioContext } from '@strudel.cycles/webaudio'; -import { writeText } from '@tauri-apps/api/clipboard'; +import { cleanupDraw, cleanupUi, controls, evalScope, getDrawContext, logger } from '@strudel.cycles/core'; +import { CodeMirror, cx, flash, useHighlighting, useStrudel, useKeydown } from '@strudel.cycles/react'; +import { getAudioContext, initAudioOnFirstClick, resetLoadedSounds, webaudioOutput } from '@strudel.cycles/webaudio'; +import { createClient } from '@supabase/supabase-js'; import { nanoid } from 'nanoid'; -import { createContext, useState } from 'react'; -import { useSettings } from '../settings.mjs'; -import { isTauri } from '../tauri.mjs'; +import React, { createContext, useCallback, useEffect, useState, useMemo } from 'react'; +import './Repl.css'; import { Footer } from './Footer'; import { Header } from './Header'; +import { prebake } from './prebake.mjs'; +import * as tunes from './tunes.mjs'; +import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon'; +import { themes } from './themes.mjs'; +import { settingsMap, useSettings, setLatestCode } from '../settings.mjs'; import Loader from './Loader'; -import './Repl.css'; -import { resetSounds } from './prebake.mjs'; -import { useStore } from '@nanostores/react'; +import { settingPatterns } from '../settings.mjs'; +import { code2hash, hash2code } from './helpers.mjs'; +import { isTauri } from '../tauri.mjs'; +import { useWidgets } from '@strudel.cycles/react/src/hooks/useWidgets.mjs'; +import { writeText } from '@tauri-apps/api/clipboard'; -let clearCanvas; +const { latestCode } = settingsMap.get(); + +initAudioOnFirstClick(); + +// Create a single supabase client for interacting with your database +const supabase = createClient( + 'https://pidxdsxphlhzjnzmifth.supabase.co', + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBpZHhkc3hwaGxoempuem1pZnRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTYyMzA1NTYsImV4cCI6MTk3MTgwNjU1Nn0.bqlw7802fsWRnqU5BLYtmXk_k-D1VFmbkHMywWc15NM', +); + +let modules = [ + import('@strudel.cycles/core'), + import('@strudel.cycles/tonal'), + import('@strudel.cycles/mini'), + import('@strudel.cycles/xen'), + import('@strudel.cycles/webaudio'), + import('@strudel/codemirror'), + import('@strudel/hydra'), + import('@strudel.cycles/serial'), + import('@strudel.cycles/soundfonts'), + import('@strudel.cycles/csound'), +]; +if (isTauri()) { + modules = modules.concat([ + import('@strudel/desktopbridge/loggerbridge.mjs'), + import('@strudel/desktopbridge/midibridge.mjs'), + import('@strudel/desktopbridge/oscbridge.mjs'), + ]); +} else { + modules = modules.concat([import('@strudel.cycles/midi'), import('@strudel.cycles/osc')]); +} + +const modulesLoading = evalScope( + controls, // sadly, this cannot be exported from core direclty + settingPatterns, + ...modules, +); + +const presets = prebake(); + +let drawContext, clearCanvas; if (typeof window !== 'undefined') { - const drawContext = getDrawContext(); + drawContext = getDrawContext(); clearCanvas = () => drawContext.clearRect(0, 0, drawContext.canvas.height, drawContext.canvas.width); } +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.cc/?J01s5i1J0200 (fixed hash length) + if (codeParam) { + // looking like https://strudel.cc/#ImMzIGUzIg%3D%3D (hash length depends on code length) + return hash2code(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(); + export const ReplContext = createContext(null); export function Repl({ embedded = false }) { - //const isEmbedded = embedded || window.location !== window.parent.location; - const isEmbedded = false; + const isEmbedded = embedded || window.location !== window.parent.location; + const [view, setView] = useState(); // codemirror view const [lastShared, setLastShared] = useState(); - const { panelPosition, isZen } = useSettings(); - const replState = useStore($replstate); - const isDirty = useStore($repldirty); + const [pending, setPending] = useState(true); + const { + theme, + keybindings, + fontSize, + fontFamily, + isLineNumbersDisplayed, + isAutoCompletionEnabled, + isLineWrappingEnabled, + panelPosition, + isZen, + } = useSettings(); + + const paintOptions = useMemo(() => ({ fontFamily }), [fontFamily]); + const { setWidgets } = useWidgets(view); + const { code, setCode, scheduler, evaluate, activateCode, isDirty, activeCode, pattern, started, stop, error } = + useStrudel({ + initialCode: '// LOADING...', + defaultOutput: webaudioOutput, + getTime, + beforeEval: async () => { + setPending(true); + await modulesLoading; + cleanupUi(); + cleanupDraw(); + }, + afterEval: ({ code, meta }) => { + setMiniLocations(meta.miniLocations); + setWidgets(meta.widgets); + setPending(false); + setLatestCode(code); + window.location.hash = '#' + code2hash(code); + }, + onEvalError: (err) => { + setPending(false); + }, + onToggle: (play) => { + if (!play) { + cleanupDraw(false); + window.postMessage('strudel-stop'); + } else { + window.postMessage('strudel-start'); + } + }, + drawContext, + // drawTime: [0, 6], + paintOptions, + }); + + // init code + useEffect(() => { + initCode().then((decoded) => { + let msg; + if (decoded) { + setCode(decoded); + msg = `I have loaded the code from the URL.`; + } else if (latestCode) { + setCode(latestCode); + msg = `Your last session has been loaded!`; + } /* if(randomTune) */ else { + setCode(randomTune); + msg = `A random code snippet named "${name}" has been loaded!`; + } + logger(`Welcome to Strudel! ${msg} Press play or hit ctrl+enter to run it!`, 'highlight'); + setPending(false); + }); + }, []); + + // keyboard shortcuts + useKeydown( + useCallback( + async (e) => { + if (e.ctrlKey || e.altKey) { + if (e.code === 'Enter') { + if (getAudioContext().state !== 'running') { + alert('please click play to initialize the audio. you can use shortcuts after that!'); + return; + } + e.preventDefault(); + flash(view); + await activateCode(); + } else if (e.key === '.' || e.code === 'Period') { + stop(); + e.preventDefault(); + } + } + }, + [activateCode, stop, view], + ), + ); + + // highlighting + const { setMiniLocations } = useHighlighting({ + view, + pattern, + active: started && !activeCode?.includes('strudel disable-highlighting'), + getTime: () => scheduler.now(), + }); // // UI Actions // + const handleChangeCode = useCallback( + (c) => { + setCode(c); + //started && logger('[edit] code changed. hit ctrl+enter to update'); + }, + [started], + ); + const handleSelectionChange = useCallback((selection) => { + // TODO: scroll to selected function in reference + // console.log('selectino change', selection.ranges[0].from); + }, []); + const handleTogglePlay = async () => { - window.postMessage('strudel-toggle-play'); - /* await getAudioContext().resume(); // fixes no sound in ios webkit + 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(); @@ -60,8 +252,9 @@ export function Repl({ embedded = false }) { const { code, name } = getRandomTune(); logger(`[repl] ✨ loading random tune "${name}"`); clearCanvas(); - await resetSounds(); - // scheduler.setCps(1); + resetLoadedSounds(); + scheduler.setCps(1); + await prebake(); // declare default samples await evaluate(code, false); }; @@ -94,24 +287,24 @@ export function Repl({ embedded = false }) { logger(message); } }; - const pending = false; - const error = undefined; - const { started, activeCode } = replState; - const context = { - // scheduler, + scheduler, embedded, started, pending, isDirty, lastShared, activeCode, - // handleChangeCode: codemirror.handleChangeCode, + handleChangeCode, handleTogglePlay, handleUpdate, handleShuffle, handleShare, }; + const currentTheme = useMemo(() => themes[theme] || themes.strudelTheme, [theme]); + const handleViewChanged = useCallback((v) => { + setView(v); + }, []); return ( // bg-gradient-to-t from-blue-900 to-slate-900 @@ -125,7 +318,7 @@ export function Repl({ embedded = false }) { >
- {/* isEmbedded && !started && ( + {isEmbedded && !started && ( - ) */} + )}
-
window.postMessage('strudel-container')} - > - {/* */} +
+
{panelPosition === 'right' && !isEmbedded &&