diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index 4ff92a80..db586363 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -17,7 +17,16 @@ 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, updateUserCode, setActivePattern } from '../settings.mjs'; +import { + settingsMap, + useSettings, + setLatestCode, + updateUserCode, + setActivePattern, + getActivePattern, + getUserPattern, + initUserCode, +} from '../settings.mjs'; import Loader from './Loader'; import { settingPatterns } from '../settings.mjs'; import { code2hash, hash2code } from './helpers.mjs'; @@ -131,7 +140,6 @@ export function Repl({ embedded = false }) { isLineWrappingEnabled, panelPosition, isZen, - activePattern, } = useSettings(); const paintOptions = useMemo(() => ({ fontFamily }), [fontFamily]); @@ -177,7 +185,7 @@ export function Repl({ embedded = false }) { let msg; if (decoded) { setCode(decoded); - setActivePattern(''); + initUserCode(decoded); msg = `I have loaded the code from the URL.`; } else if (latestCode) { setCode(latestCode); diff --git a/website/src/repl/panel/PatternsTab.jsx b/website/src/repl/panel/PatternsTab.jsx index 175ed887..4466b599 100644 --- a/website/src/repl/panel/PatternsTab.jsx +++ b/website/src/repl/panel/PatternsTab.jsx @@ -1,27 +1,27 @@ +import { DocumentDuplicateIcon, PencilSquareIcon, TrashIcon } from '@heroicons/react/20/solid'; import { useMemo } from 'react'; -import * as tunes from '../tunes.mjs'; import { - useSettings, clearUserPatterns, - newUserPattern, - setActivePattern, deleteActivePattern, duplicateActivePattern, + exportPatterns, getUserPattern, - getUserPatterns, + importPatterns, + newUserPattern, renameActivePattern, - addUserPattern, - setUserPatterns, + setActivePattern, + useActivePattern, + useSettings, } from '../../settings.mjs'; -import { logger } from '@strudel.cycles/core'; -import { DocumentDuplicateIcon, PencilSquareIcon, TrashIcon } from '@heroicons/react/20/solid'; +import * as tunes from '../tunes.mjs'; function classNames(...classes) { return classes.filter(Boolean).join(' '); } export function PatternsTab({ context }) { - const { userPatterns, activePattern } = useSettings(); + const { userPatterns } = useSettings(); + const activePattern = useActivePattern(); const isExample = useMemo(() => activePattern && !!tunes[activePattern], [activePattern]); return (
@@ -85,38 +85,11 @@ export function PatternsTab({ context }) { type="file" multiple accept="text/plain,application/json" - onChange={async (e) => { - const files = Array.from(e.target.files); - 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!`); - }} + onChange={(e) => importPatterns(e.target.files)} /> import -
diff --git a/website/src/settings.mjs b/website/src/settings.mjs index 98d3fe50..570b6446 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -1,7 +1,8 @@ -import { persistentMap } from '@nanostores/persistent'; +import { persistentMap, persistentAtom } from '@nanostores/persistent'; import { useStore } from '@nanostores/react'; import { register } from '@strudel.cycles/core'; import * as tunes from './repl/tunes.mjs'; +import { logger } from '@strudel.cycles/core'; export const defaultSettings = { activeFooter: 'intro', @@ -19,11 +20,28 @@ export const defaultSettings = { soundsFilter: 'all', panelPosition: 'bottom', userPatterns: '{}', - activePattern: '', }; 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); return { @@ -116,7 +134,7 @@ export function getUserPattern(key) { } export function renameActivePattern() { - let activePattern = getSetting('activePattern'); + let activePattern = getActivePattern(); let userPatterns = getUserPatterns(); if (!userPatterns[activePattern]) { alert('Cannot rename examples'); @@ -139,7 +157,7 @@ export function renameActivePattern() { export function updateUserCode(code) { const userPatterns = getUserPatterns(); - let activePattern = getSetting('activePattern'); + 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)) { @@ -160,7 +178,7 @@ export function updateUserCode(code) { } export function deleteActivePattern() { - let activePattern = getSetting('activePattern'); + let activePattern = getActivePattern(); if (!activePattern) { console.warn('cannot delete: no pattern selected'); return; @@ -178,7 +196,7 @@ export function deleteActivePattern() { } export function duplicateActivePattern() { - let activePattern = getSetting('activePattern'); + let activePattern = getActivePattern(); let latestCode = getSetting('latestCode'); if (!activePattern) { console.warn('cannot duplicate: no pattern selected'); @@ -190,8 +208,31 @@ export function duplicateActivePattern() { setActivePattern(activePattern); } -export function setActivePattern(key) { - settingsMap.setKey('activePattern', key); +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 function importUserPatternJSON(jsonString) {} +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); +}