diff --git a/src/App.svelte b/src/App.svelte index 3ea49b8..2d25a3b 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -133,10 +133,10 @@ projectEditor.setContent(value); } - function handleNewEmptyFile() { + async function handleNewEmptyFile() { const emptyTemplate = templateRegistry.getEmpty(); - const result = projectEditor.requestSwitch(() => - projectEditor.createNew(emptyTemplate.content), + const result = projectEditor.requestSwitch(async () => + await projectEditor.createNew(emptyTemplate.content, emptyTemplate.mode), ); if (result === "confirm-unsaved") { @@ -148,11 +148,11 @@ uiState.showTemplateDialog(); } - function handleTemplateSelect(template: CsoundTemplate) { + async function handleTemplateSelect(template: CsoundTemplate) { uiState.hideTemplateDialog(); - const result = projectEditor.requestSwitch(() => - projectEditor.createNew(template.content), + const result = projectEditor.requestSwitch(async () => + await projectEditor.createNew(template.content, template.mode), ); if (result === "confirm-unsaved") { @@ -181,11 +181,6 @@ } async function handleSave() { - if (projectEditor.isNewUnsavedBuffer) { - uiState.showSaveAsDialog(); - return; - } - await projectEditor.save(); } @@ -208,14 +203,8 @@ } async function handleSwitchSave() { - const result = await projectEditor.confirmSaveAndSwitch(); - - if (result === "show-save-as") { - uiState.hideUnsavedChangesDialog(); - uiState.showSaveAsDialog(); - } else { - uiState.hideUnsavedChangesDialog(); - } + await projectEditor.confirmSaveAndSwitch(); + uiState.hideUnsavedChangesDialog(); } function handleSwitchDiscard() { diff --git a/src/lib/components/ui/SidePanel.svelte b/src/lib/components/ui/SidePanel.svelte index 9459369..b3817ce 100644 --- a/src/lib/components/ui/SidePanel.svelte +++ b/src/lib/components/ui/SidePanel.svelte @@ -28,7 +28,23 @@ let isResizing = $state(false); let startPos = $state(0); let startWidth = $state(0); - let activeTab = $state(tabs[0]?.id || ''); + let activeTab = $state(''); + + function loadActiveTab(): string | null { + try { + return localStorage.getItem('sidePanelActiveTab'); + } catch { + return null; + } + } + + function saveActiveTab(tabId: string) { + try { + localStorage.setItem('sidePanelActiveTab', tabId); + } catch { + // Ignore localStorage errors + } + } function handleResizeStart(e: MouseEvent) { isResizing = true; @@ -62,7 +78,12 @@ } onMount(() => { - if (tabs.length > 0 && !activeTab) { + const savedTab = loadActiveTab(); + const savedTabExists = tabs.some(tab => tab.id === savedTab); + + if (savedTabExists) { + activeTab = savedTab!; + } else if (tabs.length > 0 && !activeTab) { activeTab = tabs[0].id; } @@ -75,6 +96,12 @@ }; }); + $effect(() => { + if (activeTab) { + saveActiveTab(activeTab); + } + }); + export function toggle() { visible = !visible; } diff --git a/src/lib/stores/projectEditor.svelte.ts b/src/lib/stores/projectEditor.svelte.ts index 42fd1c0..4770660 100644 --- a/src/lib/stores/projectEditor.svelte.ts +++ b/src/lib/stores/projectEditor.svelte.ts @@ -1,11 +1,11 @@ import type { CsoundProject, ProjectManager } from '../project-system'; import { saveLastProjectId } from '../project-system/persistence'; +import type { ProjectMode } from '../project-system/types'; interface ProjectEditorState { currentProject: CsoundProject | null; content: string; hasUnsavedChanges: boolean; - isNewUnsavedBuffer: boolean; } export class ProjectEditor { @@ -14,8 +14,7 @@ export class ProjectEditor { private state = $state({ currentProject: null, content: '', - hasUnsavedChanges: false, - isNewUnsavedBuffer: false + hasUnsavedChanges: false }); private pendingAction: (() => void) | null = null; @@ -37,14 +36,31 @@ export class ProjectEditor { return this.state.hasUnsavedChanges; } - get isNewUnsavedBuffer() { - return this.state.isNewUnsavedBuffer; - } - get currentProjectId() { return this.state.currentProject?.id ?? null; } + private async generateUnnamedTitle(): Promise { + const result = await this.projectManager.getAllProjects(); + if (!result.success) { + return 'Unnamed-1'; + } + + const projects = result.data; + const unnamedPattern = /^Unnamed-(\d+)$/; + const unnamedNumbers = projects + .map(p => p.title.match(unnamedPattern)?.[1]) + .filter((n): n is string => n !== undefined) + .map(n => parseInt(n, 10)); + + if (unnamedNumbers.length === 0) { + return 'Unnamed-1'; + } + + const maxNumber = Math.max(...unnamedNumbers); + return `Unnamed-${maxNumber + 1}`; + } + setContent(content: string) { this.state.content = content; this.state.hasUnsavedChanges = true; @@ -54,23 +70,32 @@ export class ProjectEditor { this.state.currentProject = project; this.state.content = project.content; this.state.hasUnsavedChanges = false; - this.state.isNewUnsavedBuffer = false; saveLastProjectId(project.id); } - createNew(template: string) { - this.state.currentProject = null; - this.state.content = template; - this.state.hasUnsavedChanges = false; - this.state.isNewUnsavedBuffer = true; - saveLastProjectId(null); + async createNew(content: string, mode: ProjectMode = 'composition'): Promise { + const title = await this.generateUnnamedTitle(); + + const result = await this.projectManager.createProject({ + title, + author: 'Anonymous', + content, + tags: [], + mode + }); + + if (result.success) { + this.state.currentProject = result.data; + this.state.content = result.data.content; + this.state.hasUnsavedChanges = false; + saveLastProjectId(result.data.id); + return true; + } + + return false; } async save(): Promise { - if (this.state.isNewUnsavedBuffer) { - throw new Error('Cannot save new buffer without title. Use saveAs instead.'); - } - if (!this.state.currentProject) { return false; } @@ -91,24 +116,22 @@ export class ProjectEditor { return false; } - async saveAs(title: string, author: string = 'Anonymous'): Promise { + async saveAs(title: string): Promise { + if (!this.state.currentProject) { + return false; + } + const finalTitle = title.trim() || 'Untitled'; - const isLiveCodingTemplate = this.state.content.includes('Live Coding Template'); - - const result = await this.projectManager.createProject({ + const result = await this.projectManager.updateProject({ + id: this.state.currentProject.id, title: finalTitle, - author, - content: this.state.content, - tags: [], - mode: isLiveCodingTemplate ? 'livecoding' : 'composition' + content: this.state.content }); - if (result.success) { + if (result.success && result.data) { this.state.currentProject = result.data; this.state.hasUnsavedChanges = false; - this.state.isNewUnsavedBuffer = false; - saveLastProjectId(result.data.id); return true; } @@ -143,15 +166,10 @@ export class ProjectEditor { return 'confirm-unsaved'; } - async confirmSaveAndSwitch(): Promise<'show-save-as' | 'done'> { - if (this.state.isNewUnsavedBuffer) { - return 'show-save-as'; - } - + async confirmSaveAndSwitch(): Promise { await this.save(); this.pendingAction?.(); this.pendingAction = null; - return 'done'; } confirmDiscardAndSwitch(): void { diff --git a/src/lib/templates/template-registry.ts b/src/lib/templates/template-registry.ts index aa71685..9ac04b2 100644 --- a/src/lib/templates/template-registry.ts +++ b/src/lib/templates/template-registry.ts @@ -71,24 +71,15 @@ const LIVECODING_TEMPLATE: CsoundTemplate = { id: 'livecoding', name: 'Live Coding', mode: 'livecoding', - content: `; LIVE CODING MODE -; Engine auto-initializes on first evaluation (Ctrl+E) -; Evaluate instruments/opcodes with Ctrl+E to define them -; Evaluate score events (i-statements) to trigger sounds -; Press Ctrl+. to stop all audio - -gaReverb init 0 + content: `gaReverb init 0 instr 1 kFreq chnget "freq" kFreq = (kFreq == 0 ? p4 : kFreq) kAmp = p5 - kEnv linsegr 0, 0.01, 1, 0.1, 0.7, 0.2, 0 aOsc vco2 kAmp * kEnv, kFreq - aFilt moogladder aOsc, 2000, 0.3 - outs aFilt, aFilt gaReverb = gaReverb + aFilt * 0.3 endin @@ -96,12 +87,9 @@ endin instr 2 iFreq = p4 iAmp = p5 - kEnv linsegr 0, 0.005, 1, 0.05, 0.5, 0.1, 0 aOsc vco2 iAmp * kEnv, iFreq, 10 - aFilt butterlp aOsc, 800 - outs aFilt, aFilt endin @@ -115,10 +103,6 @@ endin i 99 0 -1 -; === LIVE CODING EXAMPLES === -; Select a block and press Ctrl+E to evaluate - -; Basic note i 1 0 2 440 0.3 ; Arpeggio @@ -134,9 +118,8 @@ i 2 1.0 0.5 164.81 0.4 i 2 1.5 0.5 130.81 0.4 ; Long note for channel control -i 1 0 30 440 0.3 +i 1 0 10 440 0.3 -; Change frequency while playing (select and evaluate) freq = 440 freq = 554.37