247 lines
5.6 KiB
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, '"');
|
|
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>
|