Refactoring
This commit is contained in:
@ -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
|
||||||
|
|||||||
@ -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(() => {
|
||||||
|
|||||||
@ -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) =>
|
||||||
|
|||||||
@ -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()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -229,5 +229,4 @@ class ProjectDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export singleton instance
|
export { ProjectDatabase };
|
||||||
export const projectDb = new ProjectDatabase();
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
|
||||||
|
|||||||
Reference in New Issue
Block a user