mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-12 22:15:27 -04:00
Merge branch 'main' into glossing/keyboard
This commit is contained in:
@@ -24,6 +24,7 @@ import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
|
||||
import { activateTheme, initTheme, theme } from './themes.mjs';
|
||||
import { isTooltipEnabled } from './tooltip.mjs';
|
||||
import { updateWidgets, widgetPlugin } from './widget.mjs';
|
||||
import { jumpToCharacter } from './labelJump.mjs';
|
||||
|
||||
export { toggleBlockComment, toggleBlockCommentByLine, toggleComment, toggleLineComment } from '@codemirror/commands';
|
||||
|
||||
@@ -119,6 +120,14 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, roo
|
||||
preventDefault: true,
|
||||
run: () => onStop?.(),
|
||||
},
|
||||
{
|
||||
key: 'Alt-w',
|
||||
run: (view) => jumpToCharacter(view, '$', 1),
|
||||
},
|
||||
{
|
||||
key: 'Alt-q',
|
||||
run: (view) => jumpToCharacter(view, '$', -1),
|
||||
},
|
||||
/* {
|
||||
key: 'Ctrl-Shift-.',
|
||||
run: () => (onPanic ? onPanic() : onStop?.()),
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { EditorSelection } from '@codemirror/state';
|
||||
import { SearchCursor } from '@codemirror/search';
|
||||
|
||||
export function jumpToCharacter(view, character, direction = 1) {
|
||||
const { state, dispatch } = view;
|
||||
const pos = state.selection.main.head;
|
||||
const cursor = new SearchCursor(state.doc, character);
|
||||
|
||||
let characterPositions = [];
|
||||
let jumpPos;
|
||||
while (!cursor.next().done) {
|
||||
characterPositions.push(cursor.value.to);
|
||||
}
|
||||
if (!characterPositions.length) {
|
||||
return false;
|
||||
}
|
||||
if (direction > 0) {
|
||||
jumpPos = characterPositions.find((x) => x > pos + 1) ?? characterPositions.at(0); // Loop back around for convenience
|
||||
} else {
|
||||
jumpPos = characterPositions.reverse().find((x) => x < pos + 1) ?? characterPositions.at(0);
|
||||
}
|
||||
|
||||
if (jumpPos == null) {
|
||||
return false;
|
||||
}
|
||||
dispatch({
|
||||
selection: EditorSelection.cursor(jumpPos - 1),
|
||||
scrollIntoView: true,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
const ALLOW_MANY = ['by', 'url', 'genre', 'license'];
|
||||
const ALLOW_MANY = ['by', 'url', 'genre', 'license', 'tag'];
|
||||
|
||||
export function getMetadata(raw_code) {
|
||||
if (raw_code == null) {
|
||||
|
||||
@@ -19,6 +19,14 @@ While there is no charge there are some caveats, e.g.:
|
||||
- the source code must stay free, i.e. you cannot distribute strudel or tidal as part of projects with incompatible licenses - see the [license](https://www.gnu.org/licenses/agpl-3.0.en.html) for details.
|
||||
- the contributed examples and tracks are also separately licensed, and must not e.g. be used to train AI models without permission.
|
||||
|
||||
## How do I try out the latest features?
|
||||
|
||||
The main, stable strudel website is [strudel.cc](https://strudel.cc/). There is also [warm.strudel.cc](https://warm.strudel.cc), known as "warm strudel", which has the latest development features. You might find warm strudel has bug fixes and features that the main website doesn't, but it will often be less stable and probably not suitable for important performances.
|
||||
|
||||
Alternatively, you can run strudel locally to try out the latest features. You can find development-oriented [instructions for that here](https://codeberg.org/uzu/strudel/src/branch/main/CONTRIBUTING.md#project-setup).
|
||||
|
||||
You can see the [latest changes here](https://codeberg.org/uzu/strudel/pulls?q=&type=all&sort=recentupdate&state=closed&labels=&milestone=0&project=0&assignee=0&poster=0), as 'pull requests'.
|
||||
|
||||
## How to record or export audio?
|
||||
|
||||
Strudel is not a digital audio workstation and does not operate following the same principles shared by most traditional audio softwares. However, there are multiple ways to record the audio -- and video -- output of Strudel:
|
||||
|
||||
@@ -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>
|
||||
); */
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export function Reference() {
|
||||
return (
|
||||
<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 class="w-full flex">
|
||||
<div className="w-full flex">
|
||||
<Textbox className="w-full" placeholder="Search" value={search} onChange={setSearch} />
|
||||
</div>
|
||||
<div className="flex flex-col h-full overflow-y-auto gap-1.5 bg-background bg-opacity-50 rounded-md">
|
||||
|
||||
Reference in New Issue
Block a user