278 lines
6.0 KiB
Svelte
278 lines
6.0 KiB
Svelte
<script lang="ts">
|
|
import { Upload, Download, Paintbrush, Trash2, EyeOff, ZoomIn, Combine, Lock, Unlock } from 'lucide-svelte';
|
|
import { exportBoard, importBoard, mergeBoard } from './io';
|
|
import { state } from './state.svelte';
|
|
import Palette from './Palette.svelte';
|
|
|
|
let { onHide }: { onHide?: () => void } = $props();
|
|
|
|
let fileInput: HTMLInputElement;
|
|
let mergeFileInput: HTMLInputElement;
|
|
|
|
async function handleExport() {
|
|
const result = await exportBoard();
|
|
if (!result.success) {
|
|
alert(result.error || 'Export failed');
|
|
}
|
|
}
|
|
|
|
function handleImportClick() {
|
|
fileInput.click();
|
|
}
|
|
|
|
async function handleFileChange(e: Event) {
|
|
const input = e.target as HTMLInputElement;
|
|
const file = input.files?.[0];
|
|
if (!file) return;
|
|
const result = await importBoard(file);
|
|
if (!result.success) {
|
|
alert(result.error || 'Import failed');
|
|
}
|
|
input.value = '';
|
|
}
|
|
|
|
function handleMergeClick() {
|
|
mergeFileInput.click();
|
|
}
|
|
|
|
async function handleMergeChange(e: Event) {
|
|
const input = e.target as HTMLInputElement;
|
|
const files = input.files;
|
|
if (!files || files.length === 0) return;
|
|
for (const file of files) {
|
|
const result = await mergeBoard(file);
|
|
if (!result.success) {
|
|
alert(result.error || `Merge failed for ${file.name}`);
|
|
}
|
|
}
|
|
input.value = '';
|
|
}
|
|
|
|
function handleClear() {
|
|
if (confirm('Clear the canvas? This cannot be undone.')) {
|
|
state.reset();
|
|
}
|
|
}
|
|
|
|
function handleZoom(e: Event) {
|
|
const value = parseFloat((e.target as HTMLInputElement).value);
|
|
state.setZoom(value);
|
|
}
|
|
|
|
function handleFlagClick(key: string) {
|
|
if (state.hasFlag(key)) {
|
|
state.gotoFlag(key);
|
|
} else {
|
|
state.setFlag(key);
|
|
}
|
|
}
|
|
|
|
function handleFlagRightClick(e: MouseEvent, key: string) {
|
|
e.preventDefault();
|
|
state.clearFlag(key);
|
|
}
|
|
|
|
let zoomPercent = $derived(Math.round(state.viewport.zoom * 100));
|
|
let coordX = $derived(Math.round(-state.viewport.x / state.viewport.zoom));
|
|
let coordY = $derived(Math.round(-state.viewport.y / state.viewport.zoom));
|
|
const flagKeys = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
|
</script>
|
|
|
|
<div class="toolbar">
|
|
<span class="app-name">Buboard</span>
|
|
<Palette />
|
|
<div class="flags">
|
|
{#each flagKeys as key (key)}
|
|
<button
|
|
class="flag"
|
|
class:filled={state.hasFlag(key)}
|
|
onclick={() => handleFlagClick(key)}
|
|
oncontextmenu={(e) => handleFlagRightClick(e, key)}
|
|
title="Position {key}"
|
|
>
|
|
{key}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
<div class="spacer"></div>
|
|
<span class="coords">{coordX}, {coordY}</span>
|
|
<div class="zoom">
|
|
<input
|
|
type="range"
|
|
min="0.1"
|
|
max="5"
|
|
step="0.1"
|
|
value={state.viewport.zoom}
|
|
oninput={handleZoom}
|
|
title="Zoom"
|
|
/>
|
|
<button class="zoom-reset" onclick={() => state.resetViewport()} title="Reset view (0, 0 at 100%)">
|
|
<ZoomIn size={12} />
|
|
<span>{zoomPercent}%</span>
|
|
</button>
|
|
</div>
|
|
<button onclick={() => state.editGlobal(true)} title="Style" disabled={state.locked}><Paintbrush size={14} /></button>
|
|
<button onclick={handleClear} title="Clear" disabled={state.locked}><Trash2 size={14} /></button>
|
|
<button onclick={handleImportClick} title="Import" disabled={state.locked}><Upload size={14} /></button>
|
|
<button onclick={handleMergeClick} title="Merge" disabled={state.locked}><Combine size={14} /></button>
|
|
<button onclick={handleExport} title="Export"><Download size={14} /></button>
|
|
<button
|
|
class:active={state.locked}
|
|
onclick={() => state.setLocked(!state.locked)}
|
|
title={state.locked ? 'Unlock' : 'Lock'}
|
|
>
|
|
{#if state.locked}
|
|
<Unlock size={14} />
|
|
{:else}
|
|
<Lock size={14} />
|
|
{/if}
|
|
</button>
|
|
{#if onHide}
|
|
<button onclick={onHide} title="Hide interface"><EyeOff size={14} /></button>
|
|
{/if}
|
|
<input
|
|
bind:this={fileInput}
|
|
type="file"
|
|
accept=".bub"
|
|
onchange={handleFileChange}
|
|
style="display: none"
|
|
/>
|
|
<input
|
|
bind:this={mergeFileInput}
|
|
type="file"
|
|
accept=".bub"
|
|
multiple
|
|
onchange={handleMergeChange}
|
|
style="display: none"
|
|
/>
|
|
</div>
|
|
|
|
<style>
|
|
.toolbar {
|
|
padding: 4px 8px;
|
|
display: flex;
|
|
gap: 4px;
|
|
background: var(--surface, #282c34);
|
|
border-bottom: 1px solid var(--border, #333);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.app-name {
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
color: var(--text-dim, #666);
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0 8px 0 4px;
|
|
}
|
|
|
|
.spacer {
|
|
flex: 1;
|
|
}
|
|
|
|
.flags {
|
|
display: flex;
|
|
gap: 2px;
|
|
}
|
|
|
|
.flag {
|
|
width: 24px;
|
|
height: 24px;
|
|
padding: 0;
|
|
font-size: 11px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.flag.filled {
|
|
background: var(--accent, #4a9eff);
|
|
color: var(--text, #fff);
|
|
}
|
|
|
|
button {
|
|
padding: 4px 8px;
|
|
background: var(--surface, #282c34);
|
|
color: var(--text-dim, #666);
|
|
border: 1px solid var(--border, #333);
|
|
cursor: pointer;
|
|
}
|
|
|
|
button:hover {
|
|
background: var(--accent, #4a9eff);
|
|
color: var(--text, #fff);
|
|
}
|
|
|
|
button.active {
|
|
background: var(--accent, #4a9eff);
|
|
color: var(--text, #fff);
|
|
}
|
|
|
|
button:disabled {
|
|
opacity: 0.4;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
button:disabled:hover {
|
|
background: var(--surface, #282c34);
|
|
color: var(--text-dim, #666);
|
|
}
|
|
|
|
.zoom {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.zoom input[type='range'] {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 80px;
|
|
height: 4px;
|
|
background: var(--border, #333);
|
|
border: none;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.zoom input[type='range']::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 12px;
|
|
height: 12px;
|
|
background: var(--text-dim, #666);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.zoom input[type='range']::-moz-range-thumb {
|
|
width: 12px;
|
|
height: 12px;
|
|
background: var(--text-dim, #666);
|
|
border: none;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.zoom input[type='range']:hover::-webkit-slider-thumb {
|
|
background: var(--accent, #4a9eff);
|
|
}
|
|
|
|
.zoom input[type='range']:hover::-moz-range-thumb {
|
|
background: var(--accent, #4a9eff);
|
|
}
|
|
|
|
.coords {
|
|
font-size: 11px;
|
|
color: var(--text-dim, #666);
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0 8px;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.zoom-reset {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 2px 6px;
|
|
font-size: 11px;
|
|
min-width: 52px;
|
|
}
|
|
</style>
|