mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 03:39:26 +00:00
Add asset listing and delete
This commit is contained in:
@@ -66,9 +66,20 @@ body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
|
filter: brightness(0.35) saturate(0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay canvas, .broadcast-body canvas {
|
.overlay canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: auto;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.broadcast-body canvas {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -100,3 +111,45 @@ body {
|
|||||||
.panel li {
|
.panel li {
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.asset-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #111827;
|
||||||
|
border: 1px solid #1f2937;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-item.selected {
|
||||||
|
border-color: #7c3aed;
|
||||||
|
box-shadow: 0 0 0 1px rgba(124, 58, 237, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-item .meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-item small {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-item .actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-item.hidden {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ const ctx = canvas.getContext('2d');
|
|||||||
canvas.width = canvas.offsetWidth;
|
canvas.width = canvas.offsetWidth;
|
||||||
canvas.height = canvas.offsetHeight;
|
canvas.height = canvas.offsetHeight;
|
||||||
const assets = new Map();
|
const assets = new Map();
|
||||||
|
const imageCache = new Map();
|
||||||
|
let selectedAssetId = null;
|
||||||
|
let dragState = null;
|
||||||
|
|
||||||
function connect() {
|
function connect() {
|
||||||
const socket = new SockJS('/ws');
|
const socket = new SockJS('/ws');
|
||||||
@@ -18,33 +21,167 @@ function connect() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function fetchAssets() {
|
function fetchAssets() {
|
||||||
fetch(`/api/channels/${broadcaster}/assets`).then(r => r.json()).then(renderAssets);
|
fetch(`/api/channels/${broadcaster}/assets`).then((r) => r.json()).then(renderAssets);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAssets(list) {
|
function renderAssets(list) {
|
||||||
list.forEach(asset => assets.set(asset.id, asset));
|
list.forEach((asset) => assets.set(asset.id, asset));
|
||||||
draw();
|
drawAndList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEvent(event) {
|
function handleEvent(event) {
|
||||||
if (event.type === 'DELETED') {
|
if (event.type === 'DELETED') {
|
||||||
assets.delete(event.assetId);
|
assets.delete(event.assetId);
|
||||||
|
imageCache.delete(event.assetId);
|
||||||
|
if (selectedAssetId === event.assetId) {
|
||||||
|
selectedAssetId = null;
|
||||||
|
}
|
||||||
} else if (event.payload) {
|
} else if (event.payload) {
|
||||||
assets.set(event.payload.id, event.payload);
|
assets.set(event.payload.id, event.payload);
|
||||||
|
ensureImage(event.payload);
|
||||||
}
|
}
|
||||||
|
drawAndList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawAndList() {
|
||||||
draw();
|
draw();
|
||||||
|
renderAssetList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function draw() {
|
function draw() {
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
assets.forEach(asset => {
|
assets.forEach((asset) => drawAsset(asset));
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawAsset(asset) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.globalAlpha = asset.hidden ? 0.35 : 1;
|
|
||||||
ctx.translate(asset.x, asset.y);
|
ctx.translate(asset.x, asset.y);
|
||||||
ctx.rotate(asset.rotation * Math.PI / 180);
|
ctx.rotate(asset.rotation * Math.PI / 180);
|
||||||
ctx.fillStyle = 'rgba(124, 58, 237, 0.25)';
|
|
||||||
|
const image = ensureImage(asset);
|
||||||
|
if (image?.complete) {
|
||||||
|
ctx.globalAlpha = asset.hidden ? 0.35 : 0.9;
|
||||||
|
ctx.drawImage(image, 0, 0, asset.width, asset.height);
|
||||||
|
} else {
|
||||||
|
ctx.globalAlpha = asset.hidden ? 0.2 : 0.4;
|
||||||
|
ctx.fillStyle = 'rgba(124, 58, 237, 0.35)';
|
||||||
ctx.fillRect(0, 0, asset.width, asset.height);
|
ctx.fillRect(0, 0, asset.width, asset.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asset.hidden) {
|
||||||
|
ctx.fillStyle = 'rgba(15, 23, 42, 0.35)';
|
||||||
|
ctx.fillRect(0, 0, asset.width, asset.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
ctx.strokeStyle = asset.id === selectedAssetId ? 'rgba(124, 58, 237, 0.9)' : 'rgba(255, 255, 255, 0.4)';
|
||||||
|
ctx.lineWidth = asset.id === selectedAssetId ? 2 : 1;
|
||||||
|
ctx.setLineDash(asset.id === selectedAssetId ? [6, 4] : []);
|
||||||
|
ctx.strokeRect(0, 0, asset.width, asset.height);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureImage(asset) {
|
||||||
|
const cached = imageCache.get(asset.id);
|
||||||
|
if (cached && cached.src === asset.url) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = new Image();
|
||||||
|
image.onload = draw;
|
||||||
|
image.src = asset.url;
|
||||||
|
imageCache.set(asset.id, image);
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAssetList() {
|
||||||
|
const list = document.getElementById('asset-list');
|
||||||
|
list.innerHTML = '';
|
||||||
|
|
||||||
|
if (!assets.size) {
|
||||||
|
const empty = document.createElement('li');
|
||||||
|
empty.textContent = 'No assets yet. Upload to get started.';
|
||||||
|
list.appendChild(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedAssets = Array.from(assets.values()).sort(
|
||||||
|
(a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0)
|
||||||
|
);
|
||||||
|
sortedAssets.forEach((asset) => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = 'asset-item';
|
||||||
|
if (asset.id === selectedAssetId) {
|
||||||
|
li.classList.add('selected');
|
||||||
|
}
|
||||||
|
if (asset.hidden) {
|
||||||
|
li.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = document.createElement('div');
|
||||||
|
meta.className = 'meta';
|
||||||
|
const name = document.createElement('strong');
|
||||||
|
name.textContent = `Asset ${asset.id.slice(0, 6)}`;
|
||||||
|
const details = document.createElement('small');
|
||||||
|
details.textContent = `${Math.round(asset.width)}x${Math.round(asset.height)} · ${asset.hidden ? 'Hidden' : 'Visible'}`;
|
||||||
|
meta.appendChild(name);
|
||||||
|
meta.appendChild(details);
|
||||||
|
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'actions';
|
||||||
|
|
||||||
|
const toggleBtn = document.createElement('button');
|
||||||
|
toggleBtn.type = 'button';
|
||||||
|
toggleBtn.className = 'secondary';
|
||||||
|
toggleBtn.textContent = asset.hidden ? 'Show' : 'Hide';
|
||||||
|
toggleBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
selectedAssetId = asset.id;
|
||||||
|
updateVisibility(asset, !asset.hidden);
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteBtn = document.createElement('button');
|
||||||
|
deleteBtn.type = 'button';
|
||||||
|
deleteBtn.className = 'secondary';
|
||||||
|
deleteBtn.textContent = 'Delete';
|
||||||
|
deleteBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
deleteAsset(asset);
|
||||||
|
});
|
||||||
|
|
||||||
|
actions.appendChild(toggleBtn);
|
||||||
|
actions.appendChild(deleteBtn);
|
||||||
|
|
||||||
|
li.addEventListener('click', () => {
|
||||||
|
selectedAssetId = asset.id;
|
||||||
|
drawAndList();
|
||||||
|
});
|
||||||
|
|
||||||
|
li.appendChild(meta);
|
||||||
|
li.appendChild(actions);
|
||||||
|
list.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateVisibility(asset, hidden) {
|
||||||
|
fetch(`/api/channels/${broadcaster}/assets/${asset.id}/visibility`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ hidden })
|
||||||
|
}).then((r) => r.json()).then((updated) => {
|
||||||
|
assets.set(updated.id, updated);
|
||||||
|
drawAndList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteAsset(asset) {
|
||||||
|
fetch(`/api/channels/${broadcaster}/assets/${asset.id}`, { method: 'DELETE' }).then(() => {
|
||||||
|
assets.delete(asset.id);
|
||||||
|
imageCache.delete(asset.id);
|
||||||
|
if (selectedAssetId === asset.id) {
|
||||||
|
selectedAssetId = null;
|
||||||
|
}
|
||||||
|
drawAndList();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +201,95 @@ function uploadAsset() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCanvasPoint(event) {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const scaleX = canvas.width / rect.width;
|
||||||
|
const scaleY = canvas.height / rect.height;
|
||||||
|
return {
|
||||||
|
x: (event.clientX - rect.left) * scaleX,
|
||||||
|
y: (event.clientY - rect.top) * scaleY
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPointOnAsset(asset, x, y) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(asset.x, asset.y);
|
||||||
|
ctx.rotate(asset.rotation * Math.PI / 180);
|
||||||
|
const path = new Path2D();
|
||||||
|
path.rect(0, 0, asset.width, asset.height);
|
||||||
|
const hit = ctx.isPointInPath(path, x, y);
|
||||||
|
ctx.restore();
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findAssetAtPoint(x, y) {
|
||||||
|
const ordered = Array.from(assets.values()).reverse();
|
||||||
|
return ordered.find((asset) => isPointOnAsset(asset, x, y)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistTransform(asset) {
|
||||||
|
fetch(`/api/channels/${broadcaster}/assets/${asset.id}/transform`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
x: asset.x,
|
||||||
|
y: asset.y,
|
||||||
|
width: asset.width,
|
||||||
|
height: asset.height,
|
||||||
|
rotation: asset.rotation
|
||||||
|
})
|
||||||
|
}).then((r) => r.json()).then((updated) => {
|
||||||
|
assets.set(updated.id, updated);
|
||||||
|
drawAndList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.addEventListener('mousedown', (event) => {
|
||||||
|
const point = getCanvasPoint(event);
|
||||||
|
const hit = findAssetAtPoint(point.x, point.y);
|
||||||
|
if (hit) {
|
||||||
|
selectedAssetId = hit.id;
|
||||||
|
dragState = {
|
||||||
|
assetId: hit.id,
|
||||||
|
offsetX: point.x - hit.x,
|
||||||
|
offsetY: point.y - hit.y
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
selectedAssetId = null;
|
||||||
|
}
|
||||||
|
drawAndList();
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.addEventListener('mousemove', (event) => {
|
||||||
|
if (!dragState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const asset = assets.get(dragState.assetId);
|
||||||
|
if (!asset) {
|
||||||
|
dragState = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const point = getCanvasPoint(event);
|
||||||
|
asset.x = point.x - dragState.offsetX;
|
||||||
|
asset.y = point.y - dragState.offsetY;
|
||||||
|
draw();
|
||||||
|
});
|
||||||
|
|
||||||
|
function endDrag() {
|
||||||
|
if (!dragState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const asset = assets.get(dragState.assetId);
|
||||||
|
dragState = null;
|
||||||
|
drawAndList();
|
||||||
|
if (asset) {
|
||||||
|
persistTransform(asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.addEventListener('mouseup', endDrag);
|
||||||
|
canvas.addEventListener('mouseleave', endDrag);
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
canvas.width = canvas.offsetWidth;
|
canvas.width = canvas.offsetWidth;
|
||||||
canvas.height = canvas.offsetHeight;
|
canvas.height = canvas.offsetHeight;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<p>Upload images to place on the broadcaster's overlay. Changes are visible to the broadcaster instantly.</p>
|
<p>Upload images to place on the broadcaster's overlay. Changes are visible to the broadcaster instantly.</p>
|
||||||
<input id="asset-file" type="file" accept="image/*" />
|
<input id="asset-file" type="file" accept="image/*" />
|
||||||
<button onclick="uploadAsset()">Upload</button>
|
<button onclick="uploadAsset()">Upload</button>
|
||||||
<ul id="asset-list"></ul>
|
<ul id="asset-list" class="asset-list"></ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="overlay">
|
<section class="overlay">
|
||||||
|
|||||||
Reference in New Issue
Block a user