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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user