mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-24 05:57:17 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3f9ec963a |
@@ -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 useEvent from '@src/useEvent.mjs';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { getAudioContext, soundMap, connectToDestination } from '@strudel/webaudio';
|
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 { settingsMap, useSettings } from '../../../settings.mjs';
|
||||||
import { ButtonGroup } from './Forms.jsx';
|
import { ButtonGroup } from './Forms.jsx';
|
||||||
import ImportSoundsButton from './ImportSoundsButton.jsx';
|
import ImportSoundsButton from './ImportSoundsButton.jsx';
|
||||||
@@ -14,41 +19,61 @@ export function SoundsTab() {
|
|||||||
const sounds = useStore(soundMap);
|
const sounds = useStore(soundMap);
|
||||||
const { soundsFilter } = useSettings();
|
const { soundsFilter } = useSettings();
|
||||||
const [search, setSearch] = useState('');
|
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 { BASE_URL } = import.meta.env;
|
||||||
const baseNoTrailing = BASE_URL.endsWith('/') ? BASE_URL.slice(0, -1) : BASE_URL;
|
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(() => {
|
const soundEntries = useMemo(() => {
|
||||||
if (!sounds) {
|
if (!sounds) return [];
|
||||||
return [];
|
return Object.entries(sounds)
|
||||||
}
|
|
||||||
|
|
||||||
let filtered = Object.entries(sounds)
|
|
||||||
.filter(([key]) => !key.startsWith('_'))
|
.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') {
|
if (soundsFilter === 'user') {
|
||||||
return filtered.filter(([_, { data }]) => !data.prebake);
|
return soundEntries.filter(([_, { data }]) => !data.prebake);
|
||||||
}
|
}
|
||||||
if (soundsFilter === 'drums') {
|
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') {
|
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') {
|
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') {
|
if (soundsFilter === 'importSounds') {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return filtered;
|
return soundEntries;
|
||||||
}, [sounds, soundsFilter, search]);
|
}, [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();
|
const trigRef = useRef();
|
||||||
|
|
||||||
// stop current sound on mouseup
|
|
||||||
useEvent('mouseup', () => {
|
useEvent('mouseup', () => {
|
||||||
const t = trigRef.current;
|
const t = trigRef.current;
|
||||||
trigRef.current = undefined;
|
trigRef.current = undefined;
|
||||||
@@ -56,6 +81,7 @@ export function SoundsTab() {
|
|||||||
ref?.stop(getAudioContext().currentTime + 0.01);
|
ref?.stop(getAudioContext().currentTime + 0.01);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full text-foreground">
|
<div id="sounds-tab" className="px-4 flex flex-col w-full h-full text-foreground">
|
||||||
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
|
<Textbox placeholder="Search" value={search} onChange={(v) => setSearch(v)} />
|
||||||
@@ -70,42 +96,60 @@ export function SoundsTab() {
|
|||||||
synths: 'Synths',
|
synths: 'Synths',
|
||||||
user: 'User',
|
user: 'User',
|
||||||
importSounds: 'import-sounds',
|
importSounds: 'import-sounds',
|
||||||
|
starred: '⭐',
|
||||||
}}
|
}}
|
||||||
></ButtonGroup>
|
></ButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal pb-2">
|
<div className="min-h-0 max-h-full grow overflow-auto text-sm break-normal pb-2">
|
||||||
{soundEntries.map(([name, { data, onTrigger }]) => {
|
{Object.entries(groupedByFolder).map(([folder, entries]) => (
|
||||||
return (
|
<details key={folder} className="mb-2">
|
||||||
<span
|
<summary className="cursor-pointer font-semibold">{folder}</summary>
|
||||||
key={name}
|
<div className="pl-4">
|
||||||
className="cursor-pointer hover:opacity-50"
|
{entries.map(([name, { data, onTrigger }]) => (
|
||||||
onMouseDown={async () => {
|
<div key={name} className="flex justify-between mr-5 items-center py-1">
|
||||||
const ctx = getAudioContext();
|
<span
|
||||||
const params = {
|
className="cursor-pointer hover:opacity-50"
|
||||||
note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined,
|
onMouseDown={async () => {
|
||||||
s: name,
|
const ctx = getAudioContext();
|
||||||
clip: 1,
|
const params = {
|
||||||
release: 0.5,
|
note: ['synth', 'soundfont'].includes(data.type) ? 'a3' : undefined,
|
||||||
sustain: 1,
|
s: name,
|
||||||
duration: 0.5,
|
clip: 1,
|
||||||
};
|
release: 0.5,
|
||||||
const time = ctx.currentTime + 0.05;
|
sustain: 1,
|
||||||
const onended = () => trigRef.current?.node?.disconnect();
|
duration: 0.5,
|
||||||
trigRef.current = Promise.resolve(onTrigger(time, params, onended));
|
};
|
||||||
trigRef.current.then((ref) => {
|
const time = ctx.currentTime + 0.05;
|
||||||
connectToDestination(ref?.node);
|
const onended = () => trigRef.current?.node?.disconnect();
|
||||||
});
|
trigRef.current = Promise.resolve(onTrigger(time, params, onended));
|
||||||
}}
|
trigRef.current.then((ref) => {
|
||||||
>
|
connectToDestination(ref?.node);
|
||||||
{' '}
|
});
|
||||||
{name}
|
}}
|
||||||
{data?.type === 'sample' ? `(${getSamples(data.samples)})` : ''}
|
>
|
||||||
{data?.type === 'soundfont' ? `(${data.fonts.length})` : ''}
|
{name} {data?.type === 'sample' ? `(${getSamples(data.samples)})` : ''}
|
||||||
</span>
|
{data?.type === 'soundfont' ? `(${data.fonts.length})` : ''}
|
||||||
);
|
</span>
|
||||||
})}
|
<button
|
||||||
{!soundEntries.length && soundsFilter === 'importSounds' ? (
|
className="ml-2 text-yellow-400 hover:text-yellow-600"
|
||||||
|
onClick={() => {
|
||||||
|
setStarred((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.has(name) ? next.delete(name) : next.add(name);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{starred.has(name) ? '★' : '☆'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{!Object.keys(groupedByFolder).length && soundsFilter === 'importSounds' ? (
|
||||||
<div className="prose dark:prose-invert min-w-full pt-2 pb-8 px-4">
|
<div className="prose dark:prose-invert min-w-full pt-2 pb-8 px-4">
|
||||||
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
|
<ImportSoundsButton onComplete={() => settingsMap.setKey('soundsFilter', 'user')} />
|
||||||
<p>
|
<p>
|
||||||
@@ -148,10 +192,9 @@ export function SoundsTab() {
|
|||||||
sample as it appears under the "user" tab.
|
sample as it appears under the "user" tab.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : null}
|
||||||
''
|
|
||||||
)}
|
{!Object.keys(groupedByFolder).length && soundsFilter !== 'importSounds'
|
||||||
{!soundEntries.length && soundsFilter !== 'importSounds'
|
|
||||||
? 'No custom sounds loaded in this pattern (yet).'
|
? 'No custom sounds loaded in this pattern (yet).'
|
||||||
: ''}
|
: ''}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user