Let's fucking go

This commit is contained in:
2025-10-14 23:21:00 +02:00
parent cec7ed8bd4
commit 267323abf2
10 changed files with 994 additions and 45 deletions

View File

@ -6,6 +6,9 @@
import FileBrowser from './lib/FileBrowser.svelte';
import SidePanel from './lib/SidePanel.svelte';
import Popup from './lib/Popup.svelte';
import ResizablePopup from './lib/ResizablePopup.svelte';
import AudioScope from './lib/AudioScope.svelte';
import Spectrogram from './lib/Spectrogram.svelte';
import { csound, csoundLogs, type LogEntry } from './lib/csound';
import { projectManager, type CsoundProject } from './lib/project-system';
import ConfirmDialog from './lib/ConfirmDialog.svelte';
@ -18,13 +21,54 @@
PanelBottomClose,
PanelBottomOpen,
LayoutGrid,
Save
Save,
Share2,
Activity
} from 'lucide-svelte';
let sidePanelVisible = $state(true);
let sidePanelPosition = $state<'left' | 'right' | 'bottom'>('right');
let popupVisible = $state(false);
let editorValue = $state('<CsoundSynthesizer>\n<CsOptions>\n-odac\n</CsOptions>\n<CsInstruments>\n\nsr = 44100\nksmps = 32\nnchnls = 2\n0dbfs = 1\n\ninstr 1\n aOut oscili 0.5, 440\n outs aOut, aOut\nendin\n\n</CsInstruments>\n<CsScore>\ni 1 0 2\n</CsScore>\n</CsoundSynthesizer>\n');
let sharePopupVisible = $state(false);
let shareUrl = $state('');
let audioPermissionPopupVisible = $state(true);
let scopePopupVisible = $state(false);
let spectrogramPopupVisible = $state(false);
let analyserNode = $state<AnalyserNode | null>(null);
let editorValue = $state(`<CsoundSynthesizer>
<CsOptions>
-odac
</CsOptions>
<CsInstruments>
sr = 44100
ksmps = 32
nchnls = 2
0dbfs = 1
instr 1
iFreq = p4
iAmp = p5
; ADSR envelope
kEnv madsr 0.01, 0.1, 0.6, 0.2
; Sine wave oscillator
aOsc oscili iAmp * kEnv, iFreq
outs aOsc, aOsc
endin
</CsInstruments>
<CsScore>
; Arpeggio: C4 E4 G4 C5
i 1 0.0 0.5 261.63 0.3
i 1 0.5 0.5 329.63 0.3
i 1 1.0 0.5 392.00 0.3
i 1 1.5 0.5 523.25 0.3
</CsScore>
</CsoundSynthesizer>
`);
let interpreterLogs = $state<LogEntry[]>([]);
let sidePanelRef: SidePanel;
@ -37,10 +81,42 @@
let showUnsavedDialog = $state(false);
let showSaveAsDialog = $state(false);
const TEMPLATE_CONTENT = '<CsoundSynthesizer>\n<CsOptions>\n-odac\n</CsOptions>\n<CsInstruments>\n\nsr = 44100\nksmps = 32\nnchnls = 2\n0dbfs = 1\n\ninstr 1\n aOut oscili 0.5, 440\n outs aOut, aOut\nendin\n\n</CsInstruments>\n<CsScore>\ni 1 0 2\n</CsScore>\n</CsoundSynthesizer>\n';
const TEMPLATE_CONTENT = `<CsoundSynthesizer>
<CsOptions>
-odac
</CsOptions>
<CsInstruments>
sr = 44100
ksmps = 32
nchnls = 2
0dbfs = 1
instr 1
iFreq = p4
iAmp = p5
; ADSR envelope
kEnv madsr 0.01, 0.1, 0.6, 0.2
; Sine wave oscillator
aOsc oscili iAmp * kEnv, iFreq
outs aOsc, aOsc
endin
</CsInstruments>
<CsScore>
; Arpeggio: C4 E4 G4 C5
i 1 0.0 0.5 261.63 0.3
i 1 0.5 0.5 329.63 0.3
i 1 1.0 0.5 392.00 0.3
i 1 1.5 0.5 523.25 0.3
</CsScore>
</CsoundSynthesizer>
`;
onMount(async () => {
await csound.init();
await projectManager.init();
const result = await projectManager.getAllProjects();
@ -62,6 +138,15 @@
};
});
async function handleEnableAudio() {
try {
await csound.init();
audioPermissionPopupVisible = false;
} catch (error) {
console.error('Failed to initialize audio:', error);
}
}
onDestroy(async () => {
await csound.destroy();
});
@ -203,6 +288,41 @@
}
}
async function handleShare() {
if (!currentProjectId) return;
const result = await projectManager.exportProjectToUrl(currentProjectId);
if (result.success) {
shareUrl = result.data;
try {
await navigator.clipboard.writeText(shareUrl);
} catch (err) {
console.error('Failed to copy to clipboard:', err);
}
sharePopupVisible = true;
}
}
function handleOpenScope() {
scopePopupVisible = true;
}
function handleOpenSpectrogram() {
spectrogramPopupVisible = true;
}
$effect(() => {
if (scopePopupVisible || spectrogramPopupVisible) {
const interval = setInterval(() => {
analyserNode = csound.getAnalyserNode();
}, 100);
return () => clearInterval(interval);
}
});
const panelTabs = [
{
id: 'editor',
@ -232,16 +352,18 @@
{/snippet}
<div class="app-container">
<TopBar title="oldboy">
<button
class="icon-button"
onclick={saveCurrentProject}
disabled={!hasUnsavedChanges}
title="Save {hasUnsavedChanges ? '(unsaved changes)' : ''}"
class:has-changes={hasUnsavedChanges}
>
<Save size={18} />
</button>
<TopBar title="OldBoy">
{#snippet leftActions()}
<button
class="icon-button"
onclick={saveCurrentProject}
disabled={!hasUnsavedChanges}
title="Save {hasUnsavedChanges ? '(unsaved changes)' : ''}"
class:has-changes={hasUnsavedChanges}
>
<Save size={18} />
</button>
{/snippet}
<button onclick={toggleSidePanel} class="icon-button">
{#if sidePanelVisible}
{#if sidePanelPosition === 'left'}
@ -264,6 +386,34 @@
<button onclick={cyclePanelPosition} class="icon-button" title="Change panel position">
<LayoutGrid size={18} />
</button>
<button
onclick={handleShare}
class="icon-button"
disabled={!currentProjectId}
title="Share project"
>
<Share2 size={18} />
</button>
<button
onclick={handleOpenScope}
class="icon-button"
title="Open audio scope"
>
<Activity size={18} />
</button>
<button
onclick={handleOpenSpectrogram}
class="icon-button"
title="Open spectrogram"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<path d="M3 9h18"/>
<path d="M3 15h18"/>
<path d="M9 3v18"/>
<path d="M15 3v18"/>
</svg>
</button>
</TopBar>
<div class="main-content" class:panel-bottom={sidePanelPosition === 'bottom'}>
@ -321,6 +471,88 @@
<p>It stays on top of everything else.</p>
</Popup>
<ResizablePopup
bind:visible={sharePopupVisible}
title="Share Project"
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 300 : 300)}
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 100 : 200)}
width={600}
height={200}
minWidth={400}
minHeight={150}
>
{#snippet children()}
<div class="share-content">
<p>Link copied to clipboard!</p>
<div class="share-url-container">
<input
type="text"
readonly
value={shareUrl}
class="share-url-input"
onclick={(e) => e.currentTarget.select()}
/>
</div>
<p class="share-instructions">Anyone with this link can import the project.</p>
</div>
{/snippet}
</ResizablePopup>
<ResizablePopup
visible={audioPermissionPopupVisible}
title="Audio Permission Required"
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 250 : 250)}
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 125 : 200)}
width={500}
height={250}
minWidth={400}
minHeight={200}
closable={false}
>
{#snippet children()}
<div class="audio-permission-content">
<h3>Enable Audio Context</h3>
<p>OldBoy needs permission to use audio playback.</p>
<p>Click the button below to enable audio and start using the application.</p>
<button class="enable-audio-button" onclick={handleEnableAudio}>
Enable Audio
</button>
</div>
{/snippet}
</ResizablePopup>
<ResizablePopup
bind:visible={scopePopupVisible}
title="Audio Scope"
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 400 : 100)}
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 300 : 100)}
width={800}
height={600}
minWidth={400}
minHeight={300}
noPadding={true}
>
{#snippet children()}
<AudioScope analyserNode={analyserNode} />
{/snippet}
</ResizablePopup>
<ResizablePopup
bind:visible={spectrogramPopupVisible}
title="Spectrogram"
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 400 : 150)}
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 300 : 150)}
width={800}
height={600}
minWidth={400}
minHeight={300}
noPadding={true}
>
{#snippet children()}
<Spectrogram analyserNode={analyserNode} />
{/snippet}
</ResizablePopup>
<ConfirmDialog
bind:visible={showUnsavedDialog}
title="Unsaved Changes"
@ -399,4 +631,75 @@
color: rgba(255, 255, 255, 0.7);
line-height: 1.6;
}
.share-content {
display: flex;
flex-direction: column;
gap: 1rem;
}
.share-url-container {
width: 100%;
}
.share-url-input {
width: 100%;
padding: 0.75rem;
background-color: #2a2a2a;
border: 1px solid #3a3a3a;
color: rgba(255, 255, 255, 0.87);
font-size: 0.875rem;
font-family: monospace;
outline: none;
box-sizing: border-box;
}
.share-url-input:focus {
border-color: #646cff;
}
.share-instructions {
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.5);
margin: 0;
}
.audio-permission-content {
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
text-align: center;
padding: 1rem;
}
.audio-permission-content h3 {
margin: 0;
font-size: 1.25rem;
}
.audio-permission-content p {
margin: 0;
font-size: 0.95rem;
}
.enable-audio-button {
margin-top: 1rem;
padding: 0.75rem 2rem;
background-color: #646cff;
color: white;
border: none;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
}
.enable-audio-button:hover {
background-color: #535bdb;
}
.enable-audio-button:active {
background-color: #424ab8;
}
</style>