Files
buboard/src/lib/Palette.svelte

247 lines
5.6 KiB
Svelte

<script lang="ts">
import { Square, Type, Image, Music, Video, Globe } from 'lucide-svelte';
import { state as appState } from './state.svelte';
let locked = $derived(appState.locked);
let imageInput: HTMLInputElement | null = $state(null);
let soundInput: HTMLInputElement | null = $state(null);
let videoInput: HTMLInputElement | null = $state(null);
function getSpawnPosition() {
return {
x: appState.snap(-appState.viewport.x / appState.viewport.zoom + 400),
y: appState.snap(-appState.viewport.y / appState.viewport.zoom + 300)
};
}
function addTile() {
const id = crypto.randomUUID();
const pos = getSpawnPosition();
appState.addItem({
id,
html: '',
css: '',
x: pos.x,
y: pos.y,
width: 200,
height: 200,
rotation: 0,
zIndex: appState.maxZIndex + 1
});
appState.select(id);
}
function addText() {
const id = crypto.randomUUID();
const pos = getSpawnPosition();
appState.addItem({
id,
html: '<p>Text</p>',
css: `p {
color: #fff;
font-size: 24px;
font-family: 'Departure Mono', monospace;
}`,
x: pos.x,
y: pos.y,
width: 200,
height: 50,
rotation: 0,
zIndex: appState.maxZIndex + 1
});
appState.select(id);
}
function addEmbed() {
const url = prompt('Enter URL to embed:');
if (!url) return;
try {
const parsed = new URL(url);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
alert('Only HTTP/HTTPS URLs are allowed');
return;
}
} catch {
alert('Invalid URL');
return;
}
const id = crypto.randomUUID();
const pos = getSpawnPosition();
const escapedUrl = url.replace(/"/g, '&quot;');
appState.addItem({
id,
html: `<iframe src="${escapedUrl}" frameborder="0" allowfullscreen></iframe>`,
css: `iframe {
width: 100%;
height: 100%;
border: none;
}`,
x: pos.x,
y: pos.y,
width: 640,
height: 480,
rotation: 0,
zIndex: appState.maxZIndex + 1
});
appState.select(id);
}
function handleImageChange(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
addImageItem(file);
(e.target as HTMLInputElement).value = '';
}
function handleSoundChange(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
addSoundItem(file);
(e.target as HTMLInputElement).value = '';
}
function handleVideoChange(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
addVideoItem(file);
(e.target as HTMLInputElement).value = '';
}
function addImageItem(file: File) {
const id = crypto.randomUUID();
const assetId = crypto.randomUUID();
const url = URL.createObjectURL(file);
const pos = getSpawnPosition();
const img = document.createElement('img');
img.onload = () => {
appState.addAsset(assetId, { blob: file, url, filename: file.name });
appState.addItem({
id,
assetId,
html: `<img src="${url}" alt="" />`,
css: `img {
width: 100%;
height: 100%;
object-fit: contain;
}`,
x: pos.x,
y: pos.y,
width: img.naturalWidth,
height: img.naturalHeight,
rotation: 0,
zIndex: appState.maxZIndex + 1
});
appState.select(id);
};
img.src = url;
}
function addSoundItem(file: File) {
const id = crypto.randomUUID();
const assetId = crypto.randomUUID();
const url = URL.createObjectURL(file);
const pos = getSpawnPosition();
appState.addAsset(assetId, { blob: file, url, filename: file.name });
appState.addItem({
id,
assetId,
html: `<audio src="${url}" controls></audio>`,
css: `audio {
width: 100%;
}`,
x: pos.x,
y: pos.y,
width: 300,
height: 54,
rotation: 0,
zIndex: appState.maxZIndex + 1
});
appState.select(id);
}
function addVideoItem(file: File) {
const id = crypto.randomUUID();
const assetId = crypto.randomUUID();
const url = URL.createObjectURL(file);
const pos = getSpawnPosition();
const video = document.createElement('video');
video.onloadedmetadata = () => {
appState.addAsset(assetId, { blob: file, url, filename: file.name });
appState.addItem({
id,
assetId,
html: `<video src="${url}" controls></video>`,
css: `video {
width: 100%;
height: 100%;
}`,
x: pos.x,
y: pos.y,
width: video.videoWidth || 640,
height: video.videoHeight || 360,
rotation: 0,
zIndex: appState.maxZIndex + 1
});
appState.select(id);
};
video.src = url;
}
</script>
{#if !locked}
<div class="palette">
<button onclick={addTile} title="Tile"><Square size={14} /></button>
<button onclick={addText} title="Text"><Type size={14} /></button>
<button onclick={() => imageInput?.click()} title="Image"><Image size={14} /></button>
<button onclick={() => soundInput?.click()} title="Sound"><Music size={14} /></button>
<button onclick={() => videoInput?.click()} title="Video"><Video size={14} /></button>
<button onclick={addEmbed} title="Embed"><Globe size={14} /></button>
<input
bind:this={imageInput}
type="file"
accept="image/*"
onchange={handleImageChange}
style="display:none"
/>
<input
bind:this={soundInput}
type="file"
accept="audio/*"
onchange={handleSoundChange}
style="display:none"
/>
<input
bind:this={videoInput}
type="file"
accept="video/*"
onchange={handleVideoChange}
style="display:none"
/>
</div>
{/if}
<style>
.palette {
display: flex;
gap: 4px;
}
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);
}
</style>