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 && }