This commit is contained in:
2025-10-15 01:23:05 +02:00
parent 8ac5ac0770
commit e492c03f15
15 changed files with 484 additions and 330 deletions

View File

@ -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>