mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-11 13:48:34 +00:00
Merge pull request #897 from daslyfe/pattern_selection
Add useful pattern selection behavior for performing.
This commit is contained in:
commit
2cc428e968
@ -145,7 +145,6 @@ export function repl({
|
||||
afterEval?.({ code, pattern, meta });
|
||||
return pattern;
|
||||
} catch (err) {
|
||||
// console.warn(`[repl] eval error: ${err.message}`);
|
||||
logger(`[eval] error: ${err.message}`, 'error');
|
||||
updateState({ evalError: err, pending: false });
|
||||
onEvalError?.(err);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { loadFeaturedPatterns, loadPublicPatterns } from '@src/repl/util.mjs';
|
||||
import { loadFeaturedPatterns, loadPublicPatterns } from '@src/user_pattern_utils.mjs';
|
||||
import { MiniRepl } from '@src/docs/MiniRepl';
|
||||
import { PatternLabel } from '@src/repl/panel/PatternsTab';
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ export function Header({ context }) {
|
||||
isDirty,
|
||||
activeCode,
|
||||
handleTogglePlay,
|
||||
handleUpdate,
|
||||
handleEvaluate,
|
||||
handleShuffle,
|
||||
handleShare,
|
||||
} = context;
|
||||
@ -85,7 +85,7 @@ export function Header({ context }) {
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleUpdate()}
|
||||
onClick={handleEvaluate}
|
||||
title="update"
|
||||
className={cx(
|
||||
'flex items-center space-x-1',
|
||||
|
||||
@ -12,14 +12,15 @@ import { defaultAudioDeviceName } from '../settings.mjs';
|
||||
import { getAudioDevices, setAudioDevice } from './util.mjs';
|
||||
import { StrudelMirror, defaultSettings } from '@strudel/codemirror';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { settingsMap, useSettings } from '../settings.mjs';
|
||||
import {
|
||||
initUserCode,
|
||||
setActivePattern,
|
||||
setLatestCode,
|
||||
settingsMap,
|
||||
updateUserCode,
|
||||
useSettings,
|
||||
} from '../settings.mjs';
|
||||
createPatternID,
|
||||
userPattern,
|
||||
getViewingPatternData,
|
||||
setViewingPatternData,
|
||||
} from '../user_pattern_utils.mjs';
|
||||
import { Header } from './Header';
|
||||
import Loader from './Loader';
|
||||
import { Panel } from './panel/Panel';
|
||||
@ -30,7 +31,6 @@ import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
|
||||
import './Repl.css';
|
||||
|
||||
const { code: randomTune, name } = getRandomTune();
|
||||
|
||||
const { latestCode } = settingsMap.get();
|
||||
|
||||
let modulesLoading, presets, drawContext, clearCanvas, isIframe;
|
||||
@ -46,7 +46,6 @@ if (typeof window !== 'undefined') {
|
||||
export function Repl({ embedded = false }) {
|
||||
const isEmbedded = embedded || isIframe;
|
||||
const { panelPosition, isZen } = useSettings();
|
||||
|
||||
const init = useCallback(() => {
|
||||
const drawTime = [-2, 2];
|
||||
const drawContext = getDrawContext();
|
||||
@ -71,11 +70,28 @@ export function Repl({ embedded = false }) {
|
||||
onUpdateState: (state) => {
|
||||
setReplState({ ...state });
|
||||
},
|
||||
afterEval: ({ code }) => {
|
||||
updateUserCode(code);
|
||||
// setPending(false);
|
||||
afterEval: (all) => {
|
||||
const { code } = all;
|
||||
setLatestCode(code);
|
||||
window.location.hash = '#' + code2hash(code);
|
||||
const viewingPatternData = getViewingPatternData();
|
||||
const data = { ...viewingPatternData, code };
|
||||
let id = data.id;
|
||||
const isExamplePattern = viewingPatternData.collection !== userPattern.collection;
|
||||
|
||||
if (isExamplePattern) {
|
||||
const codeHasChanged = code !== viewingPatternData.code;
|
||||
if (codeHasChanged) {
|
||||
// fork example
|
||||
const newPattern = userPattern.duplicate(data);
|
||||
id = newPattern.id;
|
||||
setViewingPatternData(newPattern.data);
|
||||
}
|
||||
} else {
|
||||
id = userPattern.isValidID(id) ? id : createPatternID();
|
||||
setViewingPatternData(userPattern.update(id, data).data);
|
||||
}
|
||||
setActivePattern(id);
|
||||
},
|
||||
bgFill: false,
|
||||
});
|
||||
@ -86,7 +102,6 @@ export function Repl({ embedded = false }) {
|
||||
let msg;
|
||||
if (decoded) {
|
||||
editor.setCode(decoded);
|
||||
initUserCode(decoded);
|
||||
msg = `I have loaded the code from the URL.`;
|
||||
} else if (latestCode) {
|
||||
editor.setCode(latestCode);
|
||||
@ -96,7 +111,6 @@ export function Repl({ embedded = false }) {
|
||||
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);
|
||||
});
|
||||
|
||||
editorRef.current = editor;
|
||||
@ -138,30 +152,38 @@ export function Repl({ embedded = false }) {
|
||||
// UI Actions
|
||||
//
|
||||
|
||||
const handleTogglePlay = async () => editorRef.current?.toggle();
|
||||
const handleUpdate = async (newCode, reset = false) => {
|
||||
if (reset) {
|
||||
clearCanvas();
|
||||
resetLoadedSounds();
|
||||
editorRef.current.repl.setCps(1);
|
||||
await prebake(); // declare default samples
|
||||
}
|
||||
if (newCode) {
|
||||
editorRef.current.setCode(newCode);
|
||||
editorRef.current.repl.evaluate(newCode);
|
||||
} else if (isDirty) {
|
||||
editorRef.current.evaluate();
|
||||
}
|
||||
const handleTogglePlay = async () => {
|
||||
editorRef.current?.toggle();
|
||||
};
|
||||
const handleShuffle = async () => {
|
||||
// window.postMessage('strudel-shuffle');
|
||||
const { code, name } = getRandomTune();
|
||||
logger(`[repl] ✨ loading random tune "${name}"`);
|
||||
setActivePattern(name);
|
||||
|
||||
const resetEditor = async () => {
|
||||
clearCanvas();
|
||||
resetLoadedSounds();
|
||||
editorRef.current.repl.setCps(1);
|
||||
await prebake(); // declare default samples
|
||||
};
|
||||
|
||||
const handleUpdate = async (patternData, reset = false) => {
|
||||
setViewingPatternData(patternData);
|
||||
editorRef.current.setCode(patternData.code);
|
||||
if (reset) {
|
||||
await resetEditor();
|
||||
handleEvaluate();
|
||||
}
|
||||
};
|
||||
|
||||
const handleEvaluate = () => {
|
||||
editorRef.current.evaluate();
|
||||
};
|
||||
const handleShuffle = async () => {
|
||||
const patternData = getRandomTune();
|
||||
const code = patternData.code;
|
||||
logger(`[repl] ✨ loading random tune "${patternData.id}"`);
|
||||
setActivePattern(patternData.id);
|
||||
setViewingPatternData(patternData);
|
||||
clearCanvas();
|
||||
resetLoadedSounds();
|
||||
await prebake(); // declare default samples
|
||||
editorRef.current.setCode(code);
|
||||
editorRef.current.repl.evaluate(code);
|
||||
};
|
||||
@ -177,6 +199,7 @@ export function Repl({ embedded = false }) {
|
||||
handleUpdate,
|
||||
handleShuffle,
|
||||
handleShare,
|
||||
handleEvaluate,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -1,178 +1,190 @@
|
||||
import { DocumentDuplicateIcon, PencilSquareIcon, TrashIcon } from '@heroicons/react/20/solid';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
$featuredPatterns,
|
||||
$publicPatterns,
|
||||
clearUserPatterns,
|
||||
deleteActivePattern,
|
||||
duplicateActivePattern,
|
||||
exportPatterns,
|
||||
getUserPattern,
|
||||
importPatterns,
|
||||
newUserPattern,
|
||||
renameActivePattern,
|
||||
setActivePattern,
|
||||
patternFilterName,
|
||||
useActivePattern,
|
||||
useSettings,
|
||||
} from '../../settings.mjs';
|
||||
import * as tunes from '../tunes.mjs';
|
||||
import { useStore } from '@nanostores/react';
|
||||
useViewingPatternData,
|
||||
userPattern,
|
||||
} from '../../user_pattern_utils.mjs';
|
||||
import { useMemo } from 'react';
|
||||
import { getMetadata } from '../../metadata_parser';
|
||||
import { useExamplePatterns } from '../useExamplePatterns';
|
||||
import { parseJSON } from '../util.mjs';
|
||||
import { ButtonGroup } from './Forms.jsx';
|
||||
import { settingsMap, useSettings } from '../../settings.mjs';
|
||||
|
||||
function classNames(...classes) {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
export function PatternsTab({ context }) {
|
||||
const { userPatterns } = useSettings();
|
||||
const activePattern = useActivePattern();
|
||||
const featuredPatterns = useStore($featuredPatterns);
|
||||
const publicPatterns = useStore($publicPatterns);
|
||||
const isExample = useMemo(() => activePattern && !!tunes[activePattern], [activePattern]);
|
||||
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
|
||||
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
|
||||
let title = meta.title;
|
||||
if (title == null) {
|
||||
const date = new Date(pattern.created_at);
|
||||
if (isNaN(date)) {
|
||||
return;
|
||||
}
|
||||
title = date.toLocaleDateString();
|
||||
}
|
||||
if (title == null) {
|
||||
title = pattern.hash;
|
||||
}
|
||||
if (title == null) {
|
||||
title = 'unnamed';
|
||||
}
|
||||
|
||||
return <>{`${pattern.id}: ${title} by ${Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous'}`}</>;
|
||||
}
|
||||
|
||||
function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
|
||||
return (
|
||||
<div className="px-4 w-full dark:text-white text-stone-900 space-y-4 pb-4">
|
||||
<section>
|
||||
{activePattern && (
|
||||
<div className="flex items-center mb-2 space-x-2 overflow-auto">
|
||||
<h1 className="text-xl">{activePattern}</h1>
|
||||
<div className="space-x-4 flex w-min">
|
||||
{!isExample && (
|
||||
<button className="hover:opacity-50" onClick={() => renameActivePattern()} title="Rename">
|
||||
<PencilSquareIcon className="w-5 h-5" />
|
||||
{/* <PencilIcon className="w-5 h-5" /> */}
|
||||
</button>
|
||||
)}
|
||||
<button className="hover:opacity-50" onClick={() => duplicateActivePattern()} title="Duplicate">
|
||||
<DocumentDuplicateIcon className="w-5 h-5" />
|
||||
</button>
|
||||
{!isExample && (
|
||||
<button className="hover:opacity-50" onClick={() => deleteActivePattern()} title="Delete">
|
||||
<TrashIcon className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="font-mono text-sm">
|
||||
{Object.entries(userPatterns).map(([key, up]) => (
|
||||
<a
|
||||
key={key}
|
||||
className={classNames(
|
||||
'mr-4 hover:opacity-50 cursor-pointer inline-block',
|
||||
key === activePattern ? 'outline outline-1' : '',
|
||||
)}
|
||||
onClick={() => {
|
||||
const { code } = up;
|
||||
setActivePattern(key);
|
||||
context.handleUpdate(code, true);
|
||||
}}
|
||||
>
|
||||
{key}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<div className="pr-4 space-x-4 border-b border-foreground mb-2 h-8 flex overflow-auto max-w-full items-center">
|
||||
<button
|
||||
className="hover:opacity-50"
|
||||
onClick={() => {
|
||||
const name = newUserPattern();
|
||||
const { code } = getUserPattern(name);
|
||||
context.handleUpdate(code, true);
|
||||
}}
|
||||
>
|
||||
new
|
||||
</button>
|
||||
<button className="hover:opacity-50" onClick={() => clearUserPatterns()}>
|
||||
clear
|
||||
</button>
|
||||
<label className="hover:opacity-50 cursor-pointer">
|
||||
<input
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
multiple
|
||||
accept="text/plain,application/json"
|
||||
onChange={(e) => importPatterns(e.target.files)}
|
||||
<a
|
||||
className={classNames(
|
||||
'mr-4 hover:opacity-50 cursor-pointer block',
|
||||
showOutline && 'outline outline-1',
|
||||
showHiglight && 'bg-selection',
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<PatternLabel pattern={pattern} />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function PatternButtons({ patterns, activePattern, onClick, started }) {
|
||||
const viewingPatternStore = useViewingPatternData();
|
||||
const viewingPatternData = parseJSON(viewingPatternStore);
|
||||
const viewingPatternID = viewingPatternData.id;
|
||||
return (
|
||||
<div className="font-mono text-sm">
|
||||
{Object.values(patterns)
|
||||
.reverse()
|
||||
.map((pattern) => {
|
||||
const id = pattern.id;
|
||||
return (
|
||||
<PatternButton
|
||||
pattern={pattern}
|
||||
key={id}
|
||||
showHiglight={id === viewingPatternID}
|
||||
showOutline={id === activePattern && started}
|
||||
onClick={() => onClick(id)}
|
||||
/>
|
||||
import
|
||||
</label>
|
||||
<button className="hover:opacity-50" onClick={() => exportPatterns()}>
|
||||
export
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
{featuredPatterns && (
|
||||
<section>
|
||||
<h2 className="text-xl mb-2">Featured Patterns</h2>
|
||||
<div className="font-mono text-sm">
|
||||
{featuredPatterns.map((pattern) => (
|
||||
<a
|
||||
key={pattern.id}
|
||||
className={classNames(
|
||||
'mr-4 hover:opacity-50 cursor-pointer block',
|
||||
pattern.hash === activePattern ? 'outline outline-1' : '',
|
||||
)}
|
||||
onClick={() => {
|
||||
setActivePattern(pattern.hash);
|
||||
context.handleUpdate(pattern.code, true);
|
||||
}}
|
||||
>
|
||||
<PatternLabel pattern={pattern} />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
{publicPatterns && (
|
||||
<section>
|
||||
<h2 className="text-xl mb-2">Last Creations</h2>
|
||||
<div className="font-mono text-sm">
|
||||
{publicPatterns.map((pattern) => (
|
||||
<a
|
||||
key={'public-' + pattern.id}
|
||||
className={classNames(
|
||||
'mr-4 hover:opacity-50 cursor-pointer block', // inline-block
|
||||
pattern.hash === activePattern ? 'outline outline-1' : '',
|
||||
)}
|
||||
onClick={() => {
|
||||
setActivePattern(pattern.hash);
|
||||
context.handleUpdate(pattern.code, true);
|
||||
}}
|
||||
>
|
||||
<PatternLabel pattern={pattern} />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
<section>
|
||||
<h2 className="text-xl mb-2">Stock Examples</h2>
|
||||
<div className="font-mono text-sm">
|
||||
{Object.entries(tunes).map(([key, tune]) => (
|
||||
<a
|
||||
key={key}
|
||||
className={classNames(
|
||||
'mr-4 hover:opacity-50 cursor-pointer inline-block',
|
||||
key === activePattern ? 'outline outline-1' : '',
|
||||
)}
|
||||
onClick={() => {
|
||||
setActivePattern(key);
|
||||
context.handleUpdate(tune, true);
|
||||
}}
|
||||
>
|
||||
{key}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
|
||||
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
|
||||
function ActionButton({ children, onClick, label, labelIsHidden }) {
|
||||
return (
|
||||
<>
|
||||
{pattern.id}. {meta.title || pattern.hash} by {Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous'}
|
||||
</>
|
||||
<button className="hover:opacity-50" onClick={onClick} title={label}>
|
||||
{labelIsHidden !== true && label}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function PatternsTab({ context }) {
|
||||
const activePattern = useActivePattern();
|
||||
const viewingPatternStore = useViewingPatternData();
|
||||
const viewingPatternData = parseJSON(viewingPatternStore);
|
||||
|
||||
const { userPatterns, patternFilter } = useSettings();
|
||||
const examplePatterns = useExamplePatterns();
|
||||
const collections = examplePatterns.collections;
|
||||
|
||||
const updateCodeWindow = (patternData, reset = false) => {
|
||||
context.handleUpdate(patternData, reset);
|
||||
};
|
||||
const viewingPatternID = viewingPatternData?.id;
|
||||
|
||||
const autoResetPatternOnChange = !window.parent?.location.pathname.includes('oodles');
|
||||
|
||||
return (
|
||||
<div className="px-4 w-full dark:text-white text-stone-900 space-y-2 pb-4 flex flex-col overflow-hidden max-h-full">
|
||||
<ButtonGroup
|
||||
value={patternFilter}
|
||||
onChange={(value) => settingsMap.setKey('patternFilter', value)}
|
||||
items={patternFilterName}
|
||||
></ButtonGroup>
|
||||
{patternFilter === patternFilterName.user && (
|
||||
<div>
|
||||
<div className="pr-4 space-x-4 border-b border-foreground flex max-w-full overflow-x-auto">
|
||||
<ActionButton
|
||||
label="new"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.createAndAddToDB();
|
||||
updateCodeWindow(data);
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
label="duplicate"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.duplicate(viewingPatternData);
|
||||
updateCodeWindow(data);
|
||||
}}
|
||||
/>
|
||||
<ActionButton
|
||||
label="delete"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.delete(viewingPatternID);
|
||||
updateCodeWindow({ ...data, collection: userPattern.collection });
|
||||
}}
|
||||
/>
|
||||
<label className="hover:opacity-50 cursor-pointer">
|
||||
<input
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
multiple
|
||||
accept="text/plain,application/json"
|
||||
onChange={(e) => importPatterns(e.target.files)}
|
||||
/>
|
||||
import
|
||||
</label>
|
||||
<ActionButton label="export" onClick={exportPatterns} />
|
||||
|
||||
<ActionButton
|
||||
label="delete-all"
|
||||
onClick={() => {
|
||||
const { data } = userPattern.clearAll();
|
||||
updateCodeWindow(data);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<section className="flex overflow-y-scroll max-h-full flex-col">
|
||||
{patternFilter === patternFilterName.user && (
|
||||
<PatternButtons
|
||||
onClick={(id) =>
|
||||
updateCodeWindow({ ...userPatterns[id], collection: userPattern.collection }, autoResetPatternOnChange)
|
||||
}
|
||||
patterns={userPatterns}
|
||||
started={context.started}
|
||||
activePattern={activePattern}
|
||||
viewingPatternID={viewingPatternID}
|
||||
/>
|
||||
)}
|
||||
{patternFilter !== patternFilterName.user &&
|
||||
Array.from(collections.keys()).map((collection) => {
|
||||
const patterns = collections.get(collection);
|
||||
return (
|
||||
<section key={collection} className="py-2">
|
||||
<h2 className="text-xl mb-2">{collection}</h2>
|
||||
<div className="font-mono text-sm">
|
||||
<PatternButtons
|
||||
onClick={(id) => updateCodeWindow({ ...patterns[id], collection }, autoResetPatternOnChange)}
|
||||
started={context.started}
|
||||
patterns={patterns}
|
||||
activePattern={activePattern}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
26
website/src/repl/useExamplePatterns.jsx
Normal file
26
website/src/repl/useExamplePatterns.jsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { $featuredPatterns, $publicPatterns, collectionName } from '../user_pattern_utils.mjs';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useMemo } from 'react';
|
||||
import * as tunes from '../repl/tunes.mjs';
|
||||
|
||||
export const stockPatterns = Object.fromEntries(
|
||||
Object.entries(tunes).map(([key, code], i) => [i, { id: i, code, collection: 'Stock Examples' }]),
|
||||
);
|
||||
|
||||
export const useExamplePatterns = () => {
|
||||
const featuredPatterns = useStore($featuredPatterns);
|
||||
const publicPatterns = useStore($publicPatterns);
|
||||
const collections = useMemo(() => {
|
||||
const pats = new Map();
|
||||
pats.set(collectionName.featured, featuredPatterns);
|
||||
pats.set(collectionName.public, publicPatterns);
|
||||
// pats.set(collectionName.stock, stockPatterns);
|
||||
return pats;
|
||||
}, [featuredPatterns, publicPatterns]);
|
||||
|
||||
const patterns = useMemo(() => {
|
||||
return Object.assign({}, ...collections.values());
|
||||
}, [collections]);
|
||||
|
||||
return { patterns, collections };
|
||||
};
|
||||
@ -4,12 +4,13 @@ import { getAudioContext, initializeAudioOutput, setDefaultAudioContext } from '
|
||||
|
||||
import { isTauri } from '../tauri.mjs';
|
||||
import './Repl.css';
|
||||
import * as tunes from './tunes.mjs';
|
||||
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { writeText } from '@tauri-apps/api/clipboard';
|
||||
import { createContext } from 'react';
|
||||
import { $publicPatterns, $featuredPatterns } from '../settings.mjs';
|
||||
import { stockPatterns } from './useExamplePatterns';
|
||||
import { loadDBPatterns } from '@src/user_pattern_utils.mjs';
|
||||
|
||||
// Create a single supabase client for interacting with your database
|
||||
export const supabase = createClient(
|
||||
@ -17,25 +18,6 @@ export const supabase = createClient(
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBpZHhkc3hwaGxoempuem1pZnRoIiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTYyMzA1NTYsImV4cCI6MTk3MTgwNjU1Nn0.bqlw7802fsWRnqU5BLYtmXk_k-D1VFmbkHMywWc15NM',
|
||||
);
|
||||
|
||||
export function loadPublicPatterns() {
|
||||
return supabase.from('code').select().eq('public', true).limit(20).order('id', { ascending: false });
|
||||
}
|
||||
|
||||
export function loadFeaturedPatterns() {
|
||||
return supabase.from('code').select().eq('featured', true).limit(20).order('id', { ascending: false });
|
||||
}
|
||||
|
||||
async function loadDBPatterns() {
|
||||
try {
|
||||
const { data: publicPatterns } = await loadPublicPatterns();
|
||||
const { data: featuredPatterns } = await loadFeaturedPatterns();
|
||||
$publicPatterns.set(publicPatterns);
|
||||
$featuredPatterns.set(featuredPatterns);
|
||||
} catch (err) {
|
||||
console.error('error loading patterns');
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
loadDBPatterns();
|
||||
}
|
||||
@ -70,11 +52,20 @@ export async function initCode() {
|
||||
}
|
||||
}
|
||||
|
||||
export const parseJSON = (json) => {
|
||||
json = json != null && json.length ? json : '{}';
|
||||
try {
|
||||
return JSON.parse(json);
|
||||
} catch {
|
||||
return '{}';
|
||||
}
|
||||
};
|
||||
|
||||
export function getRandomTune() {
|
||||
const allTunes = Object.entries(tunes);
|
||||
const allTunes = Object.entries(stockPatterns);
|
||||
const randomItem = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
||||
const [name, code] = randomItem(allTunes);
|
||||
return { name, code };
|
||||
const [id, data] = randomItem(allTunes);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function loadModules() {
|
||||
|
||||
@ -1,12 +1,6 @@
|
||||
import { atom } from 'nanostores';
|
||||
import { persistentMap, persistentAtom } from '@nanostores/persistent';
|
||||
import { persistentMap } from '@nanostores/persistent';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { register } from '@strudel/core';
|
||||
import * as tunes from './repl/tunes.mjs';
|
||||
import { logger } from '@strudel/core';
|
||||
|
||||
export let $publicPatterns = atom([]);
|
||||
export let $featuredPatterns = atom([]);
|
||||
|
||||
export const defaultAudioDeviceName = 'System Standard';
|
||||
|
||||
@ -26,6 +20,7 @@ export const defaultSettings = {
|
||||
latestCode: '',
|
||||
isZen: false,
|
||||
soundsFilter: 'all',
|
||||
patternFilter: 'community',
|
||||
panelPosition: 'right',
|
||||
userPatterns: '{}',
|
||||
audioDeviceName: defaultAudioDeviceName,
|
||||
@ -33,26 +28,15 @@ export const defaultSettings = {
|
||||
|
||||
export const settingsMap = persistentMap('strudel-settings', defaultSettings);
|
||||
|
||||
// active pattern is separate, because it shouldn't sync state across tabs
|
||||
// reason: https://github.com/tidalcycles/strudel/issues/857
|
||||
const $activePattern = persistentAtom('activePattern', '', { listen: false });
|
||||
export function setActivePattern(key) {
|
||||
$activePattern.set(key);
|
||||
}
|
||||
export function getActivePattern() {
|
||||
return $activePattern.get();
|
||||
}
|
||||
export function useActivePattern() {
|
||||
return useStore($activePattern);
|
||||
}
|
||||
export function initUserCode(code) {
|
||||
const userPatterns = getUserPatterns();
|
||||
const match = Object.entries(userPatterns).find(([_, pat]) => pat.code === code);
|
||||
setActivePattern(match?.[0] || '');
|
||||
}
|
||||
|
||||
export function useSettings() {
|
||||
const state = useStore(settingsMap);
|
||||
|
||||
const userPatterns = JSON.parse(state.userPatterns);
|
||||
Object.keys(userPatterns).forEach((key) => {
|
||||
const data = userPatterns[key];
|
||||
data.id = data.id ?? key;
|
||||
userPatterns[key] = data;
|
||||
});
|
||||
return {
|
||||
...state,
|
||||
isZen: [true, 'true'].includes(state.isZen) ? true : false,
|
||||
@ -65,13 +49,12 @@ export function useSettings() {
|
||||
isFlashEnabled: [true, 'true'].includes(state.isFlashEnabled) ? true : false,
|
||||
fontSize: Number(state.fontSize),
|
||||
panelPosition: state.activeFooter !== '' ? state.panelPosition : 'bottom', // <-- keep this 'bottom' where it is!
|
||||
userPatterns: JSON.parse(state.userPatterns),
|
||||
userPatterns: userPatterns,
|
||||
};
|
||||
}
|
||||
|
||||
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
|
||||
|
||||
export const setLatestCode = (code) => settingsMap.setKey('latestCode', code);
|
||||
export const setIsZen = (active) => settingsMap.setKey('isZen', !!active);
|
||||
|
||||
const patternSetting = (key) =>
|
||||
@ -90,174 +73,3 @@ export const fontFamily = patternSetting('fontFamily');
|
||||
export const fontSize = patternSetting('fontSize');
|
||||
|
||||
export const settingPatterns = { theme, fontFamily, fontSize };
|
||||
|
||||
export function getUserPatterns() {
|
||||
return JSON.parse(settingsMap.get().userPatterns);
|
||||
}
|
||||
function getSetting(key) {
|
||||
return settingsMap.get()[key];
|
||||
}
|
||||
|
||||
export function setUserPatterns(obj) {
|
||||
settingsMap.setKey('userPatterns', JSON.stringify(obj));
|
||||
}
|
||||
|
||||
export function addUserPattern(name, config) {
|
||||
if (typeof config !== 'object') {
|
||||
throw new Error('addUserPattern expected object as second param');
|
||||
}
|
||||
if (!config.code) {
|
||||
throw new Error('addUserPattern expected code as property of second param');
|
||||
}
|
||||
const userPatterns = getUserPatterns();
|
||||
setUserPatterns({ ...userPatterns, [name]: config });
|
||||
}
|
||||
|
||||
export function newUserPattern() {
|
||||
const userPatterns = getUserPatterns();
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
const todays = Object.entries(userPatterns).filter(([name]) => name.startsWith(date));
|
||||
const num = String(todays.length + 1).padStart(3, '0');
|
||||
const defaultNewPattern = 's("hh")';
|
||||
const name = date + '_' + num;
|
||||
addUserPattern(name, { code: defaultNewPattern });
|
||||
setActivePattern(name);
|
||||
return name;
|
||||
}
|
||||
|
||||
export function clearUserPatterns() {
|
||||
if (!confirm(`This will delete all your patterns. Are you really sure?`)) {
|
||||
return;
|
||||
}
|
||||
setUserPatterns({});
|
||||
}
|
||||
|
||||
export function getNextCloneName(key) {
|
||||
const userPatterns = getUserPatterns();
|
||||
const clones = Object.entries(userPatterns).filter(([name]) => name.startsWith(key));
|
||||
const num = String(clones.length + 1).padStart(3, '0');
|
||||
return key + '_' + num;
|
||||
}
|
||||
|
||||
export function getUserPattern(key) {
|
||||
const userPatterns = getUserPatterns();
|
||||
return userPatterns[key];
|
||||
}
|
||||
|
||||
export function renameActivePattern() {
|
||||
let activePattern = getActivePattern();
|
||||
let userPatterns = getUserPatterns();
|
||||
if (!userPatterns[activePattern]) {
|
||||
alert('Cannot rename examples');
|
||||
return;
|
||||
}
|
||||
const newName = prompt('Enter new name', activePattern);
|
||||
if (newName === null) {
|
||||
// canceled
|
||||
return;
|
||||
}
|
||||
if (userPatterns[newName]) {
|
||||
alert('Name already taken!');
|
||||
return;
|
||||
}
|
||||
userPatterns[newName] = userPatterns[activePattern]; // copy code
|
||||
delete userPatterns[activePattern];
|
||||
setUserPatterns({ ...userPatterns });
|
||||
setActivePattern(newName);
|
||||
}
|
||||
|
||||
export function updateUserCode(code) {
|
||||
const userPatterns = getUserPatterns();
|
||||
let activePattern = getActivePattern();
|
||||
// check if code is that of an example tune
|
||||
const [example] = Object.entries(tunes).find(([_, tune]) => tune === code) || [];
|
||||
if (example && (!activePattern || activePattern === example)) {
|
||||
// select example
|
||||
setActivePattern(example);
|
||||
return;
|
||||
}
|
||||
const publicPattern = $publicPatterns.get().find((pat) => pat.code === code);
|
||||
if (publicPattern) {
|
||||
setActivePattern(publicPattern.hash);
|
||||
return;
|
||||
}
|
||||
const featuredPattern = $featuredPatterns.get().find((pat) => pat.code === code);
|
||||
if (featuredPattern) {
|
||||
setActivePattern(featuredPattern.hash);
|
||||
return;
|
||||
}
|
||||
if (!activePattern) {
|
||||
// create new user pattern
|
||||
activePattern = newUserPattern();
|
||||
setActivePattern(activePattern);
|
||||
} else if (
|
||||
(!!tunes[activePattern] && code !== tunes[activePattern]) || // fork example tune?
|
||||
$publicPatterns.get().find((p) => p.hash === activePattern) || // fork public pattern?
|
||||
$featuredPatterns.get().find((p) => p.hash === activePattern) // fork featured pattern?
|
||||
) {
|
||||
// fork example
|
||||
activePattern = getNextCloneName(activePattern);
|
||||
setActivePattern(activePattern);
|
||||
}
|
||||
setUserPatterns({ ...userPatterns, [activePattern]: { code } });
|
||||
}
|
||||
|
||||
export function deleteActivePattern() {
|
||||
let activePattern = getActivePattern();
|
||||
if (!activePattern) {
|
||||
console.warn('cannot delete: no pattern selected');
|
||||
return;
|
||||
}
|
||||
const userPatterns = getUserPatterns();
|
||||
if (!userPatterns[activePattern]) {
|
||||
alert('Cannot delete examples');
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Really delete the selected pattern "${activePattern}"?`)) {
|
||||
return;
|
||||
}
|
||||
setUserPatterns(Object.fromEntries(Object.entries(userPatterns).filter(([key]) => key !== activePattern)));
|
||||
setActivePattern('');
|
||||
}
|
||||
|
||||
export function duplicateActivePattern() {
|
||||
let activePattern = getActivePattern();
|
||||
let latestCode = getSetting('latestCode');
|
||||
if (!activePattern) {
|
||||
console.warn('cannot duplicate: no pattern selected');
|
||||
return;
|
||||
}
|
||||
const userPatterns = getUserPatterns();
|
||||
activePattern = getNextCloneName(activePattern);
|
||||
setUserPatterns({ ...userPatterns, [activePattern]: { code: latestCode } });
|
||||
setActivePattern(activePattern);
|
||||
}
|
||||
|
||||
export async function importPatterns(fileList) {
|
||||
const files = Array.from(fileList);
|
||||
await Promise.all(
|
||||
files.map(async (file, i) => {
|
||||
const content = await file.text();
|
||||
if (file.type === 'application/json') {
|
||||
const userPatterns = getUserPatterns() || {};
|
||||
setUserPatterns({ ...userPatterns, ...JSON.parse(content) });
|
||||
} else if (file.type === 'text/plain') {
|
||||
const name = file.name.replace(/\.[^/.]+$/, '');
|
||||
addUserPattern(name, { code: content });
|
||||
}
|
||||
}),
|
||||
);
|
||||
logger(`import done!`);
|
||||
}
|
||||
|
||||
export async function exportPatterns() {
|
||||
const userPatterns = getUserPatterns() || {};
|
||||
const blob = new Blob([JSON.stringify(userPatterns)], { type: 'application/json' });
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.href = window.URL.createObjectURL(blob);
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
downloadLink.download = `strudel_patterns_${date}.json`;
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
}
|
||||
|
||||
188
website/src/user_pattern_utils.mjs
Normal file
188
website/src/user_pattern_utils.mjs
Normal file
@ -0,0 +1,188 @@
|
||||
import { atom } from 'nanostores';
|
||||
import { persistentAtom } from '@nanostores/persistent';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { logger } from '@strudel/core';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { settingsMap } from './settings.mjs';
|
||||
import { parseJSON, supabase } from './repl/util.mjs';
|
||||
|
||||
export let $publicPatterns = atom([]);
|
||||
export let $featuredPatterns = atom([]);
|
||||
|
||||
export const collectionName = {
|
||||
user: 'user',
|
||||
public: 'Last Creations',
|
||||
stock: 'Stock Examples',
|
||||
featured: 'Featured',
|
||||
};
|
||||
|
||||
export const patternFilterName = {
|
||||
community: 'community',
|
||||
user: 'user',
|
||||
};
|
||||
|
||||
export let $viewingPatternData = persistentAtom(
|
||||
'viewingPatternData',
|
||||
{
|
||||
id: '',
|
||||
code: '',
|
||||
collection: collectionName.user,
|
||||
created_at: Date.now(),
|
||||
},
|
||||
{ listen: false },
|
||||
);
|
||||
|
||||
export const getViewingPatternData = () => {
|
||||
return parseJSON($viewingPatternData.get());
|
||||
};
|
||||
export const useViewingPatternData = () => {
|
||||
return useStore($viewingPatternData);
|
||||
};
|
||||
|
||||
export const setViewingPatternData = (data) => {
|
||||
$viewingPatternData.set(JSON.stringify(data));
|
||||
};
|
||||
|
||||
export function loadPublicPatterns() {
|
||||
return supabase.from('code').select().eq('public', true).limit(20).order('id', { ascending: false });
|
||||
}
|
||||
|
||||
export function loadFeaturedPatterns() {
|
||||
return supabase.from('code').select().eq('featured', true).limit(20).order('id', { ascending: false });
|
||||
}
|
||||
|
||||
export async function loadDBPatterns() {
|
||||
try {
|
||||
const { data: publicPatterns } = await loadPublicPatterns();
|
||||
const { data: featuredPatterns } = await loadFeaturedPatterns();
|
||||
const featured = {};
|
||||
const pub = {};
|
||||
|
||||
publicPatterns?.forEach((data, key) => (pub[data.id ?? key] = data));
|
||||
featuredPatterns?.forEach((data, key) => (featured[data.id ?? key] = data));
|
||||
$publicPatterns.set(pub);
|
||||
$featuredPatterns.set(featured);
|
||||
} catch (err) {
|
||||
console.error('error loading patterns', err);
|
||||
}
|
||||
}
|
||||
|
||||
// reason: https://github.com/tidalcycles/strudel/issues/857
|
||||
const $activePattern = persistentAtom('activePattern', '', { listen: false });
|
||||
|
||||
export function setActivePattern(key) {
|
||||
$activePattern.set(key);
|
||||
}
|
||||
export function getActivePattern() {
|
||||
return $activePattern.get();
|
||||
}
|
||||
export function useActivePattern() {
|
||||
return useStore($activePattern);
|
||||
}
|
||||
|
||||
export const setLatestCode = (code) => settingsMap.setKey('latestCode', code);
|
||||
|
||||
const defaultCode = '';
|
||||
export const userPattern = {
|
||||
collection: collectionName.user,
|
||||
getAll() {
|
||||
const patterns = parseJSON(settingsMap.get().userPatterns);
|
||||
return patterns ?? {};
|
||||
},
|
||||
getPatternData(id) {
|
||||
const userPatterns = this.getAll();
|
||||
return userPatterns[id];
|
||||
},
|
||||
exists(id) {
|
||||
return this.getPatternData(id) != null;
|
||||
},
|
||||
isValidID(id) {
|
||||
return id != null && id.length > 0;
|
||||
},
|
||||
|
||||
create() {
|
||||
const newID = createPatternID();
|
||||
const code = defaultCode;
|
||||
const data = { code, created_at: Date.now(), id: newID, collection: this.collection };
|
||||
return { id: newID, data };
|
||||
},
|
||||
createAndAddToDB() {
|
||||
const newPattern = this.create();
|
||||
return this.update(newPattern.id, newPattern.data);
|
||||
},
|
||||
|
||||
update(id, data) {
|
||||
const userPatterns = this.getAll();
|
||||
data = { ...data, id, collection: this.collection };
|
||||
setUserPatterns({ ...userPatterns, [id]: data });
|
||||
return { id, data };
|
||||
},
|
||||
duplicate(data) {
|
||||
const newPattern = this.create();
|
||||
return this.update(newPattern.id, { ...newPattern.data, code: data.code });
|
||||
},
|
||||
clearAll() {
|
||||
if (!confirm(`This will delete all your patterns. Are you really sure?`)) {
|
||||
return;
|
||||
}
|
||||
const viewingPatternData = getViewingPatternData();
|
||||
setUserPatterns({});
|
||||
|
||||
if (viewingPatternData.collection !== this.collection) {
|
||||
return { id: viewingPatternData.id, data: viewingPatternData };
|
||||
}
|
||||
setActivePattern(null);
|
||||
return this.create();
|
||||
},
|
||||
delete(id) {
|
||||
const userPatterns = this.getAll();
|
||||
delete userPatterns[id];
|
||||
if (getActivePattern() === id) {
|
||||
setActivePattern(null);
|
||||
}
|
||||
setUserPatterns(userPatterns);
|
||||
const viewingPatternData = getViewingPatternData();
|
||||
const viewingID = viewingPatternData?.id;
|
||||
if (viewingID === id) {
|
||||
return { id: null, data: { code: defaultCode } };
|
||||
}
|
||||
return { id: viewingID, data: userPatterns[viewingID] };
|
||||
},
|
||||
};
|
||||
|
||||
function setUserPatterns(obj) {
|
||||
return settingsMap.setKey('userPatterns', JSON.stringify(obj));
|
||||
}
|
||||
|
||||
export const createPatternID = () => {
|
||||
return nanoid(12);
|
||||
};
|
||||
|
||||
export async function importPatterns(fileList) {
|
||||
const files = Array.from(fileList);
|
||||
await Promise.all(
|
||||
files.map(async (file, i) => {
|
||||
const content = await file.text();
|
||||
if (file.type === 'application/json') {
|
||||
const userPatterns = userPattern.getAll();
|
||||
setUserPatterns({ ...userPatterns, ...parseJSON(content) });
|
||||
} else if (file.type === 'text/plain') {
|
||||
const id = file.name.replace(/\.[^/.]+$/, '');
|
||||
userPattern.update(id, { code: content });
|
||||
}
|
||||
}),
|
||||
);
|
||||
logger(`import done!`);
|
||||
}
|
||||
|
||||
export async function exportPatterns() {
|
||||
const userPatterns = userPattern.getAll();
|
||||
const blob = new Blob([JSON.stringify(userPatterns)], { type: 'application/json' });
|
||||
const downloadLink = document.createElement('a');
|
||||
downloadLink.href = window.URL.createObjectURL(blob);
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
downloadLink.download = `strudel_patterns_${date}.json`;
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user