Merge branch 'main' of ssh://codeberg.org/vvolhejn/strudel into radical-new-docs

This commit is contained in:
Václav Volhejn 2026-01-17 18:22:56 +01:00
commit a1d31df556
48 changed files with 2776 additions and 592 deletions

View file

@ -66,7 +66,7 @@ export default function ExportTab(Props) {
<div className="flex flex-row gap-4 w-full">
<FormItem label="Start cycle" disabled={exporting}>
<Textbox
min={1}
min={0}
max={Infinity}
onBlur={(e) => {
let v = parseInt(e.target.value);

View file

@ -1,6 +1,7 @@
import { errorLogger } from '@strudel/core';
import { useSettings, storePrebakeScript } from '../../../settings.mjs';
import { SpecialActionInput } from '../button/action-button';
import { confirmDialog, SETTING_CHANGE_RELOAD_MSG } from '@src/repl/util.mjs';
async function importScript(script) {
const reader = new FileReader();
@ -23,7 +24,19 @@ export function ImportPrebakeScriptButton() {
type="file"
label="import prebake script"
accept=".strudel"
onChange={(e) => importScript(e.target.files[0])}
onChange={async (e) => {
const file = e.target.files[0];
const confirmed = await confirmDialog(SETTING_CHANGE_RELOAD_MSG);
if (!confirmed) {
return;
}
try {
await importScript(file);
window.location.reload();
} catch (e) {
errorLogger(e);
}
}}
/>
);
}

View file

@ -18,6 +18,7 @@ import { Pagination } from '../pagination/Pagination.jsx';
import { useState } from 'react';
import { useDebounce } from '../usedebounce.jsx';
import cx from '@src/cx.mjs';
import { Textbox } from '../textbox/Textbox.jsx';
export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
const meta = useMemo(() => getMetadata(pattern.code), [pattern]);
@ -28,12 +29,12 @@ export function PatternLabel({ pattern } /* : { pattern: Tables<'code'> } */) {
if (!isNaN(date)) {
title = date.toLocaleDateString();
} else {
title = 'unnamed';
title = pattern.id || 'unnamed';
}
}
const author = Array.isArray(meta.by) ? meta.by.join(',') : 'Anonymous';
return <>{`${pattern.id}: ${title} by ${author.slice(0, 100)}`.slice(0, 60)}</>;
return <>{`${title} by ${author.slice(0, 100)}`.slice(0, 60)}</>;
}
function PatternButton({ showOutline, onClick, pattern, showHiglight }) {
@ -79,73 +80,115 @@ const updateCodeWindow = (context, patternData, reset = false) => {
context.handleUpdate(patternData, reset);
};
function UserPatterns({ context }) {
export function PatternsTab({ context }) {
const [search, setSearch] = useState('');
const activePattern = useActivePattern();
const viewingPatternStore = useViewingPatternData();
const viewingPatternData = parseJSON(viewingPatternStore);
const { userPatterns, patternFilter, patternAutoStart } = useSettings();
const { userPatterns, patternAutoStart } = useSettings();
const viewingPatternID = viewingPatternData?.id;
return (
<div className="flex flex-col gap-2 flex-grow overflow-hidden h-full pb-2 ">
<div className="pr-4 space-x-4 flex max-w-full overflow-x-auto">
<ActionButton
label="new"
onClick={() => {
const { data } = userPattern.createAndAddToDB();
updateCodeWindow(context, data);
}}
/>
<ActionButton
label="duplicate"
onClick={() => {
const { data } = userPattern.duplicate(viewingPatternData);
updateCodeWindow(context, data);
}}
/>
<ActionButton
label="delete"
onClick={() => {
const { data } = userPattern.delete(viewingPatternID);
updateCodeWindow(context, { ...data, collection: userPattern.collection });
}}
/>
<label className="hover:opacity-50 cursor-pointer">
<input
style={{ display: 'none' }}
type="file"
multiple
accept="text/plain,text/x-markdown,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(context, data);
}}
/>
</div>
const visiblePatterns = useMemo(() => {
if (!search) {
return userPatterns;
}
return Object.fromEntries(
Object.entries(userPatterns).filter(([_key, pattern]) => {
const meta = getMetadata(pattern.code);
<div className="overflow-auto h-full bg-background p-2 rounded-md">
{/* {patternFilter === patternFilterName.user && ( */}
<PatternButtons
onClick={(id) => {
updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart);
if (context.started && activePattern === id) {
context.handleEvaluate();
// Search for specific meta keys
const searchLowercaseTrimmed = search.trim().toLowerCase();
if (searchLowercaseTrimmed.includes(':')) {
const [metaKey, metaSearch] = searchLowercaseTrimmed.split(/:\s*/);
if (metaKey !== undefined && metaSearch !== undefined && metaKey in meta) {
const metaValues = meta[metaKey];
if (Array.isArray(metaValues)) {
return metaValues.some((metaValue) => metaValue.toLowerCase().includes(metaSearch));
} else if (typeof metaValues === 'string') {
return metaValues.toLowerCase().includes(metaSearch);
} else {
return false;
}
}}
patterns={userPatterns}
started={context.started}
activePattern={activePattern}
viewingPatternID={viewingPatternID}
/>
{/* )} */}
}
}
const title = meta.title ? meta.title : 'unnamed';
const authors = meta.by ? meta.by : ['anonymous'];
const tags = meta.tag ? meta.tag : [];
return (
title.toLowerCase().includes(searchLowercaseTrimmed) ||
authors.some((author) => author.toLowerCase().includes(searchLowercaseTrimmed)) ||
tags.some((tag) => tag.toLowerCase().includes(searchLowercaseTrimmed))
);
}),
);
}, [search, viewingPatternStore]);
return (
<div className="px-4 w-full text-foreground space-y-2 flex flex-col overflow-hidden max-h-full h-full">
<div className="w-full flex">
<Textbox className="w-full" placeholder="Search" value={search} onChange={setSearch} />
</div>
<div className="flex flex-col gap-2 flex-grow overflow-hidden h-full pb-2 ">
<div className="pr-4 space-x-4 flex max-w-full overflow-x-auto">
<ActionButton
label="new"
onClick={() => {
const { data } = userPattern.createAndAddToDB();
updateCodeWindow(context, data);
}}
/>
<ActionButton
label="duplicate"
onClick={() => {
const { data } = userPattern.duplicate(viewingPatternData);
updateCodeWindow(context, data);
}}
/>
<ActionButton
label="delete"
onClick={() => {
const { data } = userPattern.delete(viewingPatternID);
updateCodeWindow(context, { ...data, collection: userPattern.collection });
}}
/>
<label className="hover:opacity-50 cursor-pointer">
<input
style={{ display: 'none' }}
type="file"
multiple
accept="text/plain,text/x-markdown,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(context, data);
}}
/>
</div>
<div className="overflow-auto h-full bg-background p-2 rounded-md">
{/* {patternFilter === patternFilterName.user && ( */}
<PatternButtons
onClick={(id) => {
updateCodeWindow(context, { ...userPatterns[id], collection: userPattern.collection }, patternAutoStart);
if (context.started && activePattern === id) {
context.handleEvaluate();
}
}}
patterns={visiblePatterns}
started={context.started}
activePattern={activePattern}
viewingPatternID={viewingPatternID}
/>
{/* )} */}
</div>
</div>
</div>
);
@ -232,28 +275,3 @@ function PublicPatterns({ context }) {
}
return <LatestPatterns context={context} />;
}
export function PatternsTab({ context }) {
const { patternFilter } = useSettings();
return (
<div className="px-4 w-full text-foreground space-y-2 flex flex-col overflow-hidden max-h-full h-full">
<UserPatterns context={context} />
</div>
);
/* return (
<div className="px-4 w-full text-foreground space-y-2 flex flex-col overflow-hidden max-h-full h-full">
<ButtonGroup
value={patternFilter}
onChange={(value) => settingsMap.setKey('patternFilter', value)}
items={patternFilterName}
></ButtonGroup>
{patternFilter === patternFilterName.user ? (
<UserPatterns context={context} />
) : (
<PublicPatterns context={context} />
)}
</div>
); */
}

View file

@ -85,9 +85,12 @@ export function Reference() {
}
return (
<div className="flex flex-col h-full w-full p-2">
<div className="w-full flex flex-col gap-2 h-2/5 mb-2">
<div className="w-full flex flex-col gap-2">
// <div className="flex flex-col h-full w-full p-2">
// <div className="w-full flex flex-col gap-2 h-2/5 mb-2">
// <div className="w-full flex flex-col gap-2"></div>
<div className="flex h-full w-full p-2 overflow-hidden">
<div className="h-full flex flex-col gap-2 w-1/3 max-w-72 ">
<div className="w-full flex">
<Textbox className="w-full" placeholder="Search" value={search} onChange={setSearch} />
<div>
{Object.entries(tagCounts)

View file

@ -1,13 +1,13 @@
import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs';
import { themes } from '@strudel/codemirror';
import { Textbox } from '../textbox/Textbox.jsx';
import { isUdels } from '../../util.mjs';
import { confirmAndReloadPage, isUdels } from '../../util.mjs';
import { ButtonGroup } from './Forms.jsx';
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
import { confirmDialog } from '../../util.mjs';
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio';
import { ActionButton, SpecialActionButton } from '../button/action-button.jsx';
import { SpecialActionButton } from '../button/action-button.jsx';
import { ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx';
function Checkbox({ label, value, onChange, disabled = false }) {
@ -86,8 +86,6 @@ const fontFamilyOptions = {
galactico: 'galactico',
};
const RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?';
export function SettingsTab({ started }) {
const {
theme,
@ -127,11 +125,8 @@ export function SettingsTab({ started }) {
isDisabled={started}
audioDeviceName={audioDeviceName}
onChange={(audioDeviceName) => {
confirmDialog(RELOAD_MSG).then((r) => {
if (r == true) {
settingsMap.setKey('audioDeviceName', audioDeviceName);
return window.location.reload();
}
confirmAndReloadPage(() => {
settingsMap.setKey('audioDeviceName', audioDeviceName);
});
}}
/>
@ -141,11 +136,8 @@ export function SettingsTab({ started }) {
<AudioEngineTargetSelector
target={audioEngineTarget}
onChange={(target) => {
confirmDialog(RELOAD_MSG).then((r) => {
if (r == true) {
settingsMap.setKey('audioEngineTarget', target);
return window.location.reload();
}
confirmAndReloadPage(() => {
settingsMap.setKey('audioEngineTarget', target);
});
}}
/>
@ -175,12 +167,9 @@ export function SettingsTab({ started }) {
label="Multi Channel Orbits"
onChange={(cbEvent) => {
const val = cbEvent.target.checked;
confirmDialog(RELOAD_MSG).then((r) => {
if (r == true) {
settingsMap.setKey('multiChannelOrbits', val);
setMultiChannelOrbits(val);
return window.location.reload();
}
confirmAndReloadPage(() => {
settingsMap.setKey('multiChannelOrbits', val);
setMultiChannelOrbits(val);
});
}}
value={multiChannelOrbits}
@ -297,11 +286,8 @@ export function SettingsTab({ started }) {
label="Sync across Browser Tabs / Windows"
onChange={(cbEvent) => {
const newVal = cbEvent.target.checked;
confirmDialog(RELOAD_MSG).then((r) => {
if (r) {
settingsMap.setKey('isSyncEnabled', newVal);
window.location.reload();
}
confirmAndReloadPage(() => {
settingsMap.setKey('isSyncEnabled', newVal);
});
}}
disabled={shouldAlwaysSync}

View file

@ -210,7 +210,11 @@ export function SoundsTab() {
) : (
''
)}
{!soundEntries.length && soundsFilter !== 'importSounds' ? 'No sounds loaded' : ''}
{!soundEntries.length && soundsFilter !== 'importSounds'
? search == ''
? 'No sounds loaded'
: 'No sounds found'
: ''}
</div>
</div>
);