Better UI
This commit is contained in:
249
src/App.svelte
249
src/App.svelte
@ -1,38 +1,48 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from "svelte";
|
||||||
import TopBar from './lib/components/ui/TopBar.svelte';
|
import TopBar from "./lib/components/ui/TopBar.svelte";
|
||||||
import EditorWithLogs from './lib/components/editor/EditorWithLogs.svelte';
|
import EditorWithLogs from "./lib/components/editor/EditorWithLogs.svelte";
|
||||||
import EditorSettings from './lib/components/editor/EditorSettings.svelte';
|
import EditorSettings from "./lib/components/editor/EditorSettings.svelte";
|
||||||
import FileBrowser from './lib/components/ui/FileBrowser.svelte';
|
import FileBrowser from "./lib/components/ui/FileBrowser.svelte";
|
||||||
import SidePanel from './lib/components/ui/SidePanel.svelte';
|
import SidePanel from "./lib/components/ui/SidePanel.svelte";
|
||||||
import ResizablePopup from './lib/components/ui/ResizablePopup.svelte';
|
import ResizablePopup from "./lib/components/ui/ResizablePopup.svelte";
|
||||||
import AudioScope from './lib/components/audio/AudioScope.svelte';
|
import AudioScope from "./lib/components/audio/AudioScope.svelte";
|
||||||
import Spectrogram from './lib/components/audio/Spectrogram.svelte';
|
import Spectrogram from "./lib/components/audio/Spectrogram.svelte";
|
||||||
import ConfirmDialog from './lib/components/ui/ConfirmDialog.svelte';
|
import ConfirmDialog from "./lib/components/ui/ConfirmDialog.svelte";
|
||||||
import InputDialog from './lib/components/ui/InputDialog.svelte';
|
import InputDialog from "./lib/components/ui/InputDialog.svelte";
|
||||||
import TemplateDialog from './lib/components/ui/TemplateDialog.svelte';
|
import TemplateDialog from "./lib/components/ui/TemplateDialog.svelte";
|
||||||
import { createCsoundDerivedStores, type LogEntry, type EvalSource } from './lib/csound';
|
import {
|
||||||
import { type CsoundProject } from './lib/project-system';
|
createCsoundDerivedStores,
|
||||||
import { templateRegistry, type CsoundTemplate } from './lib/templates/template-registry';
|
type LogEntry,
|
||||||
import { createAppContext, setAppContext } from './lib/contexts/app-context';
|
type EvalSource,
|
||||||
import type { ProjectMode } from './lib/project-system/types';
|
} 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 {
|
import {
|
||||||
PanelLeftClose,
|
|
||||||
PanelLeftOpen,
|
|
||||||
PanelRightClose,
|
|
||||||
PanelRightOpen,
|
|
||||||
PanelBottomClose,
|
|
||||||
PanelBottomOpen,
|
|
||||||
LayoutGrid,
|
|
||||||
Save,
|
Save,
|
||||||
Share2,
|
Share2,
|
||||||
Activity
|
Activity,
|
||||||
} from 'lucide-svelte';
|
FileStack,
|
||||||
|
PanelLeftOpen,
|
||||||
|
PanelRightOpen,
|
||||||
|
} from "lucide-svelte";
|
||||||
|
|
||||||
const appContext = createAppContext();
|
const appContext = createAppContext();
|
||||||
setAppContext(appContext);
|
setAppContext(appContext);
|
||||||
|
|
||||||
const { csound, projectManager, editorSettings, projectEditor, uiState, executionContext } = appContext;
|
const {
|
||||||
|
csound,
|
||||||
|
projectManager,
|
||||||
|
editorSettings,
|
||||||
|
projectEditor,
|
||||||
|
uiState,
|
||||||
|
executionContext,
|
||||||
|
} = appContext;
|
||||||
const csoundDerived = createCsoundDerivedStores(csound);
|
const csoundDerived = createCsoundDerivedStores(csound);
|
||||||
|
|
||||||
let analyserNode = $state<AnalyserNode | null>(null);
|
let analyserNode = $state<AnalyserNode | null>(null);
|
||||||
@ -45,19 +55,19 @@
|
|||||||
|
|
||||||
const result = await projectManager.getAllProjects();
|
const result = await projectManager.getAllProjects();
|
||||||
if (result.success && result.data.length === 0) {
|
if (result.success && result.data.length === 0) {
|
||||||
const classicTemplate = templateRegistry.getById('classic');
|
const classicTemplate = templateRegistry.getById("classic");
|
||||||
if (classicTemplate) {
|
if (classicTemplate) {
|
||||||
await projectManager.createProject({
|
await projectManager.createProject({
|
||||||
title: 'Welcome',
|
title: "Welcome",
|
||||||
author: 'System',
|
author: "System",
|
||||||
content: classicTemplate.content,
|
content: classicTemplate.content,
|
||||||
tags: [],
|
tags: [],
|
||||||
mode: classicTemplate.mode
|
mode: classicTemplate.mode,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logsUnsubscribe = csoundDerived.logs.subscribe(logs => {
|
logsUnsubscribe = csoundDerived.logs.subscribe((logs) => {
|
||||||
interpreterLogs = logs;
|
interpreterLogs = logs;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -72,7 +82,7 @@
|
|||||||
await csound.init();
|
await csound.init();
|
||||||
uiState.closeAudioPermission();
|
uiState.closeAudioPermission();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize audio:', error);
|
console.error("Failed to initialize audio:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,11 +92,11 @@
|
|||||||
|
|
||||||
function handleNewEmptyFile() {
|
function handleNewEmptyFile() {
|
||||||
const emptyTemplate = templateRegistry.getEmpty();
|
const emptyTemplate = templateRegistry.getEmpty();
|
||||||
const result = projectEditor.requestSwitch(
|
const result = projectEditor.requestSwitch(() =>
|
||||||
() => projectEditor.createNew(emptyTemplate.content)
|
projectEditor.createNew(emptyTemplate.content),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result === 'confirm-unsaved') {
|
if (result === "confirm-unsaved") {
|
||||||
uiState.showUnsavedChangesDialog();
|
uiState.showUnsavedChangesDialog();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -98,11 +108,11 @@
|
|||||||
function handleTemplateSelect(template: CsoundTemplate) {
|
function handleTemplateSelect(template: CsoundTemplate) {
|
||||||
uiState.hideTemplateDialog();
|
uiState.hideTemplateDialog();
|
||||||
|
|
||||||
const result = projectEditor.requestSwitch(
|
const result = projectEditor.requestSwitch(() =>
|
||||||
() => projectEditor.createNew(template.content)
|
projectEditor.createNew(template.content),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result === 'confirm-unsaved') {
|
if (result === "confirm-unsaved") {
|
||||||
uiState.showUnsavedChangesDialog();
|
uiState.showUnsavedChangesDialog();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -110,11 +120,11 @@
|
|||||||
function handleFileSelect(project: CsoundProject | null) {
|
function handleFileSelect(project: CsoundProject | null) {
|
||||||
if (!project) return;
|
if (!project) return;
|
||||||
|
|
||||||
const result = projectEditor.requestSwitch(
|
const result = projectEditor.requestSwitch(() =>
|
||||||
() => projectEditor.loadProject(project)
|
projectEditor.loadProject(project),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result === 'confirm-unsaved') {
|
if (result === "confirm-unsaved") {
|
||||||
uiState.showUnsavedChangesDialog();
|
uiState.showUnsavedChangesDialog();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -123,7 +133,7 @@
|
|||||||
try {
|
try {
|
||||||
await executionContext.execute(code, source);
|
await executionContext.execute(code, source);
|
||||||
} catch (error) {
|
} 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);
|
await projectEditor.updateMetadata(updates);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSwitchSave() {
|
async function handleSwitchSave() {
|
||||||
const result = await projectEditor.confirmSaveAndSwitch();
|
const result = await projectEditor.confirmSaveAndSwitch();
|
||||||
|
|
||||||
if (result === 'show-save-as') {
|
if (result === "show-save-as") {
|
||||||
uiState.hideUnsavedChangesDialog();
|
uiState.hideUnsavedChangesDialog();
|
||||||
uiState.showSaveAsDialog();
|
uiState.showSaveAsDialog();
|
||||||
} else {
|
} else {
|
||||||
@ -166,12 +183,14 @@
|
|||||||
async function handleShare() {
|
async function handleShare() {
|
||||||
if (!projectEditor.currentProjectId) return;
|
if (!projectEditor.currentProjectId) return;
|
||||||
|
|
||||||
const result = await projectManager.exportProjectToUrl(projectEditor.currentProjectId);
|
const result = await projectManager.exportProjectToUrl(
|
||||||
|
projectEditor.currentProjectId,
|
||||||
|
);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(result.data);
|
await navigator.clipboard.writeText(result.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to copy to clipboard:', err);
|
console.error("Failed to copy to clipboard:", err);
|
||||||
}
|
}
|
||||||
uiState.showShare(result.data);
|
uiState.showShare(result.data);
|
||||||
}
|
}
|
||||||
@ -192,7 +211,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const mode = projectEditor.currentProject?.mode || 'composition';
|
const mode = projectEditor.currentProject?.mode || "composition";
|
||||||
const projectId = projectEditor.currentProjectId;
|
const projectId = projectEditor.currentProjectId;
|
||||||
|
|
||||||
if (mode !== executionContext.mode) {
|
if (mode !== executionContext.mode) {
|
||||||
@ -208,15 +227,15 @@
|
|||||||
|
|
||||||
const panelTabs = [
|
const panelTabs = [
|
||||||
{
|
{
|
||||||
id: 'editor',
|
id: "editor",
|
||||||
label: 'Editor',
|
label: "Editor",
|
||||||
content: editorTabContent
|
content: editorTabContent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'files',
|
id: "files",
|
||||||
label: 'Files',
|
label: "Files",
|
||||||
content: filesTabContent
|
content: filesTabContent,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -238,38 +257,24 @@
|
|||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<TopBar title="OldBoy">
|
<TopBar title="OldBoy">
|
||||||
{#snippet leftActions()}
|
{#snippet leftActions()}
|
||||||
|
<button
|
||||||
|
class="icon-button"
|
||||||
|
onclick={handleNewFromTemplate}
|
||||||
|
title="New from template"
|
||||||
|
>
|
||||||
|
<FileStack size={18} />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
class="icon-button"
|
class="icon-button"
|
||||||
onclick={handleSave}
|
onclick={handleSave}
|
||||||
disabled={!projectEditor.hasUnsavedChanges}
|
disabled={!projectEditor.hasUnsavedChanges}
|
||||||
title="Save {projectEditor.hasUnsavedChanges ? '(unsaved changes)' : ''}"
|
title="Save {projectEditor.hasUnsavedChanges
|
||||||
|
? '(unsaved changes)'
|
||||||
|
: ''}"
|
||||||
class:has-changes={projectEditor.hasUnsavedChanges}
|
class:has-changes={projectEditor.hasUnsavedChanges}
|
||||||
>
|
>
|
||||||
<Save size={18} />
|
<Save size={18} />
|
||||||
</button>
|
</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
|
<button
|
||||||
onclick={handleShare}
|
onclick={handleShare}
|
||||||
class="icon-button"
|
class="icon-button"
|
||||||
@ -278,6 +283,7 @@
|
|||||||
>
|
>
|
||||||
<Share2 size={18} />
|
<Share2 size={18} />
|
||||||
</button>
|
</button>
|
||||||
|
{/snippet}
|
||||||
<button
|
<button
|
||||||
onclick={() => uiState.openScope()}
|
onclick={() => uiState.openScope()}
|
||||||
class="icon-button"
|
class="icon-button"
|
||||||
@ -290,22 +296,44 @@
|
|||||||
class="icon-button"
|
class="icon-button"
|
||||||
title="Open spectrogram"
|
title="Open spectrogram"
|
||||||
>
|
>
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg
|
||||||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
width="18"
|
||||||
<path d="M3 9h18"/>
|
height="18"
|
||||||
<path d="M3 15h18"/>
|
viewBox="0 0 24 24"
|
||||||
<path d="M9 3v18"/>
|
fill="none"
|
||||||
<path d="M15 3v18"/>
|
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>
|
</svg>
|
||||||
</button>
|
</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>
|
</TopBar>
|
||||||
|
|
||||||
<div class="main-content" class:panel-bottom={uiState.sidePanelPosition === 'bottom'}>
|
<div class="main-content">
|
||||||
{#if uiState.sidePanelPosition === 'left'}
|
{#if uiState.sidePanelPosition === "left"}
|
||||||
<SidePanel
|
<SidePanel
|
||||||
bind:visible={uiState.sidePanelVisible}
|
bind:visible={uiState.sidePanelVisible}
|
||||||
bind:position={uiState.sidePanelPosition}
|
bind:position={uiState.sidePanelPosition}
|
||||||
tabs={panelTabs}
|
tabs={panelTabs}
|
||||||
|
onClose={() => uiState.toggleSidePanel()}
|
||||||
|
onCyclePosition={() => uiState.cyclePanelPosition()}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@ -320,22 +348,13 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if uiState.sidePanelPosition === 'right'}
|
{#if uiState.sidePanelPosition === "right"}
|
||||||
<SidePanel
|
<SidePanel
|
||||||
bind:visible={uiState.sidePanelVisible}
|
bind:visible={uiState.sidePanelVisible}
|
||||||
bind:position={uiState.sidePanelPosition}
|
bind:position={uiState.sidePanelPosition}
|
||||||
tabs={panelTabs}
|
tabs={panelTabs}
|
||||||
/>
|
onClose={() => uiState.toggleSidePanel()}
|
||||||
{/if}
|
onCyclePosition={() => uiState.cyclePanelPosition()}
|
||||||
|
|
||||||
{#if uiState.sidePanelPosition === 'bottom'}
|
|
||||||
<SidePanel
|
|
||||||
bind:visible={uiState.sidePanelVisible}
|
|
||||||
bind:position={uiState.sidePanelPosition}
|
|
||||||
initialWidth={200}
|
|
||||||
minWidth={100}
|
|
||||||
maxWidth={400}
|
|
||||||
tabs={panelTabs}
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@ -343,8 +362,8 @@
|
|||||||
<ResizablePopup
|
<ResizablePopup
|
||||||
bind:visible={uiState.sharePopupVisible}
|
bind:visible={uiState.sharePopupVisible}
|
||||||
title="Share Project"
|
title="Share Project"
|
||||||
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 300 : 300)}
|
x={typeof window !== "undefined" ? window.innerWidth / 2 - 300 : 300}
|
||||||
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 100 : 200)}
|
y={typeof window !== "undefined" ? window.innerHeight / 2 - 100 : 200}
|
||||||
width={600}
|
width={600}
|
||||||
height={200}
|
height={200}
|
||||||
minWidth={400}
|
minWidth={400}
|
||||||
@ -362,7 +381,9 @@
|
|||||||
onclick={(e) => e.currentTarget.select()}
|
onclick={(e) => e.currentTarget.select()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ResizablePopup>
|
</ResizablePopup>
|
||||||
@ -370,10 +391,10 @@
|
|||||||
<ResizablePopup
|
<ResizablePopup
|
||||||
visible={uiState.audioPermissionPopupVisible}
|
visible={uiState.audioPermissionPopupVisible}
|
||||||
title="Audio Permission Required"
|
title="Audio Permission Required"
|
||||||
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 250 : 250)}
|
x={typeof window !== "undefined" ? window.innerWidth / 2 - 250 : 250}
|
||||||
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 175 : 200)}
|
y={typeof window !== "undefined" ? window.innerHeight / 2 - 175 : 200}
|
||||||
width={500}
|
width={500}
|
||||||
height={350}
|
height={270}
|
||||||
minWidth={400}
|
minWidth={400}
|
||||||
minHeight={300}
|
minHeight={300}
|
||||||
closable={false}
|
closable={false}
|
||||||
@ -382,7 +403,6 @@
|
|||||||
<div class="audio-permission-content">
|
<div class="audio-permission-content">
|
||||||
<h3>Enable Audio Context</h3>
|
<h3>Enable Audio Context</h3>
|
||||||
<p>OldBoy needs permission to use audio playback.</p>
|
<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}>
|
<button class="enable-audio-button" onclick={handleEnableAudio}>
|
||||||
Enable Audio
|
Enable Audio
|
||||||
</button>
|
</button>
|
||||||
@ -393,8 +413,8 @@
|
|||||||
<ResizablePopup
|
<ResizablePopup
|
||||||
bind:visible={uiState.scopePopupVisible}
|
bind:visible={uiState.scopePopupVisible}
|
||||||
title="Audio Scope"
|
title="Audio Scope"
|
||||||
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 400 : 100)}
|
x={typeof window !== "undefined" ? window.innerWidth / 2 - 400 : 100}
|
||||||
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 300 : 100)}
|
y={typeof window !== "undefined" ? window.innerHeight / 2 - 300 : 100}
|
||||||
width={800}
|
width={800}
|
||||||
height={600}
|
height={600}
|
||||||
minWidth={400}
|
minWidth={400}
|
||||||
@ -402,15 +422,15 @@
|
|||||||
noPadding={true}
|
noPadding={true}
|
||||||
>
|
>
|
||||||
{#snippet children()}
|
{#snippet children()}
|
||||||
<AudioScope analyserNode={analyserNode} />
|
<AudioScope {analyserNode} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ResizablePopup>
|
</ResizablePopup>
|
||||||
|
|
||||||
<ResizablePopup
|
<ResizablePopup
|
||||||
bind:visible={uiState.spectrogramPopupVisible}
|
bind:visible={uiState.spectrogramPopupVisible}
|
||||||
title="Spectrogram"
|
title="Spectrogram"
|
||||||
x={(typeof window !== 'undefined' ? window.innerWidth / 2 - 400 : 150)}
|
x={typeof window !== "undefined" ? window.innerWidth / 2 - 400 : 150}
|
||||||
y={(typeof window !== 'undefined' ? window.innerHeight / 2 - 300 : 150)}
|
y={typeof window !== "undefined" ? window.innerHeight / 2 - 300 : 150}
|
||||||
width={800}
|
width={800}
|
||||||
height={600}
|
height={600}
|
||||||
minWidth={400}
|
minWidth={400}
|
||||||
@ -418,7 +438,7 @@
|
|||||||
noPadding={true}
|
noPadding={true}
|
||||||
>
|
>
|
||||||
{#snippet children()}
|
{#snippet children()}
|
||||||
<Spectrogram analyserNode={analyserNode} />
|
<Spectrogram {analyserNode} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ResizablePopup>
|
</ResizablePopup>
|
||||||
|
|
||||||
@ -462,10 +482,6 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content.panel-bottom {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-area {
|
.editor-area {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -578,4 +594,3 @@
|
|||||||
background-color: #424ab8;
|
background-color: #424ab8;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@ -28,6 +28,8 @@
|
|||||||
let isResizing = $state(false);
|
let isResizing = $state(false);
|
||||||
let startY = $state(0);
|
let startY = $state(0);
|
||||||
let startHeight = $state(0);
|
let startHeight = $state(0);
|
||||||
|
let previousHeight = $state(70);
|
||||||
|
let isCollapsed = $state(false);
|
||||||
|
|
||||||
function handleResizeStart(e: MouseEvent) {
|
function handleResizeStart(e: MouseEvent) {
|
||||||
isResizing = true;
|
isResizing = true;
|
||||||
@ -54,6 +56,17 @@
|
|||||||
isResizing = false;
|
isResizing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleCollapse() {
|
||||||
|
if (isCollapsed) {
|
||||||
|
editorHeight = previousHeight;
|
||||||
|
isCollapsed = false;
|
||||||
|
} else {
|
||||||
|
previousHeight = editorHeight;
|
||||||
|
editorHeight = 96;
|
||||||
|
isCollapsed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
document.addEventListener('mousemove', handleResizeMove);
|
document.addEventListener('mousemove', handleResizeMove);
|
||||||
document.addEventListener('mouseup', handleResizeEnd);
|
document.addEventListener('mouseup', handleResizeEnd);
|
||||||
@ -80,7 +93,7 @@
|
|||||||
<div class="resize-divider" onmousedown={handleResizeStart}></div>
|
<div class="resize-divider" onmousedown={handleResizeStart}></div>
|
||||||
|
|
||||||
<div class="logs-section" style="height: {100 - editorHeight}%;">
|
<div class="logs-section" style="height: {100 - editorHeight}%;">
|
||||||
<LogPanel bind:this={logPanelRef} {logs} />
|
<LogPanel bind:this={logPanelRef} {logs} onHeaderClick={toggleCollapse} collapsed={isCollapsed} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -6,9 +6,11 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
logs?: LogEntry[];
|
logs?: LogEntry[];
|
||||||
|
onHeaderClick?: () => void;
|
||||||
|
collapsed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { logs = [] }: Props = $props();
|
let { logs = [], onHeaderClick, collapsed = false }: Props = $props();
|
||||||
|
|
||||||
const { csound } = getAppContext();
|
const { csound } = getAppContext();
|
||||||
|
|
||||||
@ -109,9 +111,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="log-panel">
|
<div class="log-panel">
|
||||||
<div class="log-header">
|
<div class="log-header" onclick={onHeaderClick}>
|
||||||
<span class="log-title">Output</span>
|
<span class="log-title">Logs</span>
|
||||||
<div class="log-actions">
|
{#if !collapsed}
|
||||||
|
<div class="log-actions" onclick={(e) => e.stopPropagation()}>
|
||||||
<button
|
<button
|
||||||
class="action-button"
|
class="action-button"
|
||||||
class:search-active={searchVisible}
|
class:search-active={searchVisible}
|
||||||
@ -149,8 +152,9 @@
|
|||||||
<Trash2 size={14} />
|
<Trash2 size={14} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if searchVisible}
|
{#if searchVisible && !collapsed}
|
||||||
<div class="search-bar">
|
<div class="search-bar">
|
||||||
<Search size={14} class="search-icon" />
|
<Search size={14} class="search-icon" />
|
||||||
<input
|
<input
|
||||||
@ -161,6 +165,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if !collapsed}
|
||||||
<div class="log-content" bind:this={logContentEl} onscroll={handleScroll}>
|
<div class="log-content" bind:this={logContentEl} onscroll={handleScroll}>
|
||||||
{#if filteredLogs.length === 0 && searchQuery.trim() !== ''}
|
{#if filteredLogs.length === 0 && searchQuery.trim() !== ''}
|
||||||
<div class="empty-state">No matching logs found...</div>
|
<div class="empty-state">No matching logs found...</div>
|
||||||
@ -175,6 +180,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@ -192,6 +198,12 @@
|
|||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 0.75rem;
|
||||||
background-color: #2a2a2a;
|
background-color: #2a2a2a;
|
||||||
border-bottom: 1px solid #3a3a3a;
|
border-bottom: 1px solid #3a3a3a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-header:hover {
|
||||||
|
background-color: #323232;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar {
|
.search-bar {
|
||||||
|
|||||||
@ -130,7 +130,7 @@
|
|||||||
<button class="action-button" onclick={handleNewEmptyFile} title="New empty file">
|
<button class="action-button" onclick={handleNewEmptyFile} title="New empty file">
|
||||||
<FilePlus size={18} />
|
<FilePlus size={18} />
|
||||||
</button>
|
</button>
|
||||||
<button class="action-button template-button" onclick={handleNewFromTemplate} title="New from template">
|
<button class="action-button" onclick={handleNewFromTemplate} title="New from template">
|
||||||
<FileStack size={18} />
|
<FileStack size={18} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -277,15 +277,6 @@
|
|||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-button.template-button {
|
|
||||||
color: rgba(100, 200, 255, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-button.template-button:hover {
|
|
||||||
color: rgba(100, 200, 255, 1);
|
|
||||||
background-color: rgba(100, 200, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.browser-content {
|
.browser-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { X, ArrowLeftRight } from 'lucide-svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
@ -8,6 +9,8 @@
|
|||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
position?: 'left' | 'right' | 'bottom';
|
position?: 'left' | 'right' | 'bottom';
|
||||||
tabs?: Array<{ id: string; label: string; content: any }>;
|
tabs?: Array<{ id: string; label: string; content: any }>;
|
||||||
|
onClose?: () => void;
|
||||||
|
onCyclePosition?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@ -16,7 +19,9 @@
|
|||||||
minWidth = 200,
|
minWidth = 200,
|
||||||
maxWidth = 600,
|
maxWidth = 600,
|
||||||
position = $bindable('right'),
|
position = $bindable('right'),
|
||||||
tabs = []
|
tabs = [],
|
||||||
|
onClose,
|
||||||
|
onCyclePosition
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let width = $state(initialWidth);
|
let width = $state(initialWidth);
|
||||||
@ -94,6 +99,14 @@
|
|||||||
{tab.label}
|
{tab.label}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
<div class="panel-controls">
|
||||||
|
<button class="control-button" onclick={onCyclePosition} title="Switch panel side">
|
||||||
|
<ArrowLeftRight size={16} />
|
||||||
|
</button>
|
||||||
|
<button class="control-button" onclick={onClose} title="Close panel">
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="side-panel-content">
|
<div class="side-panel-content">
|
||||||
@ -106,6 +119,16 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
<div class="panel-header-fallback">
|
||||||
|
<div class="panel-controls">
|
||||||
|
<button class="control-button" onclick={onCyclePosition} title="Switch panel side">
|
||||||
|
<ArrowLeftRight size={16} />
|
||||||
|
</button>
|
||||||
|
<button class="control-button" onclick={onClose} title="Close panel">
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="side-panel-content">
|
<div class="side-panel-content">
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
@ -177,10 +200,45 @@
|
|||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
border-bottom: 1px solid #333;
|
border-bottom: 1px solid #333;
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
margin-left: auto;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button {
|
||||||
|
padding: 0.375rem;
|
||||||
|
background-color: transparent;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-button:hover {
|
||||||
|
color: rgba(255, 255, 255, 1);
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header-fallback {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
background: none;
|
background: none;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
type PanelPosition = 'left' | 'right' | 'bottom';
|
type PanelPosition = 'left' | 'right';
|
||||||
|
|
||||||
export class UIState {
|
export class UIState {
|
||||||
sidePanelVisible = $state(true);
|
sidePanelVisible = $state(true);
|
||||||
@ -21,8 +21,6 @@ export class UIState {
|
|||||||
cyclePanelPosition() {
|
cyclePanelPosition() {
|
||||||
if (this.sidePanelPosition === 'right') {
|
if (this.sidePanelPosition === 'right') {
|
||||||
this.sidePanelPosition = 'left';
|
this.sidePanelPosition = 'left';
|
||||||
} else if (this.sidePanelPosition === 'left') {
|
|
||||||
this.sidePanelPosition = 'bottom';
|
|
||||||
} else {
|
} else {
|
||||||
this.sidePanelPosition = 'right';
|
this.sidePanelPosition = 'right';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user