Refactoring

This commit is contained in:
2025-10-15 01:41:52 +02:00
parent e492c03f15
commit 8e6b07797c
11 changed files with 180 additions and 173 deletions

View File

@ -13,7 +13,6 @@
import InputDialog from './lib/InputDialog.svelte'; import InputDialog from './lib/InputDialog.svelte';
import { createCsoundDerivedStores, type LogEntry } from './lib/csound'; import { createCsoundDerivedStores, type LogEntry } from './lib/csound';
import { type CsoundProject } from './lib/project-system'; import { type CsoundProject } from './lib/project-system';
import { uiState } from './lib/stores/uiState.svelte';
import { DEFAULT_CSOUND_TEMPLATE } from './lib/config/templates'; import { DEFAULT_CSOUND_TEMPLATE } from './lib/config/templates';
import { createAppContext, setAppContext } from './lib/contexts/app-context'; import { createAppContext, setAppContext } from './lib/contexts/app-context';
import { import {
@ -32,12 +31,11 @@
const appContext = createAppContext(); const appContext = createAppContext();
setAppContext(appContext); setAppContext(appContext);
const { csound, projectManager, editorSettings, projectEditor } = appContext; const { csound, projectManager, editorSettings, projectEditor, uiState } = appContext;
const csoundDerived = createCsoundDerivedStores(csound); const csoundDerived = createCsoundDerivedStores(csound);
let analyserNode = $state<AnalyserNode | null>(null); let analyserNode = $state<AnalyserNode | null>(null);
let interpreterLogs = $state<LogEntry[]>([]); let interpreterLogs = $state<LogEntry[]>([]);
let fileBrowserRef: FileBrowser;
onMount(async () => { onMount(async () => {
await projectManager.init(); await projectManager.init();
@ -79,12 +77,11 @@
} }
function handleNewFile() { function handleNewFile() {
const needsConfirm = projectEditor.requestSwitch( const result = projectEditor.requestSwitch(
() => projectEditor.createNew(DEFAULT_CSOUND_TEMPLATE), () => projectEditor.createNew(DEFAULT_CSOUND_TEMPLATE)
handleSwitchConfirm
); );
if (needsConfirm) { if (result === 'confirm-unsaved') {
uiState.showUnsavedChangesDialog(); uiState.showUnsavedChangesDialog();
} }
} }
@ -92,12 +89,11 @@
function handleFileSelect(project: CsoundProject | null) { function handleFileSelect(project: CsoundProject | null) {
if (!project) return; if (!project) return;
const needsConfirm = projectEditor.requestSwitch( const result = projectEditor.requestSwitch(
() => projectEditor.loadProject(project), () => projectEditor.loadProject(project)
handleSwitchConfirm
); );
if (needsConfirm) { if (result === 'confirm-unsaved') {
uiState.showUnsavedChangesDialog(); uiState.showUnsavedChangesDialog();
} }
} }
@ -116,42 +112,34 @@
return; return;
} }
const success = await projectEditor.save(); await projectEditor.save();
if (success) {
fileBrowserRef?.refresh();
}
} }
async function handleSaveAs(title: string) { async function handleSaveAs(title: string) {
const success = await projectEditor.saveAs(title); const success = await projectEditor.saveAs(title);
if (success) { if (success) {
fileBrowserRef?.refresh();
uiState.hideSaveAsDialog(); uiState.hideSaveAsDialog();
} }
} }
async function handleMetadataUpdate(projectId: string, updates: { title?: string; author?: string }) { async function handleMetadataUpdate(projectId: string, updates: { title?: string; author?: string }) {
const success = await projectEditor.updateMetadata(updates); await projectEditor.updateMetadata(updates);
if (success) { }
fileBrowserRef?.refresh();
async function handleSwitchSave() {
const result = await projectEditor.confirmSaveAndSwitch();
if (result === 'show-save-as') {
uiState.hideUnsavedChangesDialog();
uiState.showSaveAsDialog();
} else {
uiState.hideUnsavedChangesDialog();
} }
} }
function handleSwitchConfirm(action: 'save' | 'discard') { function handleSwitchDiscard() {
if (action === 'save') { projectEditor.confirmDiscardAndSwitch();
if (projectEditor.isNewUnsavedBuffer) { uiState.hideUnsavedChangesDialog();
uiState.hideUnsavedChangesDialog();
uiState.showSaveAsDialog();
} else {
projectEditor.handleSaveAndSwitch().then(() => {
fileBrowserRef?.refresh();
uiState.hideUnsavedChangesDialog();
});
}
} else {
projectEditor.handleDiscardAndSwitch();
uiState.hideUnsavedChangesDialog();
}
} }
async function handleShare() { async function handleShare() {
@ -170,11 +158,15 @@
$effect(() => { $effect(() => {
if (uiState.scopePopupVisible || uiState.spectrogramPopupVisible) { if (uiState.scopePopupVisible || uiState.spectrogramPopupVisible) {
const interval = setInterval(() => { analyserNode = csound.getAnalyserNode();
analyserNode = csound.getAnalyserNode();
}, 100);
return () => clearInterval(interval); const unsubscribe = csound.onAnalyserNodeCreated((node) => {
analyserNode = node;
});
return unsubscribe;
} else {
analyserNode = null;
} }
}); });
@ -198,7 +190,6 @@
{#snippet filesTabContent()} {#snippet filesTabContent()}
<FileBrowser <FileBrowser
bind:this={fileBrowserRef}
{projectManager} {projectManager}
onFileSelect={handleFileSelect} onFileSelect={handleFileSelect}
onNewFile={handleNewFile} onNewFile={handleNewFile}
@ -400,8 +391,8 @@
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={() => handleSwitchConfirm('save')} onConfirm={handleSwitchSave}
onCancel={() => handleSwitchConfirm('discard')} onCancel={handleSwitchDiscard}
/> />
<InputDialog <InputDialog

View File

@ -58,8 +58,6 @@
]); ]);
onMount(() => { onMount(() => {
const settings = $editorSettings;
const baseExtensions = [ const baseExtensions = [
highlightActiveLineGutter(), highlightActiveLineGutter(),
highlightSpecialChars(), highlightSpecialChars(),
@ -86,6 +84,8 @@
]) ])
]; ];
const initSettings = $editorSettings;
editorView = new EditorView({ editorView = new EditorView({
doc: value, doc: value,
extensions: [ extensions: [
@ -93,9 +93,9 @@
languageExtensions[language], languageExtensions[language],
oneDark, oneDark,
evaluateKeymap, evaluateKeymap,
lineNumbersCompartment.of(settings.showLineNumbers ? lineNumbers() : []), lineNumbersCompartment.of(initSettings.showLineNumbers ? lineNumbers() : []),
lineWrappingCompartment.of(settings.enableLineWrapping ? EditorView.lineWrapping : []), lineWrappingCompartment.of(initSettings.enableLineWrapping ? EditorView.lineWrapping : []),
vimCompartment.of(settings.vimMode ? vim() : []), vimCompartment.of(initSettings.vimMode ? vim() : []),
EditorView.updateListener.of((update) => { EditorView.updateListener.of((update) => {
if (update.docChanged && onChange) { if (update.docChanged && onChange) {
onChange(update.state.doc.toString()); onChange(update.state.doc.toString());
@ -103,32 +103,29 @@
}), }),
EditorView.theme({ EditorView.theme({
'&': { '&': {
fontSize: `${settings.fontSize}px`, fontSize: `${initSettings.fontSize}px`,
fontFamily: settings.fontFamily fontFamily: initSettings.fontFamily
} }
}) })
], ],
parent: editorContainer parent: editorContainer
}); });
});
const unsubscribe = editorSettings.subscribe((newSettings) => { $effect(() => {
if (!editorView) return; const settings = $editorSettings;
if (!editorView) return;
editorView.dispatch({ editorView.dispatch({
effects: [ effects: [
lineNumbersCompartment.reconfigure(newSettings.showLineNumbers ? lineNumbers() : []), lineNumbersCompartment.reconfigure(settings.showLineNumbers ? lineNumbers() : []),
lineWrappingCompartment.reconfigure(newSettings.enableLineWrapping ? EditorView.lineWrapping : []), lineWrappingCompartment.reconfigure(settings.enableLineWrapping ? EditorView.lineWrapping : []),
vimCompartment.reconfigure(newSettings.vimMode ? vim() : []) vimCompartment.reconfigure(settings.vimMode ? vim() : [])
] ]
});
editorView.dom.style.fontSize = `${newSettings.fontSize}px`;
editorView.dom.style.fontFamily = newSettings.fontFamily;
}); });
return () => { editorView.dom.style.fontSize = `${settings.fontSize}px`;
unsubscribe(); editorView.dom.style.fontFamily = settings.fontFamily;
};
}); });
onDestroy(() => { onDestroy(() => {

View File

@ -40,12 +40,17 @@
onMount(async () => { onMount(async () => {
await loadProjects(); await loadProjects();
const unsubscribe = projectManager.onProjectsChanged(() => {
loadProjects();
});
return unsubscribe;
}); });
async function loadProjects() { async function loadProjects() {
loading = true; loading = true;
try { try {
await projectManager.init();
const result = await projectManager.getAllProjects(); const result = await projectManager.getAllProjects();
if (result.success) { if (result.success) {
projects = result.data.sort((a, b) => projects = result.data.sort((a, b) =>

View File

@ -1,8 +1,10 @@
import { getContext, setContext } from 'svelte'; import { getContext, setContext } from 'svelte';
import { ProjectManager } from '../project-system/project-manager'; import { ProjectManager } from '../project-system/project-manager';
import { ProjectDatabase } from '../project-system/db';
import { createCsoundStore } from '../csound/store'; import { createCsoundStore } from '../csound/store';
import { createEditorSettingsStore } from '../stores/editorSettings'; import { createEditorSettingsStore } from '../stores/editorSettings';
import { ProjectEditor } from '../stores/projectEditor.svelte'; import { ProjectEditor } from '../stores/projectEditor.svelte';
import { UIState } from '../stores/uiState.svelte';
import type { CsoundStore } from '../csound/store'; import type { CsoundStore } from '../csound/store';
import type { EditorSettingsStore } from '../stores/editorSettings'; import type { EditorSettingsStore } from '../stores/editorSettings';
@ -11,17 +13,20 @@ export interface AppContext {
csound: CsoundStore; csound: CsoundStore;
editorSettings: EditorSettingsStore; editorSettings: EditorSettingsStore;
projectEditor: ProjectEditor; projectEditor: ProjectEditor;
uiState: UIState;
} }
const APP_CONTEXT_KEY = Symbol('app-context'); const APP_CONTEXT_KEY = Symbol('app-context');
export function createAppContext(): AppContext { export function createAppContext(): AppContext {
const projectManager = new ProjectManager(); const db = new ProjectDatabase();
const projectManager = new ProjectManager(db);
return { return {
projectManager, projectManager,
csound: createCsoundStore(), csound: createCsoundStore(),
editorSettings: createEditorSettingsStore(), editorSettings: createEditorSettingsStore(),
projectEditor: new ProjectEditor(projectManager) projectEditor: new ProjectEditor(projectManager),
uiState: new UIState()
}; };
} }

View File

@ -4,6 +4,7 @@ export interface CsoundEngineOptions {
onMessage?: (message: string) => void; onMessage?: (message: string) => void;
onError?: (error: string) => void; onError?: (error: string) => void;
onPerformanceEnd?: () => void; onPerformanceEnd?: () => void;
onAnalyserNodeCreated?: (node: AnalyserNode) => void;
} }
export class CsoundEngine { export class CsoundEngine {
@ -145,6 +146,7 @@ export class CsoundEngine {
this.scopeNode.connect(this.audioContext.destination); this.scopeNode.connect(this.audioContext.destination);
this.log('Analyser node created and connected'); this.log('Analyser node created and connected');
this.options.onAnalyserNodeCreated?.(this.scopeNode);
} catch (error) { } catch (error) {
console.error('Failed to setup analyser:', error); console.error('Failed to setup analyser:', error);
this.log('Error setting up analyser: ' + error); this.log('Error setting up analyser: ' + error);

View File

@ -21,6 +21,7 @@ export interface CsoundStore {
clearLogs: () => void; clearLogs: () => void;
getAudioContext: () => AudioContext | null; getAudioContext: () => AudioContext | null;
getAnalyserNode: () => AnalyserNode | null; getAnalyserNode: () => AnalyserNode | null;
onAnalyserNodeCreated: (callback: (node: AnalyserNode) => void) => () => void;
destroy: () => Promise<void>; destroy: () => Promise<void>;
} }
@ -34,6 +35,7 @@ export function createCsoundStore(): CsoundStore {
const { subscribe, set, update } = writable<CsoundState>(initialState); const { subscribe, set, update } = writable<CsoundState>(initialState);
let engine: CsoundEngine | null = null; let engine: CsoundEngine | null = null;
const analyserNodeListeners: Set<(node: AnalyserNode) => void> = new Set();
function addLog(message: string, type: 'info' | 'error' = 'info') { function addLog(message: string, type: 'info' | 'error' = 'info') {
update(state => ({ update(state => ({
@ -51,7 +53,10 @@ export function createCsoundStore(): CsoundStore {
try { try {
engine = new CsoundEngine({ engine = new CsoundEngine({
onMessage: (msg) => addLog(msg, 'info'), onMessage: (msg) => addLog(msg, 'info'),
onError: (err) => addLog(err, 'error') onError: (err) => addLog(err, 'error'),
onAnalyserNodeCreated: (node) => {
analyserNodeListeners.forEach(listener => listener(node));
}
}); });
await engine.init(); await engine.init();
@ -117,11 +122,17 @@ export function createCsoundStore(): CsoundStore {
return null; return null;
}, },
onAnalyserNodeCreated(callback: (node: AnalyserNode) => void): () => void {
analyserNodeListeners.add(callback);
return () => analyserNodeListeners.delete(callback);
},
async destroy() { async destroy() {
if (engine) { if (engine) {
await engine.destroy(); await engine.destroy();
engine = null; engine = null;
} }
analyserNodeListeners.clear();
set(initialState); set(initialState);
} }
}; };

View File

@ -229,5 +229,4 @@ class ProjectDatabase {
} }
} }
// Export singleton instance export { ProjectDatabase };
export const projectDb = new ProjectDatabase();

View File

@ -1,8 +1,20 @@
import type { CsoundProject, CreateProjectData, UpdateProjectData, Result } from './types'; import type { CsoundProject, CreateProjectData, UpdateProjectData, Result } from './types';
import { projectDb } from './db'; import type { ProjectDatabase } from './db';
import { compressProject, decompressProject, projectToShareUrl, projectFromShareUrl } from './compression'; import { compressProject, decompressProject, projectToShareUrl, projectFromShareUrl } from './compression';
const CSOUND_VERSION = '7.0.0'; // This should be detected from @csound/browser const CSOUND_VERSION = '7.0.0';
async function wrapResult<T>(fn: () => Promise<T>, errorMsg: string): Promise<Result<T>> {
try {
const data = await fn();
return { success: true, data };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error(errorMsg),
};
}
}
/** /**
* Generate a unique ID for a project * Generate a unique ID for a project
@ -18,15 +30,54 @@ function getCurrentTimestamp(): string {
return new Date().toISOString(); return new Date().toISOString();
} }
/**
* Create an imported/duplicated project with new ID and timestamps
*/
function createDerivedProject(baseProject: CsoundProject, titleSuffix: string): CsoundProject {
const now = getCurrentTimestamp();
return {
...baseProject,
id: generateId(),
title: `${baseProject.title} ${titleSuffix}`,
dateCreated: now,
dateModified: now,
saveCount: 0,
};
}
type ProjectChangeListener = () => void;
/** /**
* Project Manager - Main API for managing Csound projects * Project Manager - Main API for managing Csound projects
*/ */
export class ProjectManager { export class ProjectManager {
private db: ProjectDatabase;
private changeListeners: Set<ProjectChangeListener> = new Set();
constructor(db: ProjectDatabase) {
this.db = db;
}
/**
* Subscribe to project changes
*/
onProjectsChanged(listener: ProjectChangeListener): () => void {
this.changeListeners.add(listener);
return () => this.changeListeners.delete(listener);
}
/**
* Notify all listeners that projects have changed
*/
private notifyChange(): void {
this.changeListeners.forEach(listener => listener());
}
/** /**
* Initialize the project manager (initializes database) * Initialize the project manager (initializes database)
*/ */
async init(): Promise<void> { async init(): Promise<void> {
await projectDb.init(); await this.db.init();
} }
/** /**
@ -48,7 +99,8 @@ export class ProjectManager {
csoundVersion: CSOUND_VERSION, csoundVersion: CSOUND_VERSION,
}; };
await projectDb.put(project); await this.db.put(project);
this.notifyChange();
return { success: true, data: project }; return { success: true, data: project };
} catch (error) { } catch (error) {
@ -64,7 +116,7 @@ export class ProjectManager {
*/ */
async getProject(id: string): Promise<Result<CsoundProject>> { async getProject(id: string): Promise<Result<CsoundProject>> {
try { try {
const project = await projectDb.get(id); const project = await this.db.get(id);
if (!project) { if (!project) {
return { return {
@ -86,15 +138,7 @@ export class ProjectManager {
* Get all projects * Get all projects
*/ */
async getAllProjects(): Promise<Result<CsoundProject[]>> { async getAllProjects(): Promise<Result<CsoundProject[]>> {
try { return wrapResult(() => this.db.getAll(), 'Failed to get projects');
const projects = await projectDb.getAll();
return { success: true, data: projects };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error('Failed to get projects'),
};
}
} }
/** /**
@ -102,7 +146,7 @@ export class ProjectManager {
*/ */
async updateProject(data: UpdateProjectData): Promise<Result<CsoundProject>> { async updateProject(data: UpdateProjectData): Promise<Result<CsoundProject>> {
try { try {
const existingProject = await projectDb.get(data.id); const existingProject = await this.db.get(data.id);
if (!existingProject) { if (!existingProject) {
return { return {
@ -121,7 +165,8 @@ export class ProjectManager {
saveCount: existingProject.saveCount + 1, saveCount: existingProject.saveCount + 1,
}; };
await projectDb.put(updatedProject); await this.db.put(updatedProject);
this.notifyChange();
return { success: true, data: updatedProject }; return { success: true, data: updatedProject };
} catch (error) { } catch (error) {
@ -136,45 +181,25 @@ export class ProjectManager {
* Delete a project * Delete a project
*/ */
async deleteProject(id: string): Promise<Result<void>> { async deleteProject(id: string): Promise<Result<void>> {
try { const result = await wrapResult(() => this.db.delete(id), 'Failed to delete project');
await projectDb.delete(id); if (result.success) {
return { success: true, data: undefined }; this.notifyChange();
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error('Failed to delete project'),
};
} }
return result;
} }
/** /**
* Search projects by tag * Search projects by tag
*/ */
async getProjectsByTag(tag: string): Promise<Result<CsoundProject[]>> { async getProjectsByTag(tag: string): Promise<Result<CsoundProject[]>> {
try { return wrapResult(() => this.db.getByTag(tag), 'Failed to search projects by tag');
const projects = await projectDb.getByTag(tag);
return { success: true, data: projects };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error('Failed to search projects by tag'),
};
}
} }
/** /**
* Search projects by author * Search projects by author
*/ */
async getProjectsByAuthor(author: string): Promise<Result<CsoundProject[]>> { async getProjectsByAuthor(author: string): Promise<Result<CsoundProject[]>> {
try { return wrapResult(() => this.db.getByAuthor(author), 'Failed to search projects by author');
const projects = await projectDb.getByAuthor(author);
return { success: true, data: projects };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error('Failed to search projects by author'),
};
}
} }
/** /**
@ -182,7 +207,7 @@ export class ProjectManager {
*/ */
async exportProjectToUrl(id: string, baseUrl?: string): Promise<Result<string>> { async exportProjectToUrl(id: string, baseUrl?: string): Promise<Result<string>> {
try { try {
const project = await projectDb.get(id); const project = await this.db.get(id);
if (!project) { if (!project) {
return { return {
@ -207,7 +232,7 @@ export class ProjectManager {
*/ */
async exportProjectToCompressed(id: string): Promise<Result<string>> { async exportProjectToCompressed(id: string): Promise<Result<string>> {
try { try {
const project = await projectDb.get(id); const project = await this.db.get(id);
if (!project) { if (!project) {
return { return {
@ -233,19 +258,10 @@ export class ProjectManager {
async importProjectFromUrl(url: string): Promise<Result<CsoundProject>> { async importProjectFromUrl(url: string): Promise<Result<CsoundProject>> {
try { try {
const project = projectFromShareUrl(url); const project = projectFromShareUrl(url);
const importedProject = createDerivedProject(project, '(imported)');
// Generate a new ID and reset timestamps await this.db.put(importedProject);
const now = getCurrentTimestamp(); this.notifyChange();
const importedProject: CsoundProject = {
...project,
id: generateId(),
dateCreated: now,
dateModified: now,
saveCount: 0,
title: `${project.title} (imported)`,
};
await projectDb.put(importedProject);
return { success: true, data: importedProject }; return { success: true, data: importedProject };
} catch (error) { } catch (error) {
@ -265,19 +281,10 @@ export class ProjectManager {
data: compressedData, data: compressedData,
version: 1, version: 1,
}); });
const importedProject = createDerivedProject(project, '(imported)');
// Generate a new ID and reset timestamps await this.db.put(importedProject);
const now = getCurrentTimestamp(); this.notifyChange();
const importedProject: CsoundProject = {
...project,
id: generateId(),
dateCreated: now,
dateModified: now,
saveCount: 0,
title: `${project.title} (imported)`,
};
await projectDb.put(importedProject);
return { success: true, data: importedProject }; return { success: true, data: importedProject };
} catch (error) { } catch (error) {
@ -293,7 +300,7 @@ export class ProjectManager {
*/ */
async duplicateProject(id: string): Promise<Result<CsoundProject>> { async duplicateProject(id: string): Promise<Result<CsoundProject>> {
try { try {
const originalProject = await projectDb.get(id); const originalProject = await this.db.get(id);
if (!originalProject) { if (!originalProject) {
return { return {
@ -302,17 +309,10 @@ export class ProjectManager {
}; };
} }
const now = getCurrentTimestamp(); const duplicatedProject = createDerivedProject(originalProject, '(copy)');
const duplicatedProject: CsoundProject = {
...originalProject,
id: generateId(),
title: `${originalProject.title} (copy)`,
dateCreated: now,
dateModified: now,
saveCount: 0,
};
await projectDb.put(duplicatedProject); await this.db.put(duplicatedProject);
this.notifyChange();
return { success: true, data: duplicatedProject }; return { success: true, data: duplicatedProject };
} catch (error) { } catch (error) {
@ -327,14 +327,10 @@ export class ProjectManager {
* Clear all projects (use with caution!) * Clear all projects (use with caution!)
*/ */
async clearAllProjects(): Promise<Result<void>> { async clearAllProjects(): Promise<Result<void>> {
try { const result = await wrapResult(() => this.db.clear(), 'Failed to clear projects');
await projectDb.clear(); if (result.success) {
return { success: true, data: undefined }; this.notifyChange();
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error('Failed to clear projects'),
};
} }
return result;
} }
} }

View File

@ -1,5 +1,7 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
const STORAGE_KEY = 'editorSettings';
export interface EditorSettings { export interface EditorSettings {
fontSize: number; fontSize: number;
fontFamily: string; fontFamily: string;
@ -26,7 +28,7 @@ export interface EditorSettingsStore {
} }
export function createEditorSettingsStore(): EditorSettingsStore { export function createEditorSettingsStore(): EditorSettingsStore {
const stored = typeof localStorage !== 'undefined' ? localStorage.getItem('editorSettings') : null; const stored = typeof localStorage !== 'undefined' ? localStorage.getItem(STORAGE_KEY) : 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);
@ -35,7 +37,7 @@ export function createEditorSettingsStore(): EditorSettingsStore {
subscribe, subscribe,
set: (value: EditorSettings) => { set: (value: EditorSettings) => {
if (typeof localStorage !== 'undefined') { if (typeof localStorage !== 'undefined') {
localStorage.setItem('editorSettings', JSON.stringify(value)); localStorage.setItem(STORAGE_KEY, JSON.stringify(value));
} }
set(value); set(value);
}, },
@ -43,7 +45,7 @@ export function createEditorSettingsStore(): EditorSettingsStore {
update((current) => { update((current) => {
const newValue = updater(current); const newValue = updater(current);
if (typeof localStorage !== 'undefined') { if (typeof localStorage !== 'undefined') {
localStorage.setItem('editorSettings', JSON.stringify(newValue)); localStorage.setItem(STORAGE_KEY, JSON.stringify(newValue));
} }
return newValue; return newValue;
}); });
@ -52,7 +54,7 @@ export function createEditorSettingsStore(): EditorSettingsStore {
update((current) => { update((current) => {
const newValue = { ...current, ...partial }; const newValue = { ...current, ...partial };
if (typeof localStorage !== 'undefined') { if (typeof localStorage !== 'undefined') {
localStorage.setItem('editorSettings', JSON.stringify(newValue)); localStorage.setItem(STORAGE_KEY, JSON.stringify(newValue));
} }
return newValue; return newValue;
}); });

View File

@ -123,32 +123,33 @@ export class ProjectEditor {
return false; return false;
} }
requestSwitch(action: () => void, onConfirm: (action: 'save' | 'discard') => void) { requestSwitch(action: () => void): 'proceed' | 'confirm-unsaved' {
if (!this.state.hasUnsavedChanges) { if (!this.state.hasUnsavedChanges) {
action(); action();
return false; return 'proceed';
} }
this.pendingAction = action; this.pendingAction = action;
this.confirmCallback = onConfirm; return 'confirm-unsaved';
return true;
} }
async handleSaveAndSwitch(): Promise<void> { async confirmSaveAndSwitch(): Promise<'show-save-as' | 'done'> {
if (this.state.isNewUnsavedBuffer) { if (this.state.isNewUnsavedBuffer) {
this.confirmCallback?.('save'); return 'show-save-as';
return;
} }
await this.save(); await this.save();
this.pendingAction?.(); this.pendingAction?.();
this.pendingAction = null; this.pendingAction = null;
this.confirmCallback = null; return 'done';
} }
handleDiscardAndSwitch(): void { confirmDiscardAndSwitch(): void {
this.pendingAction?.(); this.pendingAction?.();
this.pendingAction = null; this.pendingAction = null;
this.confirmCallback = null; }
cancelSwitch(): void {
this.pendingAction = null;
} }
} }

View File

@ -1,6 +1,6 @@
type PanelPosition = 'left' | 'right' | 'bottom'; type PanelPosition = 'left' | 'right' | 'bottom';
class UIState { export class UIState {
sidePanelVisible = $state(true); sidePanelVisible = $state(true);
sidePanelPosition = $state<PanelPosition>('right'); sidePanelPosition = $state<PanelPosition>('right');
@ -60,5 +60,3 @@ class UIState {
this.saveAsDialogVisible = false; this.saveAsDialogVisible = false;
} }
} }
export const uiState = new UIState();