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 ResizablePopup from './lib/ResizablePopup.svelte';
|
||||||
import AudioScope from './lib/AudioScope.svelte';
|
import AudioScope from './lib/AudioScope.svelte';
|
||||||
import Spectrogram from './lib/Spectrogram.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 ConfirmDialog from './lib/ConfirmDialog.svelte';
|
||||||
import InputDialog from './lib/InputDialog.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 {
|
import {
|
||||||
PanelLeftClose,
|
PanelLeftClose,
|
||||||
PanelLeftOpen,
|
PanelLeftOpen,
|
||||||
@ -26,95 +29,15 @@
|
|||||||
Activity
|
Activity
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
|
|
||||||
let sidePanelVisible = $state(true);
|
const appContext = createAppContext();
|
||||||
let sidePanelPosition = $state<'left' | 'right' | 'bottom'>('right');
|
setAppContext(appContext);
|
||||||
let popupVisible = $state(false);
|
|
||||||
let sharePopupVisible = $state(false);
|
const { csound, projectManager, editorSettings, projectEditor } = appContext;
|
||||||
let shareUrl = $state('');
|
const csoundDerived = createCsoundDerivedStores(csound);
|
||||||
let audioPermissionPopupVisible = $state(true);
|
|
||||||
let scopePopupVisible = $state(false);
|
|
||||||
let spectrogramPopupVisible = $state(false);
|
|
||||||
let analyserNode = $state<AnalyserNode | null>(null);
|
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 interpreterLogs = $state<LogEntry[]>([]);
|
||||||
|
|
||||||
let sidePanelRef: SidePanel;
|
|
||||||
let editorRef: EditorWithLogs;
|
|
||||||
let fileBrowserRef: FileBrowser;
|
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 () => {
|
onMount(async () => {
|
||||||
await projectManager.init();
|
await projectManager.init();
|
||||||
@ -124,12 +47,12 @@ i 1 1.5 0.5 523.25 0.3
|
|||||||
await projectManager.createProject({
|
await projectManager.createProject({
|
||||||
title: 'Template',
|
title: 'Template',
|
||||||
author: 'System',
|
author: 'System',
|
||||||
content: TEMPLATE_CONTENT,
|
content: DEFAULT_CSOUND_TEMPLATE,
|
||||||
tags: []
|
tags: []
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const unsubscribe = csoundLogs.subscribe(logs => {
|
const unsubscribe = csoundDerived.logs.subscribe(logs => {
|
||||||
interpreterLogs = 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() {
|
async function handleEnableAudio() {
|
||||||
try {
|
try {
|
||||||
await csound.init();
|
await csound.init();
|
||||||
audioPermissionPopupVisible = false;
|
uiState.closeAudioPermission();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize audio:', 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) {
|
function handleEditorChange(value: string) {
|
||||||
editorValue = value;
|
projectEditor.setContent(value);
|
||||||
hasUnsavedChanges = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNewFile() {
|
function handleNewFile() {
|
||||||
if (hasUnsavedChanges) {
|
const needsConfirm = projectEditor.requestSwitch(
|
||||||
pendingProject = null;
|
() => projectEditor.createNew(DEFAULT_CSOUND_TEMPLATE),
|
||||||
showUnsavedDialog = true;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
createNewBuffer();
|
const success = await projectEditor.save();
|
||||||
}
|
if (success) {
|
||||||
|
fileBrowserRef?.refresh();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSaveAs(title: string) {
|
async function handleSaveAs(title: string) {
|
||||||
const finalTitle = title.trim() || 'Untitled';
|
const success = await projectEditor.saveAs(title);
|
||||||
|
if (success) {
|
||||||
const result = await projectManager.createProject({
|
fileBrowserRef?.refresh();
|
||||||
title: finalTitle,
|
uiState.hideSaveAsDialog();
|
||||||
author: 'Anonymous',
|
|
||||||
content: editorValue,
|
|
||||||
tags: []
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
currentProjectId = result.data.id;
|
|
||||||
hasUnsavedChanges = false;
|
|
||||||
isNewUnsavedBuffer = false;
|
|
||||||
if (fileBrowserRef) {
|
|
||||||
fileBrowserRef.refresh();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMetadataUpdate(projectId: string, updates: { title?: string; author?: string }) {
|
async function handleMetadataUpdate(projectId: string, updates: { title?: string; author?: string }) {
|
||||||
const result = await projectManager.updateProject({
|
const success = await projectEditor.updateMetadata(updates);
|
||||||
id: projectId,
|
if (success) {
|
||||||
...updates
|
fileBrowserRef?.refresh();
|
||||||
});
|
|
||||||
|
|
||||||
if (result.success && fileBrowserRef) {
|
|
||||||
fileBrowserRef.refresh();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSaveAndSwitch() {
|
function handleSwitchConfirm(action: 'save' | 'discard') {
|
||||||
saveCurrentProject().then(() => {
|
if (action === 'save') {
|
||||||
if (pendingProject) {
|
if (projectEditor.isNewUnsavedBuffer) {
|
||||||
loadProject(pendingProject);
|
uiState.hideUnsavedChangesDialog();
|
||||||
|
uiState.showSaveAsDialog();
|
||||||
} else {
|
} else {
|
||||||
createNewBuffer();
|
projectEditor.handleSaveAndSwitch().then(() => {
|
||||||
|
fileBrowserRef?.refresh();
|
||||||
|
uiState.hideUnsavedChangesDialog();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
pendingProject = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDiscardAndSwitch() {
|
|
||||||
if (pendingProject) {
|
|
||||||
loadProject(pendingProject);
|
|
||||||
} else {
|
} else {
|
||||||
createNewBuffer();
|
projectEditor.handleDiscardAndSwitch();
|
||||||
}
|
uiState.hideUnsavedChangesDialog();
|
||||||
pendingProject = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cyclePanelPosition() {
|
|
||||||
if (sidePanelPosition === 'right') {
|
|
||||||
sidePanelPosition = 'left';
|
|
||||||
} else if (sidePanelPosition === 'left') {
|
|
||||||
sidePanelPosition = 'bottom';
|
|
||||||
} else {
|
|
||||||
sidePanelPosition = 'right';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleShare() {
|
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) {
|
if (result.success) {
|
||||||
shareUrl = result.data;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(shareUrl);
|
await navigator.clipboard.writeText(result.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to copy to clipboard:', err);
|
console.error('Failed to copy to clipboard:', err);
|
||||||
}
|
}
|
||||||
|
uiState.showShare(result.data);
|
||||||
sharePopupVisible = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOpenScope() {
|
|
||||||
scopePopupVisible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOpenSpectrogram() {
|
|
||||||
spectrogramPopupVisible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (scopePopupVisible || spectrogramPopupVisible) {
|
if (uiState.scopePopupVisible || uiState.spectrogramPopupVisible) {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
analyserNode = csound.getAnalyserNode();
|
analyserNode = csound.getAnalyserNode();
|
||||||
}, 100);
|
}, 100);
|
||||||
@ -344,10 +199,11 @@ i 1 1.5 0.5 523.25 0.3
|
|||||||
{#snippet filesTabContent()}
|
{#snippet filesTabContent()}
|
||||||
<FileBrowser
|
<FileBrowser
|
||||||
bind:this={fileBrowserRef}
|
bind:this={fileBrowserRef}
|
||||||
|
{projectManager}
|
||||||
onFileSelect={handleFileSelect}
|
onFileSelect={handleFileSelect}
|
||||||
onNewFile={handleNewFile}
|
onNewFile={handleNewFile}
|
||||||
onMetadataUpdate={handleMetadataUpdate}
|
onMetadataUpdate={handleMetadataUpdate}
|
||||||
selectedProjectId={currentProjectId}
|
selectedProjectId={projectEditor.currentProjectId}
|
||||||
/>
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
@ -356,53 +212,53 @@ i 1 1.5 0.5 523.25 0.3
|
|||||||
{#snippet leftActions()}
|
{#snippet leftActions()}
|
||||||
<button
|
<button
|
||||||
class="icon-button"
|
class="icon-button"
|
||||||
onclick={saveCurrentProject}
|
onclick={handleSave}
|
||||||
disabled={!hasUnsavedChanges}
|
disabled={!projectEditor.hasUnsavedChanges}
|
||||||
title="Save {hasUnsavedChanges ? '(unsaved changes)' : ''}"
|
title="Save {projectEditor.hasUnsavedChanges ? '(unsaved changes)' : ''}"
|
||||||
class:has-changes={hasUnsavedChanges}
|
class:has-changes={projectEditor.hasUnsavedChanges}
|
||||||
>
|
>
|
||||||
<Save size={18} />
|
<Save size={18} />
|
||||||
</button>
|
</button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
<button onclick={toggleSidePanel} class="icon-button">
|
<button onclick={() => uiState.toggleSidePanel()} class="icon-button">
|
||||||
{#if sidePanelVisible}
|
{#if uiState.sidePanelVisible}
|
||||||
{#if sidePanelPosition === 'left'}
|
{#if uiState.sidePanelPosition === 'left'}
|
||||||
<PanelLeftClose size={18} />
|
<PanelLeftClose size={18} />
|
||||||
{:else if sidePanelPosition === 'right'}
|
{:else if uiState.sidePanelPosition === 'right'}
|
||||||
<PanelRightClose size={18} />
|
<PanelRightClose size={18} />
|
||||||
{:else}
|
{:else}
|
||||||
<PanelBottomClose size={18} />
|
<PanelBottomClose size={18} />
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
{#if sidePanelPosition === 'left'}
|
{#if uiState.sidePanelPosition === 'left'}
|
||||||
<PanelLeftOpen size={18} />
|
<PanelLeftOpen size={18} />
|
||||||
{:else if sidePanelPosition === 'right'}
|
{:else if uiState.sidePanelPosition === 'right'}
|
||||||
<PanelRightOpen size={18} />
|
<PanelRightOpen size={18} />
|
||||||
{:else}
|
{:else}
|
||||||
<PanelBottomOpen size={18} />
|
<PanelBottomOpen size={18} />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</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} />
|
<LayoutGrid size={18} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={handleShare}
|
onclick={handleShare}
|
||||||
class="icon-button"
|
class="icon-button"
|
||||||
disabled={!currentProjectId}
|
disabled={!projectEditor.currentProjectId}
|
||||||
title="Share project"
|
title="Share project"
|
||||||
>
|
>
|
||||||
<Share2 size={18} />
|
<Share2 size={18} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={handleOpenScope}
|
onclick={() => uiState.openScope()}
|
||||||
class="icon-button"
|
class="icon-button"
|
||||||
title="Open audio scope"
|
title="Open audio scope"
|
||||||
>
|
>
|
||||||
<Activity size={18} />
|
<Activity size={18} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={handleOpenSpectrogram}
|
onclick={() => uiState.openSpectrogram()}
|
||||||
class="icon-button"
|
class="icon-button"
|
||||||
title="Open spectrogram"
|
title="Open spectrogram"
|
||||||
>
|
>
|
||||||
@ -416,40 +272,38 @@ i 1 1.5 0.5 523.25 0.3
|
|||||||
</button>
|
</button>
|
||||||
</TopBar>
|
</TopBar>
|
||||||
|
|
||||||
<div class="main-content" class:panel-bottom={sidePanelPosition === 'bottom'}>
|
<div class="main-content" class:panel-bottom={uiState.sidePanelPosition === 'bottom'}>
|
||||||
{#if sidePanelPosition === 'left'}
|
{#if uiState.sidePanelPosition === 'left'}
|
||||||
<SidePanel
|
<SidePanel
|
||||||
bind:this={sidePanelRef}
|
bind:visible={uiState.sidePanelVisible}
|
||||||
bind:visible={sidePanelVisible}
|
bind:position={uiState.sidePanelPosition}
|
||||||
bind:position={sidePanelPosition}
|
|
||||||
tabs={panelTabs}
|
tabs={panelTabs}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="editor-area">
|
<div class="editor-area">
|
||||||
<EditorWithLogs
|
<EditorWithLogs
|
||||||
bind:this={editorRef}
|
value={projectEditor.content}
|
||||||
initialValue={editorValue}
|
|
||||||
language="javascript"
|
language="javascript"
|
||||||
onChange={handleEditorChange}
|
onChange={handleEditorChange}
|
||||||
|
onExecute={handleExecute}
|
||||||
logs={interpreterLogs}
|
logs={interpreterLogs}
|
||||||
|
{editorSettings}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if sidePanelPosition === 'right'}
|
{#if uiState.sidePanelPosition === 'right'}
|
||||||
<SidePanel
|
<SidePanel
|
||||||
bind:this={sidePanelRef}
|
bind:visible={uiState.sidePanelVisible}
|
||||||
bind:visible={sidePanelVisible}
|
bind:position={uiState.sidePanelPosition}
|
||||||
bind:position={sidePanelPosition}
|
|
||||||
tabs={panelTabs}
|
tabs={panelTabs}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if sidePanelPosition === 'bottom'}
|
{#if uiState.sidePanelPosition === 'bottom'}
|
||||||
<SidePanel
|
<SidePanel
|
||||||
bind:this={sidePanelRef}
|
bind:visible={uiState.sidePanelVisible}
|
||||||
bind:visible={sidePanelVisible}
|
bind:position={uiState.sidePanelPosition}
|
||||||
bind:position={sidePanelPosition}
|
|
||||||
initialWidth={200}
|
initialWidth={200}
|
||||||
minWidth={100}
|
minWidth={100}
|
||||||
maxWidth={400}
|
maxWidth={400}
|
||||||
@ -458,21 +312,8 @@ i 1 1.5 0.5 523.25 0.3
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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
|
<ResizablePopup
|
||||||
bind:visible={sharePopupVisible}
|
bind:visible={uiState.sharePopupVisible}
|
||||||
title="Share Project"
|
title="Share Project"
|
||||||
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 300 : 300)}
|
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 300 : 300)}
|
||||||
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 100 : 200)}
|
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 100 : 200)}
|
||||||
@ -488,7 +329,7 @@ i 1 1.5 0.5 523.25 0.3
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
readonly
|
readonly
|
||||||
value={shareUrl}
|
value={uiState.shareUrl}
|
||||||
class="share-url-input"
|
class="share-url-input"
|
||||||
onclick={(e) => e.currentTarget.select()}
|
onclick={(e) => e.currentTarget.select()}
|
||||||
/>
|
/>
|
||||||
@ -499,7 +340,7 @@ i 1 1.5 0.5 523.25 0.3
|
|||||||
</ResizablePopup>
|
</ResizablePopup>
|
||||||
|
|
||||||
<ResizablePopup
|
<ResizablePopup
|
||||||
visible={audioPermissionPopupVisible}
|
visible={uiState.audioPermissionPopupVisible}
|
||||||
title="Audio Permission Required"
|
title="Audio Permission Required"
|
||||||
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 250 : 250)}
|
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 250 : 250)}
|
||||||
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 125 : 200)}
|
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>
|
||||||
|
|
||||||
<ResizablePopup
|
<ResizablePopup
|
||||||
bind:visible={scopePopupVisible}
|
bind:visible={uiState.scopePopupVisible}
|
||||||
title="Audio Scope"
|
title="Audio Scope"
|
||||||
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 400 : 100)}
|
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 400 : 100)}
|
||||||
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 300 : 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>
|
||||||
|
|
||||||
<ResizablePopup
|
<ResizablePopup
|
||||||
bind:visible={spectrogramPopupVisible}
|
bind:visible={uiState.spectrogramPopupVisible}
|
||||||
title="Spectrogram"
|
title="Spectrogram"
|
||||||
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 400 : 150)}
|
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 400 : 150)}
|
||||||
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 300 : 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>
|
</ResizablePopup>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
bind:visible={showUnsavedDialog}
|
bind:visible={uiState.unsavedChangesDialogVisible}
|
||||||
title="Unsaved Changes"
|
title="Unsaved Changes"
|
||||||
message="You have unsaved changes. What would you like to do?"
|
message="You have unsaved changes. What would you like to do?"
|
||||||
confirmLabel="Save"
|
confirmLabel="Save"
|
||||||
cancelLabel="Discard"
|
cancelLabel="Discard"
|
||||||
onConfirm={handleSaveAndSwitch}
|
onConfirm={() => handleSwitchConfirm('save')}
|
||||||
onCancel={handleDiscardAndSwitch}
|
onCancel={() => handleSwitchConfirm('discard')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputDialog
|
<InputDialog
|
||||||
bind:visible={showSaveAsDialog}
|
bind:visible={uiState.saveAsDialogVisible}
|
||||||
title="Save As"
|
title="Save As"
|
||||||
label="File name"
|
label="File name"
|
||||||
placeholder="Untitled"
|
placeholder="Untitled"
|
||||||
@ -703,3 +544,4 @@ i 1 1.5 0.5 523.25 0.3
|
|||||||
background-color: #424ab8;
|
background-color: #424ab8;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@ -13,19 +13,22 @@
|
|||||||
import { css } from '@codemirror/lang-css';
|
import { css } from '@codemirror/lang-css';
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
import { vim } from '@replit/codemirror-vim';
|
import { vim } from '@replit/codemirror-vim';
|
||||||
import { editorSettings } from './stores/editorSettings';
|
import type { EditorSettingsStore } from './stores/editorSettings';
|
||||||
import { csound } from './csound';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
initialValue?: string;
|
value: string;
|
||||||
language?: 'javascript' | 'html' | 'css';
|
language?: 'javascript' | 'html' | 'css';
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
|
onExecute?: (code: string) => void;
|
||||||
|
editorSettings: EditorSettingsStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
initialValue = '',
|
value = '',
|
||||||
language = 'javascript',
|
language = 'javascript',
|
||||||
onChange
|
onChange,
|
||||||
|
onExecute,
|
||||||
|
editorSettings
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let editorContainer: HTMLDivElement;
|
let editorContainer: HTMLDivElement;
|
||||||
@ -45,10 +48,10 @@
|
|||||||
{
|
{
|
||||||
key: 'Mod-e',
|
key: 'Mod-e',
|
||||||
run: (view) => {
|
run: (view) => {
|
||||||
const code = view.state.doc.toString();
|
if (onExecute) {
|
||||||
csound.evaluate(code).catch(err => {
|
const code = view.state.doc.toString();
|
||||||
console.error('Evaluation error:', err);
|
onExecute(code);
|
||||||
});
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,7 +87,7 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
editorView = new EditorView({
|
editorView = new EditorView({
|
||||||
doc: initialValue,
|
doc: value,
|
||||||
extensions: [
|
extensions: [
|
||||||
...baseExtensions,
|
...baseExtensions,
|
||||||
languageExtensions[language],
|
languageExtensions[language],
|
||||||
@ -134,17 +137,13 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export function getValue(): string {
|
$effect(() => {
|
||||||
return editorView?.state.doc.toString() || '';
|
if (editorView && value !== editorView.state.doc.toString()) {
|
||||||
}
|
|
||||||
|
|
||||||
export function setValue(value: string): void {
|
|
||||||
if (editorView) {
|
|
||||||
editorView.dispatch({
|
editorView.dispatch({
|
||||||
changes: { from: 0, to: editorView.state.doc.length, insert: value }
|
changes: { from: 0, to: editorView.state.doc.length, insert: value }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="editor-wrapper">
|
<div class="editor-wrapper">
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { editorSettings } from './stores/editorSettings';
|
import { getAppContext } from './contexts/app-context';
|
||||||
|
|
||||||
|
const { editorSettings } = getAppContext();
|
||||||
|
|
||||||
let settings = $state($editorSettings);
|
let settings = $state($editorSettings);
|
||||||
|
|
||||||
|
|||||||
@ -2,22 +2,26 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import Editor from './Editor.svelte';
|
import Editor from './Editor.svelte';
|
||||||
import LogPanel from './LogPanel.svelte';
|
import LogPanel from './LogPanel.svelte';
|
||||||
|
import type { EditorSettingsStore } from './stores/editorSettings';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
initialValue?: string;
|
value: string;
|
||||||
language?: 'javascript' | 'html' | 'css';
|
language?: 'javascript' | 'html' | 'css';
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
|
onExecute?: (code: string) => void;
|
||||||
logs?: string[];
|
logs?: string[];
|
||||||
|
editorSettings: EditorSettingsStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
initialValue = '',
|
value = '',
|
||||||
language = 'javascript',
|
language = 'javascript',
|
||||||
onChange,
|
onChange,
|
||||||
logs = []
|
onExecute,
|
||||||
|
logs = [],
|
||||||
|
editorSettings
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let editorRef: Editor;
|
|
||||||
let logPanelRef: LogPanel;
|
let logPanelRef: LogPanel;
|
||||||
|
|
||||||
let editorHeight = $state(70);
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="editor-with-logs">
|
<div class="editor-with-logs">
|
||||||
<div class="editor-section" style="height: {editorHeight}%;">
|
<div class="editor-section" style="height: {editorHeight}%;">
|
||||||
<Editor
|
<Editor
|
||||||
bind:this={editorRef}
|
{value}
|
||||||
{initialValue}
|
|
||||||
{language}
|
{language}
|
||||||
{onChange}
|
{onChange}
|
||||||
|
{onExecute}
|
||||||
|
{editorSettings}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { File, Plus, Trash2 } from 'lucide-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 ConfirmDialog from './ConfirmDialog.svelte';
|
||||||
import InputDialog from './InputDialog.svelte';
|
import InputDialog from './InputDialog.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
projectManager: ProjectManager;
|
||||||
onFileSelect?: (project: CsoundProject | null) => void;
|
onFileSelect?: (project: CsoundProject | null) => void;
|
||||||
onNewFile?: () => void;
|
onNewFile?: () => void;
|
||||||
onMetadataUpdate?: (projectId: string, updates: { title?: string; author?: string }) => void;
|
onMetadataUpdate?: (projectId: string, updates: { title?: string; author?: string }) => void;
|
||||||
selectedProjectId?: string | null;
|
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 projects = $state<CsoundProject[]>([]);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Copy, Trash2, Search, Pause, Play } from 'lucide-svelte';
|
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 type { LogEntry } from './csound';
|
||||||
import { onMount, tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
logs?: LogEntry[];
|
logs?: LogEntry[];
|
||||||
@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
let { logs = [] }: Props = $props();
|
let { logs = [] }: Props = $props();
|
||||||
|
|
||||||
|
const { csound } = getAppContext();
|
||||||
|
|
||||||
let searchQuery = $state('');
|
let searchQuery = $state('');
|
||||||
let autoFollow = $state(true);
|
let autoFollow = $state(true);
|
||||||
let searchVisible = $state(false);
|
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 { CsoundEngine } from './engine';
|
||||||
export type { CsoundEngineOptions } from './engine';
|
export type { CsoundEngineOptions } from './engine';
|
||||||
export { csound, csoundLogs, csoundInitialized, csoundRunning } from './store';
|
export { createCsoundStore, createCsoundDerivedStores } from './store';
|
||||||
export type { LogEntry } from './store';
|
export type { LogEntry, CsoundStore } from './store';
|
||||||
|
|||||||
@ -13,7 +13,18 @@ interface CsoundState {
|
|||||||
logs: LogEntry[];
|
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 = {
|
const initialState: CsoundState = {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
running: false,
|
running: false,
|
||||||
@ -116,8 +127,10 @@ function createCsoundStore() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const csound = createCsoundStore();
|
export function createCsoundDerivedStores(csound: CsoundStore) {
|
||||||
|
return {
|
||||||
export const csoundLogs = derived(csound, $csound => $csound.logs);
|
logs: derived(csound, $csound => $csound.logs),
|
||||||
export const csoundInitialized = derived(csound, $csound => $csound.initialized);
|
initialized: derived(csound, $csound => $csound.initialized),
|
||||||
export const csoundRunning = derived(csound, $csound => $csound.running);
|
running: derived(csound, $csound => $csound.running)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -41,7 +41,7 @@ export type {
|
|||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
// Export main API
|
// Export main API
|
||||||
export { ProjectManager, projectManager } from './project-manager';
|
export { ProjectManager } from './project-manager';
|
||||||
|
|
||||||
// Export database (for advanced usage)
|
// Export database (for advanced usage)
|
||||||
export { projectDb } from './db';
|
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
|
vimMode: false
|
||||||
};
|
};
|
||||||
|
|
||||||
function createEditorSettings() {
|
export interface EditorSettingsStore {
|
||||||
const stored = localStorage.getItem('editorSettings');
|
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 initial = stored ? { ...defaultSettings, ...JSON.parse(stored) } : defaultSettings;
|
||||||
|
|
||||||
const { subscribe, set, update } = writable<EditorSettings>(initial);
|
const { subscribe, set, update } = writable<EditorSettings>(initial);
|
||||||
@ -27,24 +34,28 @@ function createEditorSettings() {
|
|||||||
return {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
set: (value: EditorSettings) => {
|
set: (value: EditorSettings) => {
|
||||||
localStorage.setItem('editorSettings', JSON.stringify(value));
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
localStorage.setItem('editorSettings', JSON.stringify(value));
|
||||||
|
}
|
||||||
set(value);
|
set(value);
|
||||||
},
|
},
|
||||||
update: (updater: (value: EditorSettings) => EditorSettings) => {
|
update: (updater: (value: EditorSettings) => EditorSettings) => {
|
||||||
update((current) => {
|
update((current) => {
|
||||||
const newValue = updater(current);
|
const newValue = updater(current);
|
||||||
localStorage.setItem('editorSettings', JSON.stringify(newValue));
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
localStorage.setItem('editorSettings', JSON.stringify(newValue));
|
||||||
|
}
|
||||||
return newValue;
|
return newValue;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
updatePartial: (partial: Partial<EditorSettings>) => {
|
updatePartial: (partial: Partial<EditorSettings>) => {
|
||||||
update((current) => {
|
update((current) => {
|
||||||
const newValue = { ...current, ...partial };
|
const newValue = { ...current, ...partial };
|
||||||
localStorage.setItem('editorSettings', JSON.stringify(newValue));
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
localStorage.setItem('editorSettings', JSON.stringify(newValue));
|
||||||
|
}
|
||||||
return 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