This commit is contained in:
2025-10-15 12:56:15 +02:00
parent 008d5cb661
commit 1ead074e21
4 changed files with 92 additions and 75 deletions

View File

@ -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") {
await projectEditor.confirmSaveAndSwitch();
uiState.hideUnsavedChangesDialog();
uiState.showSaveAsDialog();
} else {
uiState.hideUnsavedChangesDialog();
}
}
function handleSwitchDiscard() {

View File

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

View File

@ -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<ProjectEditorState>({
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<string> {
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;
async createNew(content: string, mode: ProjectMode = 'composition'): Promise<boolean> {
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;
this.state.isNewUnsavedBuffer = true;
saveLastProjectId(null);
saveLastProjectId(result.data.id);
return true;
}
return false;
}
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;
}
@ -91,24 +116,22 @@ export class ProjectEditor {
return false;
}
async saveAs(title: string, author: string = 'Anonymous'): Promise<boolean> {
async saveAs(title: string): Promise<boolean> {
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<void> {
await this.save();
this.pendingAction?.();
this.pendingAction = null;
return 'done';
}
confirmDiscardAndSwitch(): void {

View File

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