diff --git a/website/src/repl/components/panel/SoundsTab.jsx b/website/src/repl/components/panel/SoundsTab.jsx index a976eb3d2..140f6e200 100644 --- a/website/src/repl/components/panel/SoundsTab.jsx +++ b/website/src/repl/components/panel/SoundsTab.jsx @@ -1,7 +1,12 @@ +// A small UI update to improve the "Sounds" section +// — better organization with folder grouping, the ability to star ⭐ favorite sounds, +// and a slightly cleaner interface. Next step: making it easier to preview individual sounds within folders, +// since right now you only see the total count without an easy way to listen to them one by one. + import useEvent from '@src/useEvent.mjs'; import { useStore } from '@nanostores/react'; import { getAudioContext, soundMap, connectToDestination } from '@strudel/webaudio'; -import { useMemo, useRef, useState } from 'react'; +import { useMemo, useRef, useState, useEffect } from 'react'; import { settingsMap, useSettings } from '../../../settings.mjs'; import { ButtonGroup } from './Forms.jsx'; import ImportSoundsButton from './ImportSoundsButton.jsx'; @@ -14,41 +19,61 @@ export function SoundsTab() { const sounds = useStore(soundMap); const { soundsFilter } = useSettings(); const [search, setSearch] = useState(''); + + // Adds persistent favorites using localStorage. Enhances user experience by allowing them to mark and recall sounds. + // (better way than using localStorage?) + const [starred, setStarred] = useState(() => new Set(JSON.parse(localStorage.getItem('starredSounds') || '[]'))); + const { BASE_URL } = import.meta.env; const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL; + useEffect(() => { + localStorage.setItem('starredSounds', JSON.stringify(Array.from(starred))); + }, [starred]); // Automatically saves favorites when updated + const soundEntries = useMemo(() => { - if (!sounds) { - return []; - } - - let filtered = Object.entries(sounds) + if (!sounds) return []; + return Object.entries(sounds) .filter(([key]) => !key.startsWith('_')) - .sort((a, b) => a[0].localeCompare(b[0])) - .filter(([name]) => name.toLowerCase().includes(search.toLowerCase())); + .filter(([name]) => name.toLowerCase().includes(search.toLowerCase())) + .sort((a, b) => a[0].localeCompare(b[0])); + }, [sounds, search]); + const filteredEntries = useMemo(() => { if (soundsFilter === 'user') { - return filtered.filter(([_, { data }]) => !data.prebake); + return soundEntries.filter(([_, { data }]) => !data.prebake); } if (soundsFilter === 'drums') { - return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag === 'drum-machines'); + return soundEntries.filter(([_, { data }]) => data.type === 'sample' && data.tag === 'drum-machines'); } if (soundsFilter === 'samples') { - return filtered.filter(([_, { data }]) => data.type === 'sample' && data.tag !== 'drum-machines'); + return soundEntries.filter(([_, { data }]) => data.type === 'sample' && data.tag !== 'drum-machines'); } if (soundsFilter === 'synths') { - return filtered.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type)); + return soundEntries.filter(([_, { data }]) => ['synth', 'soundfont'].includes(data.type)); + } + if (soundsFilter === 'starred') { + return soundEntries.filter(([name]) => starred.has(name)); } if (soundsFilter === 'importSounds') { return []; } - return filtered; - }, [sounds, soundsFilter, search]); + return soundEntries; + }, [soundEntries, soundsFilter, starred]); + + // Organizes sound list into collapsible folders for better navigation and visual clarity. + const groupedByFolder = useMemo(() => { + const groups = {}; + for (const [name, { data, onTrigger }] of filteredEntries) { + const folder = data.folder || name.split('(')[0]; + if (!groups[folder]) groups[folder] = []; + groups[folder].push([name, { data, onTrigger }]); + } + return groups; + }, [filteredEntries]); - // holds mutable ref to current triggered sound const trigRef = useRef(); - // stop current sound on mouseup useEvent('mouseup', () => { const t = trigRef.current; trigRef.current = undefined; @@ -56,6 +81,7 @@ export function SoundsTab() { ref?.stop(getAudioContext().currentTime + 0.01); }); }); + return (
@@ -148,10 +192,9 @@ export function SoundsTab() { sample as it appears under the "user" tab.