Better UI

This commit is contained in:
2025-10-15 11:27:58 +02:00
parent bbdb01200e
commit 65c3422610
6 changed files with 270 additions and 183 deletions

View File

@ -1,38 +1,48 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import TopBar from './lib/components/ui/TopBar.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';
import SidePanel from './lib/components/ui/SidePanel.svelte';
import ResizablePopup from './lib/components/ui/ResizablePopup.svelte';
import AudioScope from './lib/components/audio/AudioScope.svelte';
import Spectrogram from './lib/components/audio/Spectrogram.svelte';
import ConfirmDialog from './lib/components/ui/ConfirmDialog.svelte';
import InputDialog from './lib/components/ui/InputDialog.svelte';
import TemplateDialog from './lib/components/ui/TemplateDialog.svelte';
import { createCsoundDerivedStores, type LogEntry, type EvalSource } from './lib/csound';
import { type CsoundProject } from './lib/project-system';
import { templateRegistry, type CsoundTemplate } from './lib/templates/template-registry';
import { createAppContext, setAppContext } from './lib/contexts/app-context';
import type { ProjectMode } from './lib/project-system/types';
import { onMount, onDestroy } from "svelte";
import TopBar from "./lib/components/ui/TopBar.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";
import SidePanel from "./lib/components/ui/SidePanel.svelte";
import ResizablePopup from "./lib/components/ui/ResizablePopup.svelte";
import AudioScope from "./lib/components/audio/AudioScope.svelte";
import Spectrogram from "./lib/components/audio/Spectrogram.svelte";
import ConfirmDialog from "./lib/components/ui/ConfirmDialog.svelte";
import InputDialog from "./lib/components/ui/InputDialog.svelte";
import TemplateDialog from "./lib/components/ui/TemplateDialog.svelte";
import {
createCsoundDerivedStores,
type LogEntry,
type EvalSource,
} from "./lib/csound";
import { type CsoundProject } from "./lib/project-system";
import {
templateRegistry,
type CsoundTemplate,
} from "./lib/templates/template-registry";
import { createAppContext, setAppContext } from "./lib/contexts/app-context";
import type { ProjectMode } from "./lib/project-system/types";
import {
PanelLeftClose,
PanelLeftOpen,
PanelRightClose,
PanelRightOpen,
PanelBottomClose,
PanelBottomOpen,
LayoutGrid,
Save,
Share2,
Activity
} from 'lucide-svelte';
Activity,
FileStack,
PanelLeftOpen,
PanelRightOpen,
} from "lucide-svelte";
const appContext = createAppContext();
setAppContext(appContext);
const { csound, projectManager, editorSettings, projectEditor, uiState, executionContext } = appContext;
const {
csound,
projectManager,
editorSettings,
projectEditor,
uiState,
executionContext,
} = appContext;
const csoundDerived = createCsoundDerivedStores(csound);
let analyserNode = $state<AnalyserNode | null>(null);
@ -45,19 +55,19 @@
const result = await projectManager.getAllProjects();
if (result.success && result.data.length === 0) {
const classicTemplate = templateRegistry.getById('classic');
const classicTemplate = templateRegistry.getById("classic");
if (classicTemplate) {
await projectManager.createProject({
title: 'Welcome',
author: 'System',
title: "Welcome",
author: "System",
content: classicTemplate.content,
tags: [],
mode: classicTemplate.mode
mode: classicTemplate.mode,
});
}
}
logsUnsubscribe = csoundDerived.logs.subscribe(logs => {
logsUnsubscribe = csoundDerived.logs.subscribe((logs) => {
interpreterLogs = logs;
});
});
@ -72,7 +82,7 @@
await csound.init();
uiState.closeAudioPermission();
} catch (error) {
console.error('Failed to initialize audio:', error);
console.error("Failed to initialize audio:", error);
}
}
@ -82,11 +92,11 @@
function handleNewEmptyFile() {
const emptyTemplate = templateRegistry.getEmpty();
const result = projectEditor.requestSwitch(
() => projectEditor.createNew(emptyTemplate.content)
const result = projectEditor.requestSwitch(() =>
projectEditor.createNew(emptyTemplate.content),
);
if (result === 'confirm-unsaved') {
if (result === "confirm-unsaved") {
uiState.showUnsavedChangesDialog();
}
}
@ -98,11 +108,11 @@
function handleTemplateSelect(template: CsoundTemplate) {
uiState.hideTemplateDialog();
const result = projectEditor.requestSwitch(
() => projectEditor.createNew(template.content)
const result = projectEditor.requestSwitch(() =>
projectEditor.createNew(template.content),
);
if (result === 'confirm-unsaved') {
if (result === "confirm-unsaved") {
uiState.showUnsavedChangesDialog();
}
}
@ -110,11 +120,11 @@
function handleFileSelect(project: CsoundProject | null) {
if (!project) return;
const result = projectEditor.requestSwitch(
() => projectEditor.loadProject(project)
const result = projectEditor.requestSwitch(() =>
projectEditor.loadProject(project),
);
if (result === 'confirm-unsaved') {
if (result === "confirm-unsaved") {
uiState.showUnsavedChangesDialog();
}
}
@ -123,7 +133,7 @@
try {
await executionContext.execute(code, source);
} catch (error) {
console.error('Execution error:', error);
console.error("Execution error:", error);
}
}
@ -143,14 +153,21 @@
}
}
async function handleMetadataUpdate(projectId: string, updates: { title?: string; author?: string; mode?: import('./lib/project-system/types').ProjectMode }) {
async function handleMetadataUpdate(
projectId: string,
updates: {
title?: string;
author?: string;
mode?: import("./lib/project-system/types").ProjectMode;
},
) {
await projectEditor.updateMetadata(updates);
}
async function handleSwitchSave() {
const result = await projectEditor.confirmSaveAndSwitch();
if (result === 'show-save-as') {
if (result === "show-save-as") {
uiState.hideUnsavedChangesDialog();
uiState.showSaveAsDialog();
} else {
@ -166,12 +183,14 @@
async function handleShare() {
if (!projectEditor.currentProjectId) return;
const result = await projectManager.exportProjectToUrl(projectEditor.currentProjectId);
const result = await projectManager.exportProjectToUrl(
projectEditor.currentProjectId,
);
if (result.success) {
try {
await navigator.clipboard.writeText(result.data);
} catch (err) {
console.error('Failed to copy to clipboard:', err);
console.error("Failed to copy to clipboard:", err);
}
uiState.showShare(result.data);
}
@ -192,7 +211,7 @@
});
$effect(() => {
const mode = projectEditor.currentProject?.mode || 'composition';
const mode = projectEditor.currentProject?.mode || "composition";
const projectId = projectEditor.currentProjectId;
if (mode !== executionContext.mode) {
@ -208,15 +227,15 @@
const panelTabs = [
{
id: 'editor',
label: 'Editor',
content: editorTabContent
id: "editor",
label: "Editor",
content: editorTabContent,
},
{
id: 'files',
label: 'Files',
content: filesTabContent
}
id: "files",
label: "Files",
content: filesTabContent,
},
];
</script>
@ -238,46 +257,33 @@
<div class="app-container">
<TopBar title="OldBoy">
{#snippet leftActions()}
<button
class="icon-button"
onclick={handleNewFromTemplate}
title="New from template"
>
<FileStack size={18} />
</button>
<button
class="icon-button"
onclick={handleSave}
disabled={!projectEditor.hasUnsavedChanges}
title="Save {projectEditor.hasUnsavedChanges ? '(unsaved changes)' : ''}"
title="Save {projectEditor.hasUnsavedChanges
? '(unsaved changes)'
: ''}"
class:has-changes={projectEditor.hasUnsavedChanges}
>
<Save size={18} />
</button>
<button
onclick={handleShare}
class="icon-button"
disabled={!projectEditor.currentProjectId}
title="Share project"
>
<Share2 size={18} />
</button>
{/snippet}
<button onclick={() => uiState.toggleSidePanel()} class="icon-button">
{#if uiState.sidePanelVisible}
{#if uiState.sidePanelPosition === 'left'}
<PanelLeftClose size={18} />
{:else if uiState.sidePanelPosition === 'right'}
<PanelRightClose size={18} />
{:else}
<PanelBottomClose size={18} />
{/if}
{:else}
{#if uiState.sidePanelPosition === 'left'}
<PanelLeftOpen size={18} />
{:else if uiState.sidePanelPosition === 'right'}
<PanelRightOpen size={18} />
{:else}
<PanelBottomOpen size={18} />
{/if}
{/if}
</button>
<button onclick={() => uiState.cyclePanelPosition()} class="icon-button" title="Change panel position">
<LayoutGrid size={18} />
</button>
<button
onclick={handleShare}
class="icon-button"
disabled={!projectEditor.currentProjectId}
title="Share project"
>
<Share2 size={18} />
</button>
<button
onclick={() => uiState.openScope()}
class="icon-button"
@ -290,22 +296,44 @@
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
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>
{#if !uiState.sidePanelVisible}
<button
class="icon-button"
onclick={() => uiState.toggleSidePanel()}
title="Open side panel"
>
{#if uiState.sidePanelPosition === "left"}
<PanelLeftOpen size={18} />
{:else}
<PanelRightOpen size={18} />
{/if}
</button>
{/if}
</TopBar>
<div class="main-content" class:panel-bottom={uiState.sidePanelPosition === 'bottom'}>
{#if uiState.sidePanelPosition === 'left'}
<div class="main-content">
{#if uiState.sidePanelPosition === "left"}
<SidePanel
bind:visible={uiState.sidePanelVisible}
bind:position={uiState.sidePanelPosition}
tabs={panelTabs}
onClose={() => uiState.toggleSidePanel()}
onCyclePosition={() => uiState.cyclePanelPosition()}
/>
{/if}
@ -320,22 +348,13 @@
/>
</div>
{#if uiState.sidePanelPosition === 'right'}
{#if uiState.sidePanelPosition === "right"}
<SidePanel
bind:visible={uiState.sidePanelVisible}
bind:position={uiState.sidePanelPosition}
tabs={panelTabs}
/>
{/if}
{#if uiState.sidePanelPosition === 'bottom'}
<SidePanel
bind:visible={uiState.sidePanelVisible}
bind:position={uiState.sidePanelPosition}
initialWidth={200}
minWidth={100}
maxWidth={400}
tabs={panelTabs}
onClose={() => uiState.toggleSidePanel()}
onCyclePosition={() => uiState.cyclePanelPosition()}
/>
{/if}
</div>
@ -343,8 +362,8 @@
<ResizablePopup
bind:visible={uiState.sharePopupVisible}
title="Share Project"
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 300 : 300)}
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 100 : 200)}
x={typeof window !== "undefined" ? window.innerWidth / 2 - 300 : 300}
y={typeof window !== "undefined" ? window.innerHeight / 2 - 100 : 200}
width={600}
height={200}
minWidth={400}
@ -362,7 +381,9 @@
onclick={(e) => e.currentTarget.select()}
/>
</div>
<p class="share-instructions">Anyone with this link can import the project.</p>
<p class="share-instructions">
Anyone with this link can import the project.
</p>
</div>
{/snippet}
</ResizablePopup>
@ -370,10 +391,10 @@
<ResizablePopup
visible={uiState.audioPermissionPopupVisible}
title="Audio Permission Required"
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 250 : 250)}
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 175 : 200)}
x={typeof window !== "undefined" ? window.innerWidth / 2 - 250 : 250}
y={typeof window !== "undefined" ? window.innerHeight / 2 - 175 : 200}
width={500}
height={350}
height={270}
minWidth={400}
minHeight={300}
closable={false}
@ -382,7 +403,6 @@
<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>
@ -393,8 +413,8 @@
<ResizablePopup
bind:visible={uiState.scopePopupVisible}
title="Audio Scope"
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 400 : 100)}
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 300 : 100)}
x={typeof window !== "undefined" ? window.innerWidth / 2 - 400 : 100}
y={typeof window !== "undefined" ? window.innerHeight / 2 - 300 : 100}
width={800}
height={600}
minWidth={400}
@ -402,15 +422,15 @@
noPadding={true}
>
{#snippet children()}
<AudioScope analyserNode={analyserNode} />
<AudioScope {analyserNode} />
{/snippet}
</ResizablePopup>
<ResizablePopup
bind:visible={uiState.spectrogramPopupVisible}
title="Spectrogram"
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 400 : 150)}
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 300 : 150)}
x={typeof window !== "undefined" ? window.innerWidth / 2 - 400 : 150}
y={typeof window !== "undefined" ? window.innerHeight / 2 - 300 : 150}
width={800}
height={600}
minWidth={400}
@ -418,7 +438,7 @@
noPadding={true}
>
{#snippet children()}
<Spectrogram analyserNode={analyserNode} />
<Spectrogram {analyserNode} />
{/snippet}
</ResizablePopup>
@ -462,10 +482,6 @@
overflow: hidden;
}
.main-content.panel-bottom {
flex-direction: column;
}
.editor-area {
flex: 1;
overflow: hidden;
@ -578,4 +594,3 @@
background-color: #424ab8;
}
</style>