mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 03:39:26 +00:00
Add preview
This commit is contained in:
@@ -744,6 +744,12 @@ body {
|
|||||||
box-shadow: 0 0 0 1px rgba(124, 58, 237, 0.6);
|
box-shadow: 0 0 0 1px rgba(124, 58, 237, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.asset-item.pending {
|
||||||
|
cursor: default;
|
||||||
|
border-style: dashed;
|
||||||
|
opacity: 0.92;
|
||||||
|
}
|
||||||
|
|
||||||
.asset-item.is-hidden {
|
.asset-item.is-hidden {
|
||||||
opacity: 0.72;
|
opacity: 0.72;
|
||||||
border-style: dashed;
|
border-style: dashed;
|
||||||
@@ -834,6 +840,73 @@ body {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.asset-preview.still {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-preview.still:not(.has-image) {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: #cbd5e1;
|
||||||
|
background: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: linear-gradient(180deg, rgba(15, 23, 42, 0.4), rgba(15, 23, 42, 0.4));
|
||||||
|
color: #e5e7eb;
|
||||||
|
pointer-events: none;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending-preview {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: #cbd5e1;
|
||||||
|
background: rgba(124, 58, 237, 0.08);
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-progress {
|
||||||
|
margin-top: 10px;
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
background: rgba(148, 163, 184, 0.12);
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.25);
|
||||||
|
border-radius: 999px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, rgba(124, 58, 237, 0.8), rgba(99, 102, 241, 0.8), rgba(124, 58, 237, 0.8));
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: upload-progress 1.2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-progress-bar.is-processing {
|
||||||
|
background: linear-gradient(90deg, rgba(34, 197, 94, 0.85), rgba(52, 211, 153, 0.8), rgba(34, 197, 94, 0.85));
|
||||||
|
animation-duration: 1.8s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes upload-progress {
|
||||||
|
from {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.audio-icon {
|
.audio-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ let canvasSettings = { width: 1920, height: 1080 };
|
|||||||
canvas.width = canvasSettings.width;
|
canvas.width = canvasSettings.width;
|
||||||
canvas.height = canvasSettings.height;
|
canvas.height = canvasSettings.height;
|
||||||
const assets = new Map();
|
const assets = new Map();
|
||||||
|
let pendingUploads = [];
|
||||||
const mediaCache = new Map();
|
const mediaCache = new Map();
|
||||||
const renderStates = new Map();
|
const renderStates = new Map();
|
||||||
const animatedCache = new Map();
|
const animatedCache = new Map();
|
||||||
const audioControllers = new Map();
|
const audioControllers = new Map();
|
||||||
const pendingAudioUnlock = new Set();
|
const pendingAudioUnlock = new Set();
|
||||||
const loopPlaybackState = new Map();
|
const loopPlaybackState = new Map();
|
||||||
|
const previewCache = new Map();
|
||||||
let drawPending = false;
|
let drawPending = false;
|
||||||
let zOrderDirty = true;
|
let zOrderDirty = true;
|
||||||
let zOrderCache = [];
|
let zOrderCache = [];
|
||||||
@@ -31,6 +33,7 @@ const muteInput = document.getElementById('asset-muted');
|
|||||||
const selectedZLabel = document.getElementById('asset-z-level');
|
const selectedZLabel = document.getElementById('asset-z-level');
|
||||||
const playbackSection = document.getElementById('playback-section');
|
const playbackSection = document.getElementById('playback-section');
|
||||||
const audioSection = document.getElementById('audio-section');
|
const audioSection = document.getElementById('audio-section');
|
||||||
|
const layoutSection = document.getElementById('layout-section');
|
||||||
const audioLoopInput = document.getElementById('asset-audio-loop');
|
const audioLoopInput = document.getElementById('asset-audio-loop');
|
||||||
const audioDelayInput = document.getElementById('asset-audio-delay');
|
const audioDelayInput = document.getElementById('asset-audio-delay');
|
||||||
const audioSpeedInput = document.getElementById('asset-audio-speed');
|
const audioSpeedInput = document.getElementById('asset-audio-speed');
|
||||||
@@ -66,6 +69,40 @@ function debounce(fn, wait = 150) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addPendingUpload(name) {
|
||||||
|
const pending = {
|
||||||
|
id: `pending-${Date.now()}-${Math.round(Math.random() * 100000)}`,
|
||||||
|
name,
|
||||||
|
status: 'uploading',
|
||||||
|
createdAtMs: Date.now()
|
||||||
|
};
|
||||||
|
pendingUploads.push(pending);
|
||||||
|
renderAssetList();
|
||||||
|
return pending.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePendingUpload(id, updates = {}) {
|
||||||
|
const pending = pendingUploads.find((item) => item.id === id);
|
||||||
|
if (!pending) return;
|
||||||
|
Object.assign(pending, updates);
|
||||||
|
renderAssetList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removePendingUpload(id) {
|
||||||
|
const index = pendingUploads.findIndex((item) => item.id === id);
|
||||||
|
if (index === -1) return;
|
||||||
|
pendingUploads.splice(index, 1);
|
||||||
|
renderAssetList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePendingUploadByName(name) {
|
||||||
|
if (!name) return;
|
||||||
|
const index = pendingUploads.findIndex((item) => item.name === name);
|
||||||
|
if (index === -1) return;
|
||||||
|
pendingUploads.splice(index, 1);
|
||||||
|
renderAssetList();
|
||||||
|
}
|
||||||
|
|
||||||
function formatDurationLabel(durationMs) {
|
function formatDurationLabel(durationMs) {
|
||||||
const totalSeconds = Math.max(0, Math.round(durationMs / 1000));
|
const totalSeconds = Math.max(0, Math.round(durationMs / 1000));
|
||||||
const seconds = totalSeconds % 60;
|
const seconds = totalSeconds % 60;
|
||||||
@@ -228,6 +265,11 @@ function renderAssets(list) {
|
|||||||
|
|
||||||
function storeAsset(asset) {
|
function storeAsset(asset) {
|
||||||
if (!asset) return;
|
if (!asset) return;
|
||||||
|
const existing = assets.get(asset.id);
|
||||||
|
if (existing && existing.url !== asset.url) {
|
||||||
|
clearMedia(asset.id);
|
||||||
|
previewCache.delete(asset.id);
|
||||||
|
}
|
||||||
asset.zIndex = Math.max(1, asset.zIndex ?? 1);
|
asset.zIndex = Math.max(1, asset.zIndex ?? 1);
|
||||||
const parsedCreatedAt = asset.createdAt ? new Date(asset.createdAt).getTime() : NaN;
|
const parsedCreatedAt = asset.createdAt ? new Date(asset.createdAt).getTime() : NaN;
|
||||||
const hasCreatedAtMs = typeof asset.createdAtMs === 'number' && Number.isFinite(asset.createdAtMs);
|
const hasCreatedAtMs = typeof asset.createdAtMs === 'number' && Number.isFinite(asset.createdAtMs);
|
||||||
@@ -239,6 +281,7 @@ function storeAsset(asset) {
|
|||||||
if (!renderStates.has(asset.id)) {
|
if (!renderStates.has(asset.id)) {
|
||||||
renderStates.set(asset.id, { ...asset });
|
renderStates.set(asset.id, { ...asset });
|
||||||
}
|
}
|
||||||
|
resolvePendingUploadByName(asset.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRenderState(asset) {
|
function updateRenderState(asset) {
|
||||||
@@ -615,6 +658,7 @@ function isDrawable(element) {
|
|||||||
|
|
||||||
function clearMedia(assetId) {
|
function clearMedia(assetId) {
|
||||||
mediaCache.delete(assetId);
|
mediaCache.delete(assetId);
|
||||||
|
previewCache.delete(assetId);
|
||||||
const animated = animatedCache.get(assetId);
|
const animated = animatedCache.get(assetId);
|
||||||
if (animated) {
|
if (animated) {
|
||||||
animated.cancelled = true;
|
animated.cancelled = true;
|
||||||
@@ -717,6 +761,9 @@ function autoStartAudio(asset) {
|
|||||||
|
|
||||||
function ensureMedia(asset) {
|
function ensureMedia(asset) {
|
||||||
const cached = mediaCache.get(asset.id);
|
const cached = mediaCache.get(asset.id);
|
||||||
|
if (cached && cached.src !== asset.url) {
|
||||||
|
clearMedia(asset.id);
|
||||||
|
}
|
||||||
if (cached && cached.src === asset.url) {
|
if (cached && cached.src === asset.url) {
|
||||||
applyMediaSettings(cached, asset);
|
applyMediaSettings(cached, asset);
|
||||||
return cached;
|
return cached;
|
||||||
@@ -737,6 +784,7 @@ function ensureMedia(asset) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const element = isVideoAsset(asset) ? document.createElement('video') : new Image();
|
const element = isVideoAsset(asset) ? document.createElement('video') : new Image();
|
||||||
|
element.crossOrigin = 'anonymous';
|
||||||
if (isVideoElement(element)) {
|
if (isVideoElement(element)) {
|
||||||
element.loop = true;
|
element.loop = true;
|
||||||
element.muted = asset.muted ?? true;
|
element.muted = asset.muted ?? true;
|
||||||
@@ -867,7 +915,10 @@ function renderAssetList() {
|
|||||||
}
|
}
|
||||||
list.innerHTML = '';
|
list.innerHTML = '';
|
||||||
|
|
||||||
if (!assets.size) {
|
const hasAssets = assets.size > 0;
|
||||||
|
const hasPending = pendingUploads.length > 0;
|
||||||
|
|
||||||
|
if (!hasAssets && !hasPending) {
|
||||||
selectedAssetId = null;
|
selectedAssetId = null;
|
||||||
if (assetInspector) {
|
if (assetInspector) {
|
||||||
assetInspector.classList.add('hidden');
|
assetInspector.classList.add('hidden');
|
||||||
@@ -880,9 +931,14 @@ function renderAssetList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (assetInspector) {
|
if (assetInspector) {
|
||||||
assetInspector.classList.remove('hidden');
|
assetInspector.classList.toggle('hidden', !hasAssets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pendingItems = [...pendingUploads].sort((a, b) => (a.createdAtMs || 0) - (b.createdAtMs || 0));
|
||||||
|
pendingItems.forEach((pending) => {
|
||||||
|
list.appendChild(createPendingListItem(pending));
|
||||||
|
});
|
||||||
|
|
||||||
const sortedAssets = getChronologicalAssets();
|
const sortedAssets = getChronologicalAssets();
|
||||||
sortedAssets.forEach((asset) => {
|
sortedAssets.forEach((asset) => {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
@@ -990,6 +1046,43 @@ function renderAssetList() {
|
|||||||
updateSelectedAssetControls();
|
updateSelectedAssetControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createPendingListItem(pending) {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = 'asset-item pending';
|
||||||
|
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'asset-row';
|
||||||
|
|
||||||
|
const preview = document.createElement('div');
|
||||||
|
preview.className = 'asset-preview pending-preview';
|
||||||
|
preview.innerHTML = '<i class="fa-solid fa-cloud-arrow-up" aria-hidden="true"></i>';
|
||||||
|
|
||||||
|
const meta = document.createElement('div');
|
||||||
|
meta.className = 'meta';
|
||||||
|
const name = document.createElement('strong');
|
||||||
|
name.textContent = pending?.name || 'Uploading asset';
|
||||||
|
const details = document.createElement('small');
|
||||||
|
details.textContent = pending.status === 'processing' ? 'Processing upload…' : 'Uploading…';
|
||||||
|
meta.appendChild(name);
|
||||||
|
meta.appendChild(details);
|
||||||
|
|
||||||
|
const progress = document.createElement('div');
|
||||||
|
progress.className = 'upload-progress';
|
||||||
|
const bar = document.createElement('div');
|
||||||
|
bar.className = 'upload-progress-bar';
|
||||||
|
if (pending.status === 'processing') {
|
||||||
|
bar.classList.add('is-processing');
|
||||||
|
}
|
||||||
|
progress.appendChild(bar);
|
||||||
|
meta.appendChild(progress);
|
||||||
|
|
||||||
|
row.appendChild(preview);
|
||||||
|
row.appendChild(meta);
|
||||||
|
li.appendChild(row);
|
||||||
|
|
||||||
|
return li;
|
||||||
|
}
|
||||||
|
|
||||||
function createBadge(label, extraClass = '') {
|
function createBadge(label, extraClass = '') {
|
||||||
const badge = document.createElement('span');
|
const badge = document.createElement('span');
|
||||||
badge.className = `badge ${extraClass}`.trim();
|
badge.className = `badge ${extraClass}`.trim();
|
||||||
@@ -1021,25 +1114,125 @@ function createPreviewElement(asset) {
|
|||||||
icon.innerHTML = '<i class="fa-solid fa-music" aria-hidden="true"></i>';
|
icon.innerHTML = '<i class="fa-solid fa-music" aria-hidden="true"></i>';
|
||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
if (isVideoAsset(asset)) {
|
if (isVideoAsset(asset) || isGifAsset(asset)) {
|
||||||
const video = document.createElement('video');
|
const still = document.createElement('div');
|
||||||
video.className = 'asset-preview';
|
still.className = 'asset-preview still';
|
||||||
video.src = asset.url;
|
still.setAttribute('aria-label', asset.name || 'Asset preview');
|
||||||
video.loop = true;
|
|
||||||
video.muted = true;
|
const overlay = document.createElement('div');
|
||||||
video.playsInline = true;
|
overlay.className = 'preview-overlay';
|
||||||
video.autoplay = true;
|
overlay.innerHTML = '<i class="fa-solid fa-play"></i>';
|
||||||
video.play().catch(() => { });
|
still.appendChild(overlay);
|
||||||
return video;
|
|
||||||
|
loadPreviewFrame(asset, still);
|
||||||
|
return still;
|
||||||
}
|
}
|
||||||
|
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.className = 'asset-preview';
|
img.className = 'asset-preview';
|
||||||
img.src = asset.url;
|
img.src = asset.url;
|
||||||
img.alt = asset.name || 'Asset preview';
|
img.alt = asset.name || 'Asset preview';
|
||||||
|
img.loading = 'lazy';
|
||||||
return img;
|
return img;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadPreviewFrame(asset, element) {
|
||||||
|
if (!asset || !element) return;
|
||||||
|
const cached = previewCache.get(asset.id);
|
||||||
|
if (cached) {
|
||||||
|
applyPreviewFrame(element, cached);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = isVideoAsset(asset)
|
||||||
|
? captureVideoFrame(asset)
|
||||||
|
: isGifAsset(asset)
|
||||||
|
? captureGifFrame(asset)
|
||||||
|
: Promise.resolve(null);
|
||||||
|
|
||||||
|
source
|
||||||
|
.then((dataUrl) => {
|
||||||
|
if (!dataUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
previewCache.set(asset.id, dataUrl);
|
||||||
|
applyPreviewFrame(element, dataUrl);
|
||||||
|
})
|
||||||
|
.catch(() => { });
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPreviewFrame(element, dataUrl) {
|
||||||
|
if (!element || !dataUrl) return;
|
||||||
|
element.style.backgroundImage = `url(${dataUrl})`;
|
||||||
|
element.classList.add('has-image');
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureVideoFrame(asset) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const video = document.createElement('video');
|
||||||
|
video.crossOrigin = 'anonymous';
|
||||||
|
video.preload = 'auto';
|
||||||
|
video.muted = true;
|
||||||
|
video.playsInline = true;
|
||||||
|
video.src = asset.url;
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
video.pause();
|
||||||
|
video.removeAttribute('src');
|
||||||
|
video.load();
|
||||||
|
};
|
||||||
|
|
||||||
|
video.addEventListener('loadeddata', () => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = video.videoWidth || asset.width || 0;
|
||||||
|
canvas.height = video.videoHeight || asset.height || 0;
|
||||||
|
if (!canvas.width || !canvas.height) {
|
||||||
|
cleanup();
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||||
|
try {
|
||||||
|
const dataUrl = canvas.toDataURL('image/png');
|
||||||
|
resolve(dataUrl);
|
||||||
|
} catch (err) {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
cleanup();
|
||||||
|
}, { once: true });
|
||||||
|
|
||||||
|
video.addEventListener('error', () => {
|
||||||
|
cleanup();
|
||||||
|
resolve(null);
|
||||||
|
}, { once: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function captureGifFrame(asset) {
|
||||||
|
if (!('ImageDecoder' in window)) {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
return fetch(asset.url)
|
||||||
|
.then((r) => r.blob())
|
||||||
|
.then((blob) => new ImageDecoder({ data: blob, type: blob.type || 'image/gif' }))
|
||||||
|
.then((decoder) => decoder.decode({ frameIndex: 0 }))
|
||||||
|
.then(({ image }) => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = image.displayWidth || asset.width || 0;
|
||||||
|
canvas.height = image.displayHeight || asset.height || 0;
|
||||||
|
const ctx2d = canvas.getContext('2d');
|
||||||
|
ctx2d.drawImage(image, 0, 0, canvas.width, canvas.height);
|
||||||
|
image.close?.();
|
||||||
|
try {
|
||||||
|
return canvas.toDataURL('image/png');
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => null);
|
||||||
|
}
|
||||||
|
|
||||||
function getSelectedAsset() {
|
function getSelectedAsset() {
|
||||||
return selectedAssetId ? assets.get(selectedAssetId) : null;
|
return selectedAssetId ? assets.get(selectedAssetId) : null;
|
||||||
}
|
}
|
||||||
@@ -1068,6 +1261,15 @@ function updateSelectedAssetControls(asset = getSelectedAsset()) {
|
|||||||
aspectLockInput.checked = isAspectLocked(asset.id);
|
aspectLockInput.checked = isAspectLocked(asset.id);
|
||||||
aspectLockInput.onchange = () => setAspectLock(asset.id, aspectLockInput.checked);
|
aspectLockInput.onchange = () => setAspectLock(asset.id, aspectLockInput.checked);
|
||||||
}
|
}
|
||||||
|
if (layoutSection) {
|
||||||
|
const hideLayout = isAudioAsset(asset);
|
||||||
|
layoutSection.classList.toggle('hidden', hideLayout);
|
||||||
|
const layoutControls = layoutSection.querySelectorAll('input, button');
|
||||||
|
layoutControls.forEach((control) => {
|
||||||
|
control.disabled = hideLayout;
|
||||||
|
control.classList.toggle('disabled', hideLayout);
|
||||||
|
});
|
||||||
|
}
|
||||||
if (speedInput) {
|
if (speedInput) {
|
||||||
const percent = Math.round((asset.speed ?? 1) * 100);
|
const percent = Math.round((asset.speed ?? 1) * 100);
|
||||||
speedInput.value = Math.min(1000, Math.max(0, percent));
|
speedInput.value = Math.min(1000, Math.max(0, percent));
|
||||||
@@ -1435,6 +1637,8 @@ function uploadAsset(file = null) {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pendingId = addPendingUpload(selectedFile.name);
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
data.append('file', selectedFile);
|
data.append('file', selectedFile);
|
||||||
if (fileNameLabel) {
|
if (fileNameLabel) {
|
||||||
@@ -1452,12 +1656,14 @@ function uploadAsset(file = null) {
|
|||||||
handleFileSelection(fileInput);
|
handleFileSelection(fileInput);
|
||||||
}
|
}
|
||||||
if (typeof showToast === 'function') {
|
if (typeof showToast === 'function') {
|
||||||
showToast('Asset uploaded successfully.', 'success');
|
showToast('Upload received. Processing asset...', 'success');
|
||||||
}
|
}
|
||||||
|
updatePendingUpload(pendingId, { status: 'processing' });
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
if (fileNameLabel) {
|
if (fileNameLabel) {
|
||||||
fileNameLabel.textContent = 'Upload failed';
|
fileNameLabel.textContent = 'Upload failed';
|
||||||
}
|
}
|
||||||
|
removePendingUpload(pendingId);
|
||||||
if (typeof showToast === 'function') {
|
if (typeof showToast === 'function') {
|
||||||
showToast('Upload failed. Please try again with a supported file.', 'error');
|
showToast('Upload failed. Please try again with a supported file.', 'error');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -437,6 +437,9 @@ function autoStartAudio(asset) {
|
|||||||
|
|
||||||
function ensureMedia(asset) {
|
function ensureMedia(asset) {
|
||||||
const cached = mediaCache.get(asset.id);
|
const cached = mediaCache.get(asset.id);
|
||||||
|
if (cached && cached.src !== asset.url) {
|
||||||
|
clearMedia(asset.id);
|
||||||
|
}
|
||||||
if (cached && cached.src === asset.url) {
|
if (cached && cached.src === asset.url) {
|
||||||
applyMediaSettings(cached, asset);
|
applyMediaSettings(cached, asset);
|
||||||
return cached;
|
return cached;
|
||||||
@@ -457,6 +460,7 @@ function ensureMedia(asset) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const element = isVideoAsset(asset) ? document.createElement('video') : new Image();
|
const element = isVideoAsset(asset) ? document.createElement('video') : new Image();
|
||||||
|
element.crossOrigin = 'anonymous';
|
||||||
if (isVideoElement(element)) {
|
if (isVideoElement(element)) {
|
||||||
element.loop = true;
|
element.loop = true;
|
||||||
element.muted = asset.muted ?? true;
|
element.muted = asset.muted ?? true;
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="asset-controls-placeholder" class="asset-controls-placeholder">
|
<div id="asset-controls-placeholder" class="asset-controls-placeholder">
|
||||||
<div id="asset-controls" class="hidden asset-settings">
|
<div id="asset-controls" class="hidden asset-settings">
|
||||||
<div class="panel-section">
|
<div class="panel-section" id="layout-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h5>Layout & order</h5>
|
<h5>Layout & order</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user