Cleaning
This commit is contained in:
394
src/App.svelte
394
src/App.svelte
@ -9,10 +9,13 @@
|
||||
import ResizablePopup from './lib/ResizablePopup.svelte';
|
||||
import AudioScope from './lib/AudioScope.svelte';
|
||||
import Spectrogram from './lib/Spectrogram.svelte';
|
||||
import { csound, csoundLogs, type LogEntry } from './lib/csound';
|
||||
import { projectManager, type CsoundProject } from './lib/project-system';
|
||||
import ConfirmDialog from './lib/ConfirmDialog.svelte';
|
||||
import InputDialog from './lib/InputDialog.svelte';
|
||||
import { createCsoundDerivedStores, type LogEntry } from './lib/csound';
|
||||
import { type CsoundProject } from './lib/project-system';
|
||||
import { uiState } from './lib/stores/uiState.svelte';
|
||||
import { DEFAULT_CSOUND_TEMPLATE } from './lib/config/templates';
|
||||
import { createAppContext, setAppContext } from './lib/contexts/app-context';
|
||||
import {
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
@ -26,95 +29,15 @@
|
||||
Activity
|
||||
} from 'lucide-svelte';
|
||||
|
||||
let sidePanelVisible = $state(true);
|
||||
let sidePanelPosition = $state<'left' | 'right' | 'bottom'>('right');
|
||||
let popupVisible = $state(false);
|
||||
let sharePopupVisible = $state(false);
|
||||
let shareUrl = $state('');
|
||||
let audioPermissionPopupVisible = $state(true);
|
||||
let scopePopupVisible = $state(false);
|
||||
let spectrogramPopupVisible = $state(false);
|
||||
const appContext = createAppContext();
|
||||
setAppContext(appContext);
|
||||
|
||||
const { csound, projectManager, editorSettings, projectEditor } = appContext;
|
||||
const csoundDerived = createCsoundDerivedStores(csound);
|
||||
|
||||
let analyserNode = $state<AnalyserNode | null>(null);
|
||||
let editorValue = $state(`<CsoundSynthesizer>
|
||||
<CsOptions>
|
||||
-odac
|
||||
</CsOptions>
|
||||
<CsInstruments>
|
||||
|
||||
sr = 44100
|
||||
ksmps = 32
|
||||
nchnls = 2
|
||||
0dbfs = 1
|
||||
|
||||
instr 1
|
||||
iFreq = p4
|
||||
iAmp = p5
|
||||
|
||||
; ADSR envelope
|
||||
kEnv madsr 0.01, 0.1, 0.6, 0.2
|
||||
|
||||
; Sine wave oscillator
|
||||
aOsc oscili iAmp * kEnv, iFreq
|
||||
|
||||
outs aOsc, aOsc
|
||||
endin
|
||||
|
||||
</CsInstruments>
|
||||
<CsScore>
|
||||
; Arpeggio: C4 E4 G4 C5
|
||||
i 1 0.0 0.5 261.63 0.3
|
||||
i 1 0.5 0.5 329.63 0.3
|
||||
i 1 1.0 0.5 392.00 0.3
|
||||
i 1 1.5 0.5 523.25 0.3
|
||||
</CsScore>
|
||||
</CsoundSynthesizer>
|
||||
`);
|
||||
let interpreterLogs = $state<LogEntry[]>([]);
|
||||
|
||||
let sidePanelRef: SidePanel;
|
||||
let editorRef: EditorWithLogs;
|
||||
let fileBrowserRef: FileBrowser;
|
||||
let currentProjectId = $state<string | null>(null);
|
||||
let hasUnsavedChanges = $state(false);
|
||||
let isNewUnsavedBuffer = $state(false);
|
||||
let pendingProject: CsoundProject | null = null;
|
||||
let showUnsavedDialog = $state(false);
|
||||
let showSaveAsDialog = $state(false);
|
||||
|
||||
const TEMPLATE_CONTENT = `<CsoundSynthesizer>
|
||||
<CsOptions>
|
||||
-odac
|
||||
</CsOptions>
|
||||
<CsInstruments>
|
||||
|
||||
sr = 44100
|
||||
ksmps = 32
|
||||
nchnls = 2
|
||||
0dbfs = 1
|
||||
|
||||
instr 1
|
||||
iFreq = p4
|
||||
iAmp = p5
|
||||
|
||||
; ADSR envelope
|
||||
kEnv madsr 0.01, 0.1, 0.6, 0.2
|
||||
|
||||
; Sine wave oscillator
|
||||
aOsc oscili iAmp * kEnv, iFreq
|
||||
|
||||
outs aOsc, aOsc
|
||||
endin
|
||||
|
||||
</CsInstruments>
|
||||
<CsScore>
|
||||
; Arpeggio: C4 E4 G4 C5
|
||||
i 1 0.0 0.5 261.63 0.3
|
||||
i 1 0.5 0.5 329.63 0.3
|
||||
i 1 1.0 0.5 392.00 0.3
|
||||
i 1 1.5 0.5 523.25 0.3
|
||||
</CsScore>
|
||||
</CsoundSynthesizer>
|
||||
`;
|
||||
|
||||
onMount(async () => {
|
||||
await projectManager.init();
|
||||
@ -124,12 +47,12 @@ i 1 1.5 0.5 523.25 0.3
|
||||
await projectManager.createProject({
|
||||
title: 'Template',
|
||||
author: 'System',
|
||||
content: TEMPLATE_CONTENT,
|
||||
content: DEFAULT_CSOUND_TEMPLATE,
|
||||
tags: []
|
||||
});
|
||||
}
|
||||
|
||||
const unsubscribe = csoundLogs.subscribe(logs => {
|
||||
const unsubscribe = csoundDerived.logs.subscribe(logs => {
|
||||
interpreterLogs = logs;
|
||||
});
|
||||
|
||||
@ -138,183 +61,115 @@ i 1 1.5 0.5 523.25 0.3
|
||||
};
|
||||
});
|
||||
|
||||
onDestroy(async () => {
|
||||
await csound.destroy();
|
||||
});
|
||||
|
||||
async function handleEnableAudio() {
|
||||
try {
|
||||
await csound.init();
|
||||
audioPermissionPopupVisible = false;
|
||||
uiState.closeAudioPermission();
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize audio:', error);
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(async () => {
|
||||
await csound.destroy();
|
||||
});
|
||||
|
||||
function toggleSidePanel() {
|
||||
sidePanelVisible = !sidePanelVisible;
|
||||
}
|
||||
|
||||
function showPopup() {
|
||||
popupVisible = true;
|
||||
}
|
||||
|
||||
function handleEditorChange(value: string) {
|
||||
editorValue = value;
|
||||
hasUnsavedChanges = true;
|
||||
projectEditor.setContent(value);
|
||||
}
|
||||
|
||||
function handleNewFile() {
|
||||
if (hasUnsavedChanges) {
|
||||
pendingProject = null;
|
||||
showUnsavedDialog = true;
|
||||
const needsConfirm = projectEditor.requestSwitch(
|
||||
() => projectEditor.createNew(DEFAULT_CSOUND_TEMPLATE),
|
||||
handleSwitchConfirm
|
||||
);
|
||||
|
||||
if (needsConfirm) {
|
||||
uiState.showUnsavedChangesDialog();
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileSelect(project: CsoundProject | null) {
|
||||
if (!project) return;
|
||||
|
||||
const needsConfirm = projectEditor.requestSwitch(
|
||||
() => projectEditor.loadProject(project),
|
||||
handleSwitchConfirm
|
||||
);
|
||||
|
||||
if (needsConfirm) {
|
||||
uiState.showUnsavedChangesDialog();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleExecute(code: string) {
|
||||
try {
|
||||
await csound.evaluate(code);
|
||||
} catch (error) {
|
||||
console.error('Execution error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (projectEditor.isNewUnsavedBuffer) {
|
||||
uiState.showSaveAsDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
createNewBuffer();
|
||||
}
|
||||
|
||||
function createNewBuffer() {
|
||||
currentProjectId = null;
|
||||
editorValue = TEMPLATE_CONTENT;
|
||||
hasUnsavedChanges = false;
|
||||
isNewUnsavedBuffer = true;
|
||||
if (editorRef) {
|
||||
editorRef.setValue(TEMPLATE_CONTENT);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFileSelect(project: CsoundProject | null) {
|
||||
if (hasUnsavedChanges) {
|
||||
pendingProject = project;
|
||||
showUnsavedDialog = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (project) {
|
||||
loadProject(project);
|
||||
}
|
||||
}
|
||||
|
||||
function loadProject(project: CsoundProject) {
|
||||
currentProjectId = project.id;
|
||||
editorValue = project.content;
|
||||
hasUnsavedChanges = false;
|
||||
isNewUnsavedBuffer = false;
|
||||
if (editorRef) {
|
||||
editorRef.setValue(project.content);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCurrentProject() {
|
||||
if (isNewUnsavedBuffer) {
|
||||
showSaveAsDialog = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentProjectId) return;
|
||||
|
||||
const result = await projectManager.updateProject({
|
||||
id: currentProjectId,
|
||||
content: editorValue
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
hasUnsavedChanges = false;
|
||||
if (fileBrowserRef) {
|
||||
fileBrowserRef.refresh();
|
||||
}
|
||||
const success = await projectEditor.save();
|
||||
if (success) {
|
||||
fileBrowserRef?.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveAs(title: string) {
|
||||
const finalTitle = title.trim() || 'Untitled';
|
||||
|
||||
const result = await projectManager.createProject({
|
||||
title: finalTitle,
|
||||
author: 'Anonymous',
|
||||
content: editorValue,
|
||||
tags: []
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
currentProjectId = result.data.id;
|
||||
hasUnsavedChanges = false;
|
||||
isNewUnsavedBuffer = false;
|
||||
if (fileBrowserRef) {
|
||||
fileBrowserRef.refresh();
|
||||
}
|
||||
const success = await projectEditor.saveAs(title);
|
||||
if (success) {
|
||||
fileBrowserRef?.refresh();
|
||||
uiState.hideSaveAsDialog();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMetadataUpdate(projectId: string, updates: { title?: string; author?: string }) {
|
||||
const result = await projectManager.updateProject({
|
||||
id: projectId,
|
||||
...updates
|
||||
});
|
||||
|
||||
if (result.success && fileBrowserRef) {
|
||||
fileBrowserRef.refresh();
|
||||
const success = await projectEditor.updateMetadata(updates);
|
||||
if (success) {
|
||||
fileBrowserRef?.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSaveAndSwitch() {
|
||||
saveCurrentProject().then(() => {
|
||||
if (pendingProject) {
|
||||
loadProject(pendingProject);
|
||||
function handleSwitchConfirm(action: 'save' | 'discard') {
|
||||
if (action === 'save') {
|
||||
if (projectEditor.isNewUnsavedBuffer) {
|
||||
uiState.hideUnsavedChangesDialog();
|
||||
uiState.showSaveAsDialog();
|
||||
} else {
|
||||
createNewBuffer();
|
||||
projectEditor.handleSaveAndSwitch().then(() => {
|
||||
fileBrowserRef?.refresh();
|
||||
uiState.hideUnsavedChangesDialog();
|
||||
});
|
||||
}
|
||||
pendingProject = null;
|
||||
});
|
||||
}
|
||||
|
||||
function handleDiscardAndSwitch() {
|
||||
if (pendingProject) {
|
||||
loadProject(pendingProject);
|
||||
} else {
|
||||
createNewBuffer();
|
||||
}
|
||||
pendingProject = null;
|
||||
}
|
||||
|
||||
function cyclePanelPosition() {
|
||||
if (sidePanelPosition === 'right') {
|
||||
sidePanelPosition = 'left';
|
||||
} else if (sidePanelPosition === 'left') {
|
||||
sidePanelPosition = 'bottom';
|
||||
} else {
|
||||
sidePanelPosition = 'right';
|
||||
projectEditor.handleDiscardAndSwitch();
|
||||
uiState.hideUnsavedChangesDialog();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleShare() {
|
||||
if (!currentProjectId) return;
|
||||
if (!projectEditor.currentProjectId) return;
|
||||
|
||||
const result = await projectManager.exportProjectToUrl(currentProjectId);
|
||||
const result = await projectManager.exportProjectToUrl(projectEditor.currentProjectId);
|
||||
if (result.success) {
|
||||
shareUrl = result.data;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(shareUrl);
|
||||
await navigator.clipboard.writeText(result.data);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy to clipboard:', err);
|
||||
}
|
||||
|
||||
sharePopupVisible = true;
|
||||
uiState.showShare(result.data);
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenScope() {
|
||||
scopePopupVisible = true;
|
||||
}
|
||||
|
||||
function handleOpenSpectrogram() {
|
||||
spectrogramPopupVisible = true;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (scopePopupVisible || spectrogramPopupVisible) {
|
||||
if (uiState.scopePopupVisible || uiState.spectrogramPopupVisible) {
|
||||
const interval = setInterval(() => {
|
||||
analyserNode = csound.getAnalyserNode();
|
||||
}, 100);
|
||||
@ -344,10 +199,11 @@ i 1 1.5 0.5 523.25 0.3
|
||||
{#snippet filesTabContent()}
|
||||
<FileBrowser
|
||||
bind:this={fileBrowserRef}
|
||||
{projectManager}
|
||||
onFileSelect={handleFileSelect}
|
||||
onNewFile={handleNewFile}
|
||||
onMetadataUpdate={handleMetadataUpdate}
|
||||
selectedProjectId={currentProjectId}
|
||||
selectedProjectId={projectEditor.currentProjectId}
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
@ -356,53 +212,53 @@ i 1 1.5 0.5 523.25 0.3
|
||||
{#snippet leftActions()}
|
||||
<button
|
||||
class="icon-button"
|
||||
onclick={saveCurrentProject}
|
||||
disabled={!hasUnsavedChanges}
|
||||
title="Save {hasUnsavedChanges ? '(unsaved changes)' : ''}"
|
||||
class:has-changes={hasUnsavedChanges}
|
||||
onclick={handleSave}
|
||||
disabled={!projectEditor.hasUnsavedChanges}
|
||||
title="Save {projectEditor.hasUnsavedChanges ? '(unsaved changes)' : ''}"
|
||||
class:has-changes={projectEditor.hasUnsavedChanges}
|
||||
>
|
||||
<Save size={18} />
|
||||
</button>
|
||||
{/snippet}
|
||||
<button onclick={toggleSidePanel} class="icon-button">
|
||||
{#if sidePanelVisible}
|
||||
{#if sidePanelPosition === 'left'}
|
||||
<button onclick={() => uiState.toggleSidePanel()} class="icon-button">
|
||||
{#if uiState.sidePanelVisible}
|
||||
{#if uiState.sidePanelPosition === 'left'}
|
||||
<PanelLeftClose size={18} />
|
||||
{:else if sidePanelPosition === 'right'}
|
||||
{:else if uiState.sidePanelPosition === 'right'}
|
||||
<PanelRightClose size={18} />
|
||||
{:else}
|
||||
<PanelBottomClose size={18} />
|
||||
{/if}
|
||||
{:else}
|
||||
{#if sidePanelPosition === 'left'}
|
||||
{#if uiState.sidePanelPosition === 'left'}
|
||||
<PanelLeftOpen size={18} />
|
||||
{:else if sidePanelPosition === 'right'}
|
||||
{:else if uiState.sidePanelPosition === 'right'}
|
||||
<PanelRightOpen size={18} />
|
||||
{:else}
|
||||
<PanelBottomOpen size={18} />
|
||||
{/if}
|
||||
{/if}
|
||||
</button>
|
||||
<button onclick={cyclePanelPosition} class="icon-button" title="Change panel position">
|
||||
<button onclick={() => uiState.cyclePanelPosition()} class="icon-button" title="Change panel position">
|
||||
<LayoutGrid size={18} />
|
||||
</button>
|
||||
<button
|
||||
onclick={handleShare}
|
||||
class="icon-button"
|
||||
disabled={!currentProjectId}
|
||||
disabled={!projectEditor.currentProjectId}
|
||||
title="Share project"
|
||||
>
|
||||
<Share2 size={18} />
|
||||
</button>
|
||||
<button
|
||||
onclick={handleOpenScope}
|
||||
onclick={() => uiState.openScope()}
|
||||
class="icon-button"
|
||||
title="Open audio scope"
|
||||
>
|
||||
<Activity size={18} />
|
||||
</button>
|
||||
<button
|
||||
onclick={handleOpenSpectrogram}
|
||||
onclick={() => uiState.openSpectrogram()}
|
||||
class="icon-button"
|
||||
title="Open spectrogram"
|
||||
>
|
||||
@ -416,40 +272,38 @@ i 1 1.5 0.5 523.25 0.3
|
||||
</button>
|
||||
</TopBar>
|
||||
|
||||
<div class="main-content" class:panel-bottom={sidePanelPosition === 'bottom'}>
|
||||
{#if sidePanelPosition === 'left'}
|
||||
<div class="main-content" class:panel-bottom={uiState.sidePanelPosition === 'bottom'}>
|
||||
{#if uiState.sidePanelPosition === 'left'}
|
||||
<SidePanel
|
||||
bind:this={sidePanelRef}
|
||||
bind:visible={sidePanelVisible}
|
||||
bind:position={sidePanelPosition}
|
||||
bind:visible={uiState.sidePanelVisible}
|
||||
bind:position={uiState.sidePanelPosition}
|
||||
tabs={panelTabs}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="editor-area">
|
||||
<EditorWithLogs
|
||||
bind:this={editorRef}
|
||||
initialValue={editorValue}
|
||||
value={projectEditor.content}
|
||||
language="javascript"
|
||||
onChange={handleEditorChange}
|
||||
onExecute={handleExecute}
|
||||
logs={interpreterLogs}
|
||||
{editorSettings}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if sidePanelPosition === 'right'}
|
||||
{#if uiState.sidePanelPosition === 'right'}
|
||||
<SidePanel
|
||||
bind:this={sidePanelRef}
|
||||
bind:visible={sidePanelVisible}
|
||||
bind:position={sidePanelPosition}
|
||||
bind:visible={uiState.sidePanelVisible}
|
||||
bind:position={uiState.sidePanelPosition}
|
||||
tabs={panelTabs}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if sidePanelPosition === 'bottom'}
|
||||
{#if uiState.sidePanelPosition === 'bottom'}
|
||||
<SidePanel
|
||||
bind:this={sidePanelRef}
|
||||
bind:visible={sidePanelVisible}
|
||||
bind:position={sidePanelPosition}
|
||||
bind:visible={uiState.sidePanelVisible}
|
||||
bind:position={uiState.sidePanelPosition}
|
||||
initialWidth={200}
|
||||
minWidth={100}
|
||||
maxWidth={400}
|
||||
@ -458,21 +312,8 @@ i 1 1.5 0.5 523.25 0.3
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Popup
|
||||
bind:visible={popupVisible}
|
||||
title="Example Popup"
|
||||
x={200}
|
||||
y={150}
|
||||
width={500}
|
||||
height={400}
|
||||
>
|
||||
<h3>This is a popup!</h3>
|
||||
<p>You can drag it around by the header.</p>
|
||||
<p>It stays on top of everything else.</p>
|
||||
</Popup>
|
||||
|
||||
<ResizablePopup
|
||||
bind:visible={sharePopupVisible}
|
||||
bind:visible={uiState.sharePopupVisible}
|
||||
title="Share Project"
|
||||
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 300 : 300)}
|
||||
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 100 : 200)}
|
||||
@ -488,7 +329,7 @@ i 1 1.5 0.5 523.25 0.3
|
||||
<input
|
||||
type="text"
|
||||
readonly
|
||||
value={shareUrl}
|
||||
value={uiState.shareUrl}
|
||||
class="share-url-input"
|
||||
onclick={(e) => e.currentTarget.select()}
|
||||
/>
|
||||
@ -499,7 +340,7 @@ i 1 1.5 0.5 523.25 0.3
|
||||
</ResizablePopup>
|
||||
|
||||
<ResizablePopup
|
||||
visible={audioPermissionPopupVisible}
|
||||
visible={uiState.audioPermissionPopupVisible}
|
||||
title="Audio Permission Required"
|
||||
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 250 : 250)}
|
||||
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 125 : 200)}
|
||||
@ -522,7 +363,7 @@ i 1 1.5 0.5 523.25 0.3
|
||||
</ResizablePopup>
|
||||
|
||||
<ResizablePopup
|
||||
bind:visible={scopePopupVisible}
|
||||
bind:visible={uiState.scopePopupVisible}
|
||||
title="Audio Scope"
|
||||
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 400 : 100)}
|
||||
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 300 : 100)}
|
||||
@ -538,7 +379,7 @@ i 1 1.5 0.5 523.25 0.3
|
||||
</ResizablePopup>
|
||||
|
||||
<ResizablePopup
|
||||
bind:visible={spectrogramPopupVisible}
|
||||
bind:visible={uiState.spectrogramPopupVisible}
|
||||
title="Spectrogram"
|
||||
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 400 : 150)}
|
||||
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 300 : 150)}
|
||||
@ -554,17 +395,17 @@ i 1 1.5 0.5 523.25 0.3
|
||||
</ResizablePopup>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:visible={showUnsavedDialog}
|
||||
bind:visible={uiState.unsavedChangesDialogVisible}
|
||||
title="Unsaved Changes"
|
||||
message="You have unsaved changes. What would you like to do?"
|
||||
confirmLabel="Save"
|
||||
cancelLabel="Discard"
|
||||
onConfirm={handleSaveAndSwitch}
|
||||
onCancel={handleDiscardAndSwitch}
|
||||
onConfirm={() => handleSwitchConfirm('save')}
|
||||
onCancel={() => handleSwitchConfirm('discard')}
|
||||
/>
|
||||
|
||||
<InputDialog
|
||||
bind:visible={showSaveAsDialog}
|
||||
bind:visible={uiState.saveAsDialogVisible}
|
||||
title="Save As"
|
||||
label="File name"
|
||||
placeholder="Untitled"
|
||||
@ -703,3 +544,4 @@ i 1 1.5 0.5 523.25 0.3
|
||||
background-color: #424ab8;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@ -13,19 +13,22 @@
|
||||
import { css } from '@codemirror/lang-css';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { vim } from '@replit/codemirror-vim';
|
||||
import { editorSettings } from './stores/editorSettings';
|
||||
import { csound } from './csound';
|
||||
import type { EditorSettingsStore } from './stores/editorSettings';
|
||||
|
||||
interface Props {
|
||||
initialValue?: string;
|
||||
value: string;
|
||||
language?: 'javascript' | 'html' | 'css';
|
||||
onChange?: (value: string) => void;
|
||||
onExecute?: (code: string) => void;
|
||||
editorSettings: EditorSettingsStore;
|
||||
}
|
||||
|
||||
let {
|
||||
initialValue = '',
|
||||
value = '',
|
||||
language = 'javascript',
|
||||
onChange
|
||||
onChange,
|
||||
onExecute,
|
||||
editorSettings
|
||||
}: Props = $props();
|
||||
|
||||
let editorContainer: HTMLDivElement;
|
||||
@ -45,10 +48,10 @@
|
||||
{
|
||||
key: 'Mod-e',
|
||||
run: (view) => {
|
||||
const code = view.state.doc.toString();
|
||||
csound.evaluate(code).catch(err => {
|
||||
console.error('Evaluation error:', err);
|
||||
});
|
||||
if (onExecute) {
|
||||
const code = view.state.doc.toString();
|
||||
onExecute(code);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -84,7 +87,7 @@
|
||||
];
|
||||
|
||||
editorView = new EditorView({
|
||||
doc: initialValue,
|
||||
doc: value,
|
||||
extensions: [
|
||||
...baseExtensions,
|
||||
languageExtensions[language],
|
||||
@ -134,17 +137,13 @@
|
||||
}
|
||||
});
|
||||
|
||||
export function getValue(): string {
|
||||
return editorView?.state.doc.toString() || '';
|
||||
}
|
||||
|
||||
export function setValue(value: string): void {
|
||||
if (editorView) {
|
||||
$effect(() => {
|
||||
if (editorView && value !== editorView.state.doc.toString()) {
|
||||
editorView.dispatch({
|
||||
changes: { from: 0, to: editorView.state.doc.length, insert: value }
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="editor-wrapper">
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { editorSettings } from './stores/editorSettings';
|
||||
import { getAppContext } from './contexts/app-context';
|
||||
|
||||
const { editorSettings } = getAppContext();
|
||||
|
||||
let settings = $state($editorSettings);
|
||||
|
||||
|
||||
@ -2,22 +2,26 @@
|
||||
import { onMount } from 'svelte';
|
||||
import Editor from './Editor.svelte';
|
||||
import LogPanel from './LogPanel.svelte';
|
||||
import type { EditorSettingsStore } from './stores/editorSettings';
|
||||
|
||||
interface Props {
|
||||
initialValue?: string;
|
||||
value: string;
|
||||
language?: 'javascript' | 'html' | 'css';
|
||||
onChange?: (value: string) => void;
|
||||
onExecute?: (code: string) => void;
|
||||
logs?: string[];
|
||||
editorSettings: EditorSettingsStore;
|
||||
}
|
||||
|
||||
let {
|
||||
initialValue = '',
|
||||
value = '',
|
||||
language = 'javascript',
|
||||
onChange,
|
||||
logs = []
|
||||
onExecute,
|
||||
logs = [],
|
||||
editorSettings
|
||||
}: Props = $props();
|
||||
|
||||
let editorRef: Editor;
|
||||
let logPanelRef: LogPanel;
|
||||
|
||||
let editorHeight = $state(70);
|
||||
@ -60,22 +64,16 @@
|
||||
};
|
||||
});
|
||||
|
||||
export function getValue(): string {
|
||||
return editorRef?.getValue() || '';
|
||||
}
|
||||
|
||||
export function setValue(value: string): void {
|
||||
editorRef?.setValue(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="editor-with-logs">
|
||||
<div class="editor-section" style="height: {editorHeight}%;">
|
||||
<Editor
|
||||
bind:this={editorRef}
|
||||
{initialValue}
|
||||
{value}
|
||||
{language}
|
||||
{onChange}
|
||||
{onExecute}
|
||||
{editorSettings}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,18 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { File, Plus, Trash2 } from 'lucide-svelte';
|
||||
import { projectManager, type CsoundProject } from './project-system';
|
||||
import type { CsoundProject, ProjectManager } from './project-system';
|
||||
import ConfirmDialog from './ConfirmDialog.svelte';
|
||||
import InputDialog from './InputDialog.svelte';
|
||||
|
||||
interface Props {
|
||||
projectManager: ProjectManager;
|
||||
onFileSelect?: (project: CsoundProject | null) => void;
|
||||
onNewFile?: () => void;
|
||||
onMetadataUpdate?: (projectId: string, updates: { title?: string; author?: string }) => void;
|
||||
selectedProjectId?: string | null;
|
||||
}
|
||||
|
||||
let { onFileSelect, onNewFile, onMetadataUpdate, selectedProjectId = null }: Props = $props();
|
||||
let { projectManager, onFileSelect, onNewFile, onMetadataUpdate, selectedProjectId = null }: Props = $props();
|
||||
|
||||
let projects = $state<CsoundProject[]>([]);
|
||||
let loading = $state(true);
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Copy, Trash2, Search, Pause, Play } from 'lucide-svelte';
|
||||
import { csound } from './csound';
|
||||
import { getAppContext } from './contexts/app-context';
|
||||
import type { LogEntry } from './csound';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
logs?: LogEntry[];
|
||||
@ -10,6 +10,8 @@
|
||||
|
||||
let { logs = [] }: Props = $props();
|
||||
|
||||
const { csound } = getAppContext();
|
||||
|
||||
let searchQuery = $state('');
|
||||
let autoFollow = $state(true);
|
||||
let searchVisible = $state(false);
|
||||
|
||||
33
src/lib/config/templates.ts
Normal file
33
src/lib/config/templates.ts
Normal file
@ -0,0 +1,33 @@
|
||||
export const DEFAULT_CSOUND_TEMPLATE = `<CsoundSynthesizer>
|
||||
<CsOptions>
|
||||
</CsOptions>
|
||||
<CsInstruments>
|
||||
|
||||
sr = 44100
|
||||
ksmps = 32
|
||||
nchnls = 2
|
||||
0dbfs = 1
|
||||
|
||||
instr 1
|
||||
iFreq = p4
|
||||
iAmp = p5
|
||||
|
||||
; ADSR envelope
|
||||
kEnv madsr 0.01, 0.1, 0.6, 0.2
|
||||
|
||||
; Sine wave oscillator
|
||||
aOsc oscili iAmp * kEnv, iFreq
|
||||
|
||||
outs aOsc, aOsc
|
||||
endin
|
||||
|
||||
</CsInstruments>
|
||||
<CsScore>
|
||||
; Arpeggio: C4 E4 G4 C5
|
||||
i 1 0.0 0.5 261.63 0.3
|
||||
i 1 0.5 0.5 329.63 0.3
|
||||
i 1 1.0 0.5 392.00 0.3
|
||||
i 1 1.5 0.5 523.25 0.3
|
||||
</CsScore>
|
||||
</CsoundSynthesizer>
|
||||
`;
|
||||
38
src/lib/contexts/app-context.ts
Normal file
38
src/lib/contexts/app-context.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import { ProjectManager } from '../project-system/project-manager';
|
||||
import { createCsoundStore } from '../csound/store';
|
||||
import { createEditorSettingsStore } from '../stores/editorSettings';
|
||||
import { ProjectEditor } from '../stores/projectEditor.svelte';
|
||||
import type { CsoundStore } from '../csound/store';
|
||||
import type { EditorSettingsStore } from '../stores/editorSettings';
|
||||
|
||||
export interface AppContext {
|
||||
projectManager: ProjectManager;
|
||||
csound: CsoundStore;
|
||||
editorSettings: EditorSettingsStore;
|
||||
projectEditor: ProjectEditor;
|
||||
}
|
||||
|
||||
const APP_CONTEXT_KEY = Symbol('app-context');
|
||||
|
||||
export function createAppContext(): AppContext {
|
||||
const projectManager = new ProjectManager();
|
||||
return {
|
||||
projectManager,
|
||||
csound: createCsoundStore(),
|
||||
editorSettings: createEditorSettingsStore(),
|
||||
projectEditor: new ProjectEditor(projectManager)
|
||||
};
|
||||
}
|
||||
|
||||
export function setAppContext(context: AppContext): void {
|
||||
setContext<AppContext>(APP_CONTEXT_KEY, context);
|
||||
}
|
||||
|
||||
export function getAppContext(): AppContext {
|
||||
const context = getContext<AppContext>(APP_CONTEXT_KEY);
|
||||
if (!context) {
|
||||
throw new Error('AppContext not found. Did you forget to call setAppContext?');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
export { CsoundEngine } from './engine';
|
||||
export type { CsoundEngineOptions } from './engine';
|
||||
export { csound, csoundLogs, csoundInitialized, csoundRunning } from './store';
|
||||
export type { LogEntry } from './store';
|
||||
export { createCsoundStore, createCsoundDerivedStores } from './store';
|
||||
export type { LogEntry, CsoundStore } from './store';
|
||||
|
||||
@ -13,7 +13,18 @@ interface CsoundState {
|
||||
logs: LogEntry[];
|
||||
}
|
||||
|
||||
function createCsoundStore() {
|
||||
export interface CsoundStore {
|
||||
subscribe: (run: (value: CsoundState) => void) => () => void;
|
||||
init: () => Promise<void>;
|
||||
evaluate: (code: string) => Promise<void>;
|
||||
stop: () => Promise<void>;
|
||||
clearLogs: () => void;
|
||||
getAudioContext: () => AudioContext | null;
|
||||
getAnalyserNode: () => AnalyserNode | null;
|
||||
destroy: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function createCsoundStore(): CsoundStore {
|
||||
const initialState: CsoundState = {
|
||||
initialized: false,
|
||||
running: false,
|
||||
@ -116,8 +127,10 @@ function createCsoundStore() {
|
||||
};
|
||||
}
|
||||
|
||||
export const csound = createCsoundStore();
|
||||
|
||||
export const csoundLogs = derived(csound, $csound => $csound.logs);
|
||||
export const csoundInitialized = derived(csound, $csound => $csound.initialized);
|
||||
export const csoundRunning = derived(csound, $csound => $csound.running);
|
||||
export function createCsoundDerivedStores(csound: CsoundStore) {
|
||||
return {
|
||||
logs: derived(csound, $csound => $csound.logs),
|
||||
initialized: derived(csound, $csound => $csound.initialized),
|
||||
running: derived(csound, $csound => $csound.running)
|
||||
};
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ export type {
|
||||
} from './types';
|
||||
|
||||
// Export main API
|
||||
export { ProjectManager, projectManager } from './project-manager';
|
||||
export { ProjectManager } from './project-manager';
|
||||
|
||||
// Export database (for advanced usage)
|
||||
export { projectDb } from './db';
|
||||
|
||||
@ -338,6 +338,3 @@ export class ProjectManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const projectManager = new ProjectManager();
|
||||
|
||||
@ -18,8 +18,15 @@ const defaultSettings: EditorSettings = {
|
||||
vimMode: false
|
||||
};
|
||||
|
||||
function createEditorSettings() {
|
||||
const stored = localStorage.getItem('editorSettings');
|
||||
export interface EditorSettingsStore {
|
||||
subscribe: (run: (value: EditorSettings) => void) => () => void;
|
||||
set: (value: EditorSettings) => void;
|
||||
update: (updater: (value: EditorSettings) => EditorSettings) => void;
|
||||
updatePartial: (partial: Partial<EditorSettings>) => void;
|
||||
}
|
||||
|
||||
export function createEditorSettingsStore(): EditorSettingsStore {
|
||||
const stored = typeof localStorage !== 'undefined' ? localStorage.getItem('editorSettings') : null;
|
||||
const initial = stored ? { ...defaultSettings, ...JSON.parse(stored) } : defaultSettings;
|
||||
|
||||
const { subscribe, set, update } = writable<EditorSettings>(initial);
|
||||
@ -27,24 +34,28 @@ function createEditorSettings() {
|
||||
return {
|
||||
subscribe,
|
||||
set: (value: EditorSettings) => {
|
||||
localStorage.setItem('editorSettings', JSON.stringify(value));
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('editorSettings', JSON.stringify(value));
|
||||
}
|
||||
set(value);
|
||||
},
|
||||
update: (updater: (value: EditorSettings) => EditorSettings) => {
|
||||
update((current) => {
|
||||
const newValue = updater(current);
|
||||
localStorage.setItem('editorSettings', JSON.stringify(newValue));
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('editorSettings', JSON.stringify(newValue));
|
||||
}
|
||||
return newValue;
|
||||
});
|
||||
},
|
||||
updatePartial: (partial: Partial<EditorSettings>) => {
|
||||
update((current) => {
|
||||
const newValue = { ...current, ...partial };
|
||||
localStorage.setItem('editorSettings', JSON.stringify(newValue));
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('editorSettings', JSON.stringify(newValue));
|
||||
}
|
||||
return newValue;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const editorSettings = createEditorSettings();
|
||||
|
||||
154
src/lib/stores/projectEditor.svelte.ts
Normal file
154
src/lib/stores/projectEditor.svelte.ts
Normal file
@ -0,0 +1,154 @@
|
||||
import type { CsoundProject, ProjectManager } from '../project-system';
|
||||
|
||||
interface ProjectEditorState {
|
||||
currentProject: CsoundProject | null;
|
||||
content: string;
|
||||
hasUnsavedChanges: boolean;
|
||||
isNewUnsavedBuffer: boolean;
|
||||
}
|
||||
|
||||
export class ProjectEditor {
|
||||
private projectManager: ProjectManager;
|
||||
|
||||
private state = $state<ProjectEditorState>({
|
||||
currentProject: null,
|
||||
content: '',
|
||||
hasUnsavedChanges: false,
|
||||
isNewUnsavedBuffer: false
|
||||
});
|
||||
|
||||
private pendingAction: (() => void) | null = null;
|
||||
private confirmCallback: ((action: 'save' | 'discard') => void) | null = null;
|
||||
|
||||
constructor(projectManager: ProjectManager) {
|
||||
this.projectManager = projectManager;
|
||||
}
|
||||
|
||||
get currentProject() {
|
||||
return this.state.currentProject;
|
||||
}
|
||||
|
||||
get content() {
|
||||
return this.state.content;
|
||||
}
|
||||
|
||||
get hasUnsavedChanges() {
|
||||
return this.state.hasUnsavedChanges;
|
||||
}
|
||||
|
||||
get isNewUnsavedBuffer() {
|
||||
return this.state.isNewUnsavedBuffer;
|
||||
}
|
||||
|
||||
get currentProjectId() {
|
||||
return this.state.currentProject?.id ?? null;
|
||||
}
|
||||
|
||||
setContent(content: string) {
|
||||
this.state.content = content;
|
||||
this.state.hasUnsavedChanges = true;
|
||||
}
|
||||
|
||||
loadProject(project: CsoundProject) {
|
||||
this.state.currentProject = project;
|
||||
this.state.content = project.content;
|
||||
this.state.hasUnsavedChanges = false;
|
||||
this.state.isNewUnsavedBuffer = false;
|
||||
}
|
||||
|
||||
createNew(template: string) {
|
||||
this.state.currentProject = null;
|
||||
this.state.content = template;
|
||||
this.state.hasUnsavedChanges = false;
|
||||
this.state.isNewUnsavedBuffer = true;
|
||||
}
|
||||
|
||||
async save(): Promise<boolean> {
|
||||
if (this.state.isNewUnsavedBuffer) {
|
||||
throw new Error('Cannot save new buffer without title. Use saveAs instead.');
|
||||
}
|
||||
|
||||
if (!this.state.currentProject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = await this.projectManager.updateProject({
|
||||
id: this.state.currentProject.id,
|
||||
content: this.state.content
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
this.state.hasUnsavedChanges = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async saveAs(title: string, author: string = 'Anonymous'): Promise<boolean> {
|
||||
const finalTitle = title.trim() || 'Untitled';
|
||||
|
||||
const result = await this.projectManager.createProject({
|
||||
title: finalTitle,
|
||||
author,
|
||||
content: this.state.content,
|
||||
tags: []
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
this.state.currentProject = result.data;
|
||||
this.state.hasUnsavedChanges = false;
|
||||
this.state.isNewUnsavedBuffer = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async updateMetadata(updates: { title?: string; author?: string }): Promise<boolean> {
|
||||
if (!this.state.currentProject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = await this.projectManager.updateProject({
|
||||
id: this.state.currentProject.id,
|
||||
...updates
|
||||
});
|
||||
|
||||
if (result.success && result.data) {
|
||||
this.state.currentProject = result.data;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
requestSwitch(action: () => void, onConfirm: (action: 'save' | 'discard') => void) {
|
||||
if (!this.state.hasUnsavedChanges) {
|
||||
action();
|
||||
return false;
|
||||
}
|
||||
|
||||
this.pendingAction = action;
|
||||
this.confirmCallback = onConfirm;
|
||||
return true;
|
||||
}
|
||||
|
||||
async handleSaveAndSwitch(): Promise<void> {
|
||||
if (this.state.isNewUnsavedBuffer) {
|
||||
this.confirmCallback?.('save');
|
||||
return;
|
||||
}
|
||||
|
||||
await this.save();
|
||||
this.pendingAction?.();
|
||||
this.pendingAction = null;
|
||||
this.confirmCallback = null;
|
||||
}
|
||||
|
||||
handleDiscardAndSwitch(): void {
|
||||
this.pendingAction?.();
|
||||
this.pendingAction = null;
|
||||
this.confirmCallback = null;
|
||||
}
|
||||
}
|
||||
64
src/lib/stores/uiState.svelte.ts
Normal file
64
src/lib/stores/uiState.svelte.ts
Normal file
@ -0,0 +1,64 @@
|
||||
type PanelPosition = 'left' | 'right' | 'bottom';
|
||||
|
||||
class UIState {
|
||||
sidePanelVisible = $state(true);
|
||||
sidePanelPosition = $state<PanelPosition>('right');
|
||||
|
||||
scopePopupVisible = $state(false);
|
||||
spectrogramPopupVisible = $state(false);
|
||||
sharePopupVisible = $state(false);
|
||||
audioPermissionPopupVisible = $state(true);
|
||||
unsavedChangesDialogVisible = $state(false);
|
||||
saveAsDialogVisible = $state(false);
|
||||
|
||||
shareUrl = $state('');
|
||||
|
||||
toggleSidePanel() {
|
||||
this.sidePanelVisible = !this.sidePanelVisible;
|
||||
}
|
||||
|
||||
cyclePanelPosition() {
|
||||
if (this.sidePanelPosition === 'right') {
|
||||
this.sidePanelPosition = 'left';
|
||||
} else if (this.sidePanelPosition === 'left') {
|
||||
this.sidePanelPosition = 'bottom';
|
||||
} else {
|
||||
this.sidePanelPosition = 'right';
|
||||
}
|
||||
}
|
||||
|
||||
openScope() {
|
||||
this.scopePopupVisible = true;
|
||||
}
|
||||
|
||||
openSpectrogram() {
|
||||
this.spectrogramPopupVisible = true;
|
||||
}
|
||||
|
||||
showShare(url: string) {
|
||||
this.shareUrl = url;
|
||||
this.sharePopupVisible = true;
|
||||
}
|
||||
|
||||
closeAudioPermission() {
|
||||
this.audioPermissionPopupVisible = false;
|
||||
}
|
||||
|
||||
showUnsavedChangesDialog() {
|
||||
this.unsavedChangesDialogVisible = true;
|
||||
}
|
||||
|
||||
hideUnsavedChangesDialog() {
|
||||
this.unsavedChangesDialogVisible = false;
|
||||
}
|
||||
|
||||
showSaveAsDialog() {
|
||||
this.saveAsDialogVisible = true;
|
||||
}
|
||||
|
||||
hideSaveAsDialog() {
|
||||
this.saveAsDialogVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
export const uiState = new UIState();
|
||||
Reference in New Issue
Block a user