From 22e9457779bd590ed0298f3a4bfe8c4f0a185208 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Fri, 14 Nov 2025 14:45:22 +0100 Subject: [PATCH] Working on menus --- q | 12 + src/App.svelte | 231 +++++++++++++++++- .../components/editor/EditorSettings.svelte | 2 + .../components/editor/EditorWithLogs.svelte | 12 + .../components/ui/AudioVolumeControl.svelte | 119 +++++++++ src/lib/components/ui/MenuBar.svelte | 15 +- src/lib/components/ui/MenuItem.svelte | 5 +- src/lib/components/ui/MenuSeparator.svelte | 9 + src/lib/csound/engine.ts | 48 +++- src/lib/csound/store.ts | 15 ++ src/lib/project-system/file-manager.ts | 6 +- 11 files changed, 459 insertions(+), 15 deletions(-) create mode 100644 q create mode 100644 src/lib/components/ui/AudioVolumeControl.svelte create mode 100644 src/lib/components/ui/MenuSeparator.svelte diff --git a/q b/q new file mode 100644 index 0000000..1d634ea --- /dev/null +++ b/q @@ -0,0 +1,12 @@ +We are currently working on top bar menu items for this web editor. We are working on the "Share" functionality. +When clicking the share button, here is what should happen: + +- the code for the currently opened file is getting compressed and generates an URL link that you can send to other people. +- when clicking on this link, the web editor opens with the same file code loaded in it. It acts like an import. It will prompt you to rename the file eventually to avoid name conflicts or accidental overwriting. +- when clicking "Share", it opens up a popup that shows you the generated link with a "Copy to clipboard" button next to it. + +Ultrathink and do. + +Currently, clicking on the Share button does absolutely nothing. There is no feedback at all, and no errors either in the console of whatever. + +Explore. diff --git a/src/App.svelte b/src/App.svelte index 94cecd9..184e3f1 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -3,7 +3,9 @@ import MenuBar from "./lib/components/ui/MenuBar.svelte"; import MenuItem from "./lib/components/ui/MenuItem.svelte"; import MenuAction from "./lib/components/ui/MenuAction.svelte"; + import MenuSeparator from "./lib/components/ui/MenuSeparator.svelte"; import AboutDialog from "./lib/components/ui/AboutDialog.svelte"; + import AudioVolumeControl from "./lib/components/ui/AudioVolumeControl.svelte"; import EditorWithLogs from "./lib/components/editor/EditorWithLogs.svelte"; import EditorSettings from "./lib/components/editor/EditorSettings.svelte"; import FileBrowser from "./lib/components/ui/FileBrowser.svelte"; @@ -26,7 +28,11 @@ Save, Share2, Activity, - FileStack, + FilePlus, + Copy, + Download, + Archive, + Upload, PanelLeftOpen, PanelRightOpen, CircleStop, @@ -49,6 +55,7 @@ let analyserNode = $state(null); let interpreterLogs = $state([]); let editorWithLogsRef: EditorWithLogs; + let fileInputRef: HTMLInputElement; let logsUnsubscribe: (() => void) | undefined; @@ -56,6 +63,49 @@ await fileManager.init(); await editorState.refreshFileCache(); + // Check for shared file in URL + const urlParams = new URLSearchParams(window.location.search); + const sharedData = urlParams.get('d'); + const version = urlParams.get('v'); + + if (sharedData && version) { + try { + const result = await fileManager.importFilesFromUrl(window.location.href); + if (result.success && result.data.length > 0) { + // Open the imported file + await editorState.openFile(result.data[0].id); + // Clean the URL for better UX + window.history.replaceState({}, '', window.location.pathname); + + // Setup keyboard shortcuts and exit + logsUnsubscribe = csoundLogs.subscribe((logs) => { + interpreterLogs = logs; + }); + + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key === '.') { + e.preventDefault(); + handleStop(); + } + + if ((e.ctrlKey || e.metaKey) && e.key === 's') { + e.preventDefault(); + handleSave(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + + return () => { + window.removeEventListener('keydown', handleKeyDown); + }; + } + } catch (error) { + console.error('Failed to import shared file:', error); + // Fall through to normal startup + } + } + // Try to restore open tabs const openTabIds = loadOpenTabs(); const currentFileId = loadCurrentFileId(); @@ -100,6 +150,11 @@ e.preventDefault(); handleSave(); } + + if ((e.ctrlKey || e.metaKey) && e.key === 'r') { + e.preventDefault(); + handleExecuteFile(); + } }; window.addEventListener('keydown', handleKeyDown); @@ -175,6 +230,18 @@ } } + function triggerEvalBlock() { + editorWithLogsRef?.executeBlock(); + } + + function triggerEvalSelection() { + editorWithLogsRef?.executeSelection(); + } + + function triggerEvalFile() { + editorWithLogsRef?.executeFile(); + } + async function handleSave() { await editorState.save(); } @@ -190,6 +257,8 @@ console.error("Failed to copy to clipboard:", err); } uiState.showShare(result.data); + } else { + console.error("Failed to generate share URL:", result.error); } } @@ -201,6 +270,100 @@ } } + async function handleDuplicateFile() { + if (!editorState.currentFile) return; + + const currentFile = editorState.currentFile; + const newTitle = `${currentFile.title.replace(/\.orc$/, '')} copy.orc`; + + const result = await fileManager.createFile({ + title: newTitle, + content: currentFile.content + }); + + if (result.success) { + await editorState.openFile(result.data.id); + } + } + + async function handleExportFile() { + if (!editorState.currentFile) return; + + const file = editorState.currentFile; + const blob = new Blob([file.content], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = file.title; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + + async function handleExportAllFiles() { + const allFilesResult = await fileManager.getAllFiles(); + if (!allFilesResult.success || allFilesResult.data.length === 0) return; + + const JSZip = (await import('https://cdn.jsdelivr.net/npm/jszip@3.10.1/+esm')).default; + const zip = new JSZip(); + + for (const file of allFilesResult.data) { + zip.file(file.title, file.content); + } + + const blob = await zip.generateAsync({ type: 'blob' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'oldboy-files.zip'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + + function handleImportFiles() { + fileInputRef.click(); + } + + async function onFileInputChange(event: Event) { + const input = event.target as HTMLInputElement; + if (!input.files || input.files.length === 0) return; + + const supportedExtensions = ['.orc', '.csd', '.sco']; + let firstImportedId: string | null = null; + + for (const file of Array.from(input.files)) { + const extension = file.name.substring(file.name.lastIndexOf('.')); + + if (!supportedExtensions.includes(extension.toLowerCase())) { + console.warn(`Skipping unsupported file: ${file.name}`); + continue; + } + + try { + const content = await file.text(); + const result = await fileManager.createFile({ + title: file.name, + content: content + }); + + if (result.success && !firstImportedId) { + firstImportedId = result.data.id; + } + } catch (error) { + console.error(`Failed to import ${file.name}:`, error); + } + } + + if (firstImportedId) { + await editorState.openFile(firstImportedId); + } + + input.value = ''; + } + $effect(() => { const theme = themes[$editorSettings.theme]; if (theme) { @@ -265,12 +428,22 @@
uiState.showAboutDialog()}> + {#snippet rightControls()} + + {/snippet} + + + + + + + + + + + @@ -379,10 +589,10 @@ @@ -464,6 +674,15 @@ onClose={() => uiState.hideAboutDialog()} /> + +
diff --git a/src/lib/components/ui/MenuBar.svelte b/src/lib/components/ui/MenuBar.svelte index ffeb149..991a0b4 100644 --- a/src/lib/components/ui/MenuBar.svelte +++ b/src/lib/components/ui/MenuBar.svelte @@ -4,10 +4,11 @@ interface Props { children: Snippet; + rightControls?: Snippet; onLogoClick?: () => void; } - let { children, onLogoClick }: Props = $props(); + let { children, rightControls, onLogoClick }: Props = $props(); + {#if rightControls} + + {/if} diff --git a/src/lib/csound/engine.ts b/src/lib/csound/engine.ts index 3d38080..61a5d9d 100644 --- a/src/lib/csound/engine.ts +++ b/src/lib/csound/engine.ts @@ -32,6 +32,7 @@ export class CsoundEngine { private scopeNode: AnalyserNode | null = null; private audioNode: AudioNode | null = null; private audioContext: AudioContext | null = null; + private gainNode: GainNode | null = null; private useCsound7: boolean; constructor(options: CsoundEngineOptions = {}) { @@ -139,6 +140,7 @@ export class CsoundEngine { this.csound.on('onAudioNodeCreated', (node: AudioNode) => { this.audioNode = node; this.audioContext = node.context as AudioContext; + this.setupGainNode(); this.log('Audio node created and captured'); }); @@ -510,6 +512,26 @@ export class CsoundEngine { return this.audioContext; } + private setupGainNode(): void { + if (!this.audioNode || !this.audioContext || this.gainNode) { + return; + } + + try { + this.gainNode = this.audioContext.createGain(); + this.gainNode.gain.value = 1.0; + + this.audioNode.disconnect(); + this.audioNode.connect(this.gainNode); + this.gainNode.connect(this.audioContext.destination); + + this.log('Gain node created and connected'); + } catch (error) { + console.error('Failed to setup gain node:', error); + this.log('Error setting up gain node: ' + error); + } + } + private setupAnalyser(): void { if (!this.audioNode || !this.audioContext) { this.log('Warning: Audio node not available yet'); @@ -517,13 +539,19 @@ export class CsoundEngine { } try { + if (!this.gainNode) { + this.setupGainNode(); + } + this.scopeNode = this.audioContext.createAnalyser(); this.scopeNode.fftSize = 2048; this.scopeNode.smoothingTimeConstant = 0.3; - this.audioNode.disconnect(); - this.audioNode.connect(this.scopeNode); - this.scopeNode.connect(this.audioContext.destination); + if (this.gainNode) { + this.gainNode.disconnect(); + this.gainNode.connect(this.scopeNode); + this.scopeNode.connect(this.audioContext.destination); + } this.log('Analyser node created and connected'); this.options.onAnalyserNodeCreated?.(this.scopeNode); @@ -537,6 +565,20 @@ export class CsoundEngine { return this.scopeNode; } + setVolume(value: number): void { + if (!this.gainNode) { + return; + } + this.gainNode.gain.value = Math.max(0, Math.min(1, value)); + } + + getVolume(): number { + if (!this.gainNode) { + return 1.0; + } + return this.gainNode.gain.value; + } + private log(message: string): void { this.options.onMessage?.(message); } diff --git a/src/lib/csound/store.ts b/src/lib/csound/store.ts index f4f74a0..7959bf2 100644 --- a/src/lib/csound/store.ts +++ b/src/lib/csound/store.ts @@ -42,6 +42,8 @@ export interface CsoundStore { getAudioContext: () => AudioContext | null; getAnalyserNode: () => AnalyserNode | null; onAnalyserNodeCreated: (callback: (node: AnalyserNode) => void) => () => void; + setVolume: (value: number) => void; + getVolume: () => number; destroy: () => Promise; } @@ -357,6 +359,19 @@ export function createCsoundStore(options: CsoundStoreOptions = {}): CsoundStore return () => analyserNodeListeners.delete(callback); }, + setVolume(value: number): void { + if (engine) { + engine.setVolume(value); + } + }, + + getVolume(): number { + if (engine) { + return engine.getVolume(); + } + return 1.0; + }, + async destroy() { if (engine) { await engine.destroy(); diff --git a/src/lib/project-system/file-manager.ts b/src/lib/project-system/file-manager.ts index 2a65ad1..0509a63 100644 --- a/src/lib/project-system/file-manager.ts +++ b/src/lib/project-system/file-manager.ts @@ -251,9 +251,9 @@ export class FileManager { const files: File[] = []; for (const id of fileIds) { - const file = await this.db.get(id); - if (file) { - files.push(file); + const result = await this.getFile(id); + if (result.success) { + files.push(result.data); } }