diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx
index 9bd26e02..8ec9feb2 100644
--- a/website/src/repl/Repl.jsx
+++ b/website/src/repl/Repl.jsx
@@ -12,18 +12,17 @@ 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,
- useSettings,
- getViewingPattern,
setViewingPattern,
createPatternID,
userPattern,
- getNextCloneID,
-} from '../settings.mjs';
+ getViewingPatternData,
+ setViewingPatternData,
+} from '../user_pattern_utils.mjs';
import { Header } from './Header';
import Loader from './Loader';
import { Panel } from './panel/Panel';
@@ -32,12 +31,8 @@ import { prebake } from './prebake.mjs';
import { getRandomTune, initCode, loadModules, shareCode, ReplContext } from './util.mjs';
import PlayCircleIcon from '@heroicons/react/20/solid/PlayCircleIcon';
import './Repl.css';
-import { useExamplePatterns } from './useExamplePatterns';
const { code: randomTune, name } = getRandomTune();
-
-//
-
const { latestCode } = settingsMap.get();
let modulesLoading, presets, drawContext, clearCanvas, isIframe;
@@ -50,8 +45,6 @@ if (typeof window !== 'undefined') {
isIframe = window.location !== window.parent.location;
}
-let viewingPatternData = { id: '', code: null, collection: userPattern.source };
-
export function Repl({ embedded = false }) {
const isEmbedded = embedded || isIframe;
const { panelPosition, isZen } = useSettings();
@@ -83,23 +76,24 @@ export function Repl({ embedded = false }) {
const { code } = all;
setLatestCode(code);
window.location.hash = '#' + code2hash(code);
+ const viewingPatternData = getViewingPatternData();
const data = { ...viewingPatternData, code };
- let id = getViewingPattern();
- const isExamplePattern = viewingPatternData.collection != userPattern.source;
+ let id = data.id;
+ const isExamplePattern = viewingPatternData.collection !== userPattern.collection;
if (isExamplePattern) {
const codeHasChanged = code !== viewingPatternData.code;
if (codeHasChanged) {
// fork example
- id = getNextCloneID(id);
+ id = createPatternID();
setViewingPattern(id);
- viewingPatternData = userPattern.update(id, data).data;
+ setViewingPatternData(userPattern.update(id, data).data);
}
} else {
id = id == null ? createPatternID() : id;
setViewingPattern(id);
- viewingPatternData = userPattern.update(id, data).data;
+ setViewingPatternData(userPattern.update(id, data).data);
}
setActivePattern(id);
},
@@ -176,7 +170,7 @@ export function Repl({ embedded = false }) {
};
const handleUpdate = async (id, data, reset = false) => {
- viewingPatternData = data;
+ setViewingPatternData(data);
if (reset) {
await resetEditor();
}
diff --git a/website/src/repl/panel/PatternsTab.jsx b/website/src/repl/panel/PatternsTab.jsx
index 60ca4465..a950d754 100644
--- a/website/src/repl/panel/PatternsTab.jsx
+++ b/website/src/repl/panel/PatternsTab.jsx
@@ -1,20 +1,13 @@
-import { DocumentDuplicateIcon, PencilSquareIcon, TrashIcon } from '@heroicons/react/20/solid';
-
+import { DocumentDuplicateIcon, TrashIcon } from '@heroicons/react/20/solid';
+import { useSettings } from '../../settings.mjs';
import {
- $featuredPatterns,
- $publicPatterns,
exportPatterns,
importPatterns,
useActivePattern,
useViewingPattern,
- useSettings,
userPattern,
- examplePattern,
-} from '../../settings.mjs';
-
+} from '../../user_pattern_utils.mjs';
import { useMemo } from 'react';
-
-import { useStore } from '@nanostores/react';
import { getMetadata } from '../../metadata_parser';
import { useExamplePatterns } from '../useExamplePatterns';
@@ -24,19 +17,14 @@ function classNames(...classes) {
function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
+
return (
- <>{`${pattern.id}: ${meta.title ?? pattern.hash ?? 'unnamed'} by ${
- Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous'
- }`}>
+ <>{`${pattern.id}: ${
+ meta.title ?? pattern.hash ?? new Date(pattern.created_at).toLocaleDateString() ?? 'unnamed'
+ } by ${Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous'}`}>
);
}
-const getPatternLabel = (pattern) => {
- return `${pattern.id}: ${meta.title ?? pattern.hash ?? 'unnamed'} by ${
- Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous'
- }`;
-};
-
function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
return (
{
-// setActivePattern(pattern.hash);
-// context.handleUpdate(pattern.code, true);
-// }}
-// >
-//
-//
-// );
-// }
-
function PatternButtons({ patterns, activePattern, onClick, viewingPattern, started }) {
return (
@@ -89,28 +59,27 @@ function PatternButtons({ patterns, activePattern, onClick, viewingPattern, star
);
}
+function ActionButton({ children, onClick, label, labelIsHidden }) {
+ return (
+
+ );
+}
+
export function PatternsTab({ context }) {
const activePattern = useActivePattern();
const viewingPattern = useViewingPattern();
const { userPatterns } = useSettings();
const examplePatterns = useExamplePatterns();
const collections = examplePatterns.collections;
- const examplesData = examplePatterns.patterns;
const updateCodeWindow = (id, data, reset = false) => {
context.handleUpdate(id, data, reset);
- // if (patternSource === userPattern.source) {
-
- // } else {
- // const source = otherPatterns.get(patternSource);
- // const data = source[id];
- // }
};
-
const isUserPattern = userPatterns[viewingPattern] != null;
- // const isExample = useMemo(() => activePattern && !!tunes[activePattern], [activePattern]);
-
return (
@@ -118,69 +87,56 @@ export function PatternsTab({ context }) {
{`${viewingPattern}`}
- {/* {!isExample && (
-
- )} */}
-
+
{isUserPattern && (
-
+
)}
)}
updateCodeWindow(id, { ...userPatterns[id], collection: userPattern.source }, false)}
+ onClick={(id) => updateCodeWindow(id, { ...userPatterns[id], collection: userPattern.collection }, false)}
patterns={userPatterns}
started={context.started}
activePattern={activePattern}
viewingPattern={viewingPattern}
/>
-
-
+
{
const { id, data } = userPattern.clearAll();
- updateCodeWindow(id, data.code);
+ updateCodeWindow(id, data);
}}
- >
- clear
-
+ />
+
-
+
{Array.from(collections.keys()).map((collection) => {
const patterns = collections.get(collection);
-
return (
{collection}
diff --git a/website/src/repl/useExamplePatterns.jsx b/website/src/repl/useExamplePatterns.jsx
index f51d9091..65d94583 100644
--- a/website/src/repl/useExamplePatterns.jsx
+++ b/website/src/repl/useExamplePatterns.jsx
@@ -1,34 +1,23 @@
-import { examplePattern, $featuredPatterns, $publicPatterns } from '../settings.mjs';
+import { $featuredPatterns, $publicPatterns } from '../user_pattern_utils.mjs';
import { useStore } from '@nanostores/react';
-import { useCallback, useMemo } from 'react';
+import { useMemo } from 'react';
+import * as tunes from '../repl/tunes.mjs';
export const useExamplePatterns = () => {
const featuredPatterns = useStore($featuredPatterns);
const publicPatterns = useStore($publicPatterns);
const collections = useMemo(() => {
+ const stockPatterns = Object.fromEntries(Object.entries(tunes).map(([key, code], i) => [i, { id: i, code }]));
const pats = new Map();
pats.set('Featured', featuredPatterns);
pats.set('Last Creations', publicPatterns);
- pats.set(examplePattern.source, examplePattern.getAll());
+ pats.set('Stock Examples', stockPatterns);
return pats;
}, [featuredPatterns, publicPatterns]);
const patterns = useMemo(() => {
- const allPatterns = Object.assign({}, ...collections.values());
- return allPatterns;
+ return Object.assign({}, ...collections.values());
}, [collections]);
- // const examplePatterns = examplePattern.getAll();
-
- // const collections = new Map();
- // collections.set('Featured', featuredPatterns);
- // collections.set('Last Creations', publicPatterns);
- // collections.set(examplePattern.source, examplePatterns);
- // const patterns = {
- // ...examplePatterns,
- // ...publicPatterns,
- // ...examplePatterns,
- // };
-
return { patterns, collections };
};
diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs
index 1da4fc65..19afdabb 100644
--- a/website/src/repl/util.mjs
+++ b/website/src/repl/util.mjs
@@ -9,7 +9,6 @@ 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';
// Create a single supabase client for interacting with your database
export const supabase = createClient(
@@ -17,33 +16,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();
- 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');
- }
-}
-
-if (typeof window !== 'undefined') {
- loadDBPatterns();
-}
-
export async function initCode() {
// load code from url hash (either short hash from database or decode long hash)
try {
diff --git a/website/src/settings.mjs b/website/src/settings.mjs
index 9f2da640..8815a215 100644
--- a/website/src/settings.mjs
+++ b/website/src/settings.mjs
@@ -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.cycles/core';
-import * as tunes from './repl/tunes.mjs';
-import { logger } from '@strudel.cycles/core';
-import { nanoid } from 'nanoid';
-export let $publicPatterns = atom([]);
-export let $featuredPatterns = atom([]);
export const defaultAudioDeviceName = 'System Standard';
@@ -33,54 +27,6 @@ export const defaultSettings = {
export const settingsMap = persistentMap('strudel-settings', defaultSettings);
-const defaultCode = '';
-//pattern that the use is currently viewing in the window
-const $viewingPattern = persistentAtom('viewingPattern', '', { listen: false });
-export function setViewingPattern(key) {
- $viewingPattern.set(key);
-}
-export function getViewingPattern() {
- return $viewingPattern.get();
-}
-
-export function useViewingPattern() {
- return useStore($viewingPattern);
-}
-
-// const $viewingCollection = persistentAtom('viewingCollection', '', { listen: false });
-// export function setViewingCollection(key) {
-// $viewingCollection.set(key);
-// }
-// export function getViewingCollection() {
-// return $viewingCollection.get();
-// }
-
-// export function useViewingCollection() {
-// return useStore($viewingCollection);
-// }
-// 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 patterns = { ...userPattern.getAll(), ...examplePattern.getAll() };
- const match = Object.entries(patterns).find(([_, pat]) => pat.code === code);
- const id = match?.[0];
- if (id != null) {
- setActivePattern(id);
- setViewingPattern(id);
- }
-}
-
export function useSettings() {
const state = useStore(settingsMap);
@@ -88,7 +34,6 @@ export function useSettings() {
Object.keys(userPatterns).forEach((key) => {
const data = userPatterns[key];
data.id = data.id ?? key;
- data.date = data.date ?? 0;
userPatterns[key] = data;
});
return {
@@ -109,7 +54,6 @@ export function useSettings() {
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) =>
@@ -128,186 +72,3 @@ export const fontFamily = patternSetting('fontFamily');
export const fontSize = patternSetting('fontSize');
export const settingPatterns = { theme, fontFamily, fontSize };
-
-function getUserPatterns() {
- return JSON.parse(settingsMap.get().userPatterns);
-}
-
-function setUserPatterns(obj) {
- return settingsMap.setKey('userPatterns', JSON.stringify(obj));
-}
-
-export const createPatternID = () => {
- return nanoid(12);
-};
-
-export const getNextCloneID = (id) => {
- return createPatternID();
-};
-
-const examplePatterns = Object.fromEntries(Object.entries(tunes).map(([key, code], i) => [i, { id: i, code }]));
-
-export const examplePattern = {
- source: 'Stock Examples',
- getAll() {
- return examplePatterns;
- },
- getPatternData(id) {
- const pats = this.getAll();
- return pats[id];
- },
- exists(id) {
- return this.getPatternData(id) != null;
- },
-};
-
-// break
-export const userPattern = {
- source: 'user',
- collection: 'user',
- getAll() {
- const patterns = JSON.parse(settingsMap.get().userPatterns);
- return patterns;
- },
- getPatternData(id) {
- const userPatterns = this.getAll();
- return userPatterns[id];
- },
- exists(id) {
- return this.getPatternData(id) != null;
- },
-
- create() {
- const newID = createPatternID();
- const code = defaultCode;
- const data = { code, created_at: Date.now(), id: newID, collection: this.collection };
- return this.update(newID, data);
- },
- update(id, data) {
- const userPatterns = this.getAll();
- data = { ...data, id, collection: this.collection };
- setUserPatterns({ ...userPatterns, [id]: data });
- return { id, data };
- },
- duplicate(id) {
- const examplePatternData = examplePattern.getPatternData(id);
- const data = examplePatternData != null ? examplePatternData : this.getPatternData(id);
- const newID = getNextCloneID(id);
- return this.update(newID, data);
- },
- clearAll() {
- if (!confirm(`This will delete all your patterns. Are you really sure?`)) {
- return;
- }
- const viewingPattern = getViewingPattern();
- const examplePatternData = examplePattern.getPatternData(viewingPattern);
- setUserPatterns({});
- if (examplePatternData != null) {
- return { id: viewingPattern, data: examplePatternData };
- }
- // setViewingPattern(null);
- setActivePattern(null);
-
- return { id: null, data: { code: defaultCode, id: null, collection: this.collection } };
- },
- delete(id) {
- const userPatterns = this.getAll();
- delete userPatterns[id];
- if (getActivePattern() === id) {
- setActivePattern(null);
- }
- setUserPatterns(userPatterns);
- const viewingPattern = getViewingPattern();
- if (viewingPattern === id) {
- return { id: null, data: { code: defaultCode } };
- }
- return { id: viewingPattern, data: userPatterns[viewingPattern] };
- },
-
- rename(id) {
- const userPatterns = this.getAll();
- const newID = prompt('Enter new name', id);
- const data = userPatterns[id];
- if (newID === null) {
- // canceled
- return { id, data };
- }
- if (userPatterns[newID]) {
- alert('Name already taken!');
- return { id, data };
- }
- userPatterns[newID] = data; // copy code
- delete userPatterns[id];
-
- setUserPatterns({ ...userPatterns });
- if (id === getActivePattern()) {
- setActivePattern(newID);
- }
- return { id: newID, data };
- },
-};
-
-// 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 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 id = file.name.replace(/\.[^/.]+$/, '');
- userPattern.update(id, { 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);
-}
diff --git a/website/src/user_pattern_utils.mjs b/website/src/user_pattern_utils.mjs
new file mode 100644
index 00000000..70b64dc1
--- /dev/null
+++ b/website/src/user_pattern_utils.mjs
@@ -0,0 +1,190 @@
+import { atom } from 'nanostores';
+import { persistentAtom } from '@nanostores/persistent';
+import { useStore } from '@nanostores/react';
+
+import { logger } from '@strudel.cycles/core';
+import { nanoid } from 'nanoid';
+import { settingsMap } from './settings.mjs';
+import { supabase } from './repl/util.mjs';
+
+export let $publicPatterns = atom([]);
+export let $featuredPatterns = atom([]);
+const userPatternCollectionName = 'user';
+export let $viewingPatternData = atom({ id: null, code: null, collection: userPatternCollectionName });
+
+export const getViewingPatternData = () => {
+ return $viewingPatternData.get();
+};
+
+export const setViewingPatternData = (data) => {
+ $viewingPatternData.set(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 });
+}
+
+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');
+ }
+}
+
+if (typeof window !== 'undefined') {
+ loadDBPatterns();
+}
+
+//pattern that the use is currently viewing in the window
+const $viewingPattern = persistentAtom('viewingPattern', '', { listen: false });
+export function setViewingPattern(key) {
+ $viewingPattern.set(key);
+}
+export function getViewingPattern() {
+ return $viewingPattern.get();
+}
+
+export function useViewingPattern() {
+ return useStore($viewingPattern);
+}
+// 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 patterns = { ...userPattern.getAll() };
+ const match = Object.entries(patterns).find(([_, pat]) => pat.code === code);
+ const id = match?.[0];
+ if (id != null) {
+ setActivePattern(id);
+ setViewingPattern(id);
+ }
+}
+
+export const setLatestCode = (code) => settingsMap.setKey('latestCode', code);
+
+const defaultCode = '';
+export const userPattern = {
+ collection: userPatternCollectionName,
+ getAll() {
+ const patterns = JSON.parse(settingsMap.get().userPatterns);
+ return patterns ?? {};
+ },
+ getPatternData(id) {
+ const userPatterns = this.getAll();
+ return userPatterns[id];
+ },
+ exists(id) {
+ return this.getPatternData(id) != null;
+ },
+
+ 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();
+ 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 newID = createPatternID();
+ return this.update(newID, data);
+ },
+ 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 viewingPattern = getViewingPattern();
+ if (viewingPattern === id) {
+ return { id: null, data: { code: defaultCode } };
+ }
+ return { id: viewingPattern, data: userPatterns[viewingPattern] };
+ },
+};
+
+function setUserPatterns(obj) {
+ return settingsMap.setKey('userPatterns', JSON.stringify(obj));
+}
+
+export const createPatternID = () => {
+ return nanoid(12);
+};
+
+export const getNextCloneID = (id) => {
+ return createPatternID();
+};
+
+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, ...JSON.parse(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);
+}