mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 03:39:26 +00:00
Ad js upload
This commit is contained in:
@@ -34,7 +34,9 @@ public class AssetStorageService {
|
|||||||
Map.entry("audio/wav", ".wav"),
|
Map.entry("audio/wav", ".wav"),
|
||||||
Map.entry("audio/ogg", ".ogg"),
|
Map.entry("audio/ogg", ".ogg"),
|
||||||
Map.entry("audio/webm", ".webm"),
|
Map.entry("audio/webm", ".webm"),
|
||||||
Map.entry("audio/flac", ".flac")
|
Map.entry("audio/flac", ".flac"),
|
||||||
|
Map.entry("application/javascript", ".js"),
|
||||||
|
Map.entry("text/javascript", ".js")
|
||||||
);
|
);
|
||||||
|
|
||||||
private final Path assetRoot;
|
private final Path assetRoot;
|
||||||
|
|||||||
@@ -155,8 +155,9 @@ public class ChannelDirectoryService {
|
|||||||
.orElse("asset_" + System.currentTimeMillis());
|
.orElse("asset_" + System.currentTimeMillis());
|
||||||
|
|
||||||
boolean isAudio = optimized.mediaType().startsWith("audio/");
|
boolean isAudio = optimized.mediaType().startsWith("audio/");
|
||||||
double defaultWidth = isAudio ? 400 : 640;
|
boolean isCode = isCodeMediaType(optimized.mediaType()) || isCodeMediaType(mediaType);
|
||||||
double defaultHeight = isAudio ? 80 : 360;
|
double defaultWidth = isAudio ? 400 : isCode ? 480 : 640;
|
||||||
|
double defaultHeight = isAudio ? 80 : isCode ? 270 : 360;
|
||||||
double width = optimized.width() > 0 ? optimized.width() : defaultWidth;
|
double width = optimized.width() > 0 ? optimized.width() : defaultWidth;
|
||||||
double height = optimized.height() > 0 ? optimized.height() : defaultHeight;
|
double height = optimized.height() > 0 ? optimized.height() : defaultHeight;
|
||||||
|
|
||||||
@@ -362,6 +363,14 @@ public class ChannelDirectoryService {
|
|||||||
return value == null ? null : value.toLowerCase(Locale.ROOT);
|
return value == null ? null : value.toLowerCase(Locale.ROOT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isCodeMediaType(String mediaType) {
|
||||||
|
if (mediaType == null || mediaType.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String normalized = mediaType.toLowerCase(Locale.ROOT);
|
||||||
|
return normalized.startsWith("application/javascript") || normalized.startsWith("text/javascript");
|
||||||
|
}
|
||||||
|
|
||||||
private String topicFor(String broadcaster) {
|
private String topicFor(String broadcaster) {
|
||||||
return "/topic/channel/" + broadcaster.toLowerCase(Locale.ROOT);
|
return "/topic/channel/" + broadcaster.toLowerCase(Locale.ROOT);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ public class MediaDetectionService {
|
|||||||
Map.entry("mov", "video/quicktime"),
|
Map.entry("mov", "video/quicktime"),
|
||||||
Map.entry("mp3", "audio/mpeg"),
|
Map.entry("mp3", "audio/mpeg"),
|
||||||
Map.entry("wav", "audio/wav"),
|
Map.entry("wav", "audio/wav"),
|
||||||
Map.entry("ogg", "audio/ogg")
|
Map.entry("ogg", "audio/ogg"),
|
||||||
|
Map.entry("js", "application/javascript"),
|
||||||
|
Map.entry("mjs", "text/javascript")
|
||||||
);
|
);
|
||||||
private static final Set<String> ALLOWED_MEDIA_TYPES = Set.copyOf(EXTENSION_TYPES.values());
|
private static final Set<String> ALLOWED_MEDIA_TYPES = Set.copyOf(EXTENSION_TYPES.values());
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,10 @@ public class MediaOptimizationService {
|
|||||||
return new OptimizedAsset(bytes, mediaType, 0, 0, null);
|
return new OptimizedAsset(bytes, mediaType, 0, 0, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mediaType.startsWith("application/javascript") || mediaType.startsWith("text/javascript")) {
|
||||||
|
return new OptimizedAsset(bytes, mediaType, 0, 0, null);
|
||||||
|
}
|
||||||
|
|
||||||
BufferedImage image = ImageIO.read(new ByteArrayInputStream(bytes));
|
BufferedImage image = ImageIO.read(new ByteArrayInputStream(bytes));
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
return new OptimizedAsset(bytes, mediaType, image.getWidth(), image.getHeight(), null);
|
return new OptimizedAsset(bytes, mediaType, image.getWidth(), image.getHeight(), null);
|
||||||
|
|||||||
@@ -1755,6 +1755,14 @@ button:disabled:hover {
|
|||||||
color: #fbbf24;
|
color: #fbbf24;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.code-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #60a5fa;
|
||||||
|
}
|
||||||
|
|
||||||
.sr-only {
|
.sr-only {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const selectedAssetMeta = document.getElementById("selected-asset-meta");
|
|||||||
const selectedAssetResolution = document.getElementById("selected-asset-resolution");
|
const selectedAssetResolution = document.getElementById("selected-asset-resolution");
|
||||||
const selectedAssetIdLabel = document.getElementById("selected-asset-id");
|
const selectedAssetIdLabel = document.getElementById("selected-asset-id");
|
||||||
const selectedAssetBadges = document.getElementById("selected-asset-badges");
|
const selectedAssetBadges = document.getElementById("selected-asset-badges");
|
||||||
|
const selectedEditBtn = document.getElementById("selected-asset-edit");
|
||||||
const selectedVisibilityBtn = document.getElementById("selected-asset-visibility");
|
const selectedVisibilityBtn = document.getElementById("selected-asset-visibility");
|
||||||
const selectedDeleteBtn = document.getElementById("selected-asset-delete");
|
const selectedDeleteBtn = document.getElementById("selected-asset-delete");
|
||||||
const assetActionRow = document.getElementById("asset-actions");
|
const assetActionRow = document.getElementById("asset-actions");
|
||||||
@@ -111,7 +112,7 @@ function cancelPendingTransform(assetId) {
|
|||||||
|
|
||||||
function ensureLayerPosition(assetId, placement = "keep") {
|
function ensureLayerPosition(assetId, placement = "keep") {
|
||||||
const asset = assets.get(assetId);
|
const asset = assets.get(assetId);
|
||||||
if (asset && isAudioAsset(asset)) {
|
if (asset && (isAudioAsset(asset) || isCodeAsset(asset))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const existingIndex = layerOrder.indexOf(assetId);
|
const existingIndex = layerOrder.indexOf(assetId);
|
||||||
@@ -132,10 +133,10 @@ function ensureLayerPosition(assetId, placement = "keep") {
|
|||||||
function getLayerOrder() {
|
function getLayerOrder() {
|
||||||
layerOrder = layerOrder.filter((id) => {
|
layerOrder = layerOrder.filter((id) => {
|
||||||
const asset = assets.get(id);
|
const asset = assets.get(id);
|
||||||
return asset && !isAudioAsset(asset);
|
return asset && !isAudioAsset(asset) && !isCodeAsset(asset);
|
||||||
});
|
});
|
||||||
assets.forEach((asset, id) => {
|
assets.forEach((asset, id) => {
|
||||||
if (isAudioAsset(asset)) {
|
if (isAudioAsset(asset) || isCodeAsset(asset)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!layerOrder.includes(id)) {
|
if (!layerOrder.includes(id)) {
|
||||||
@@ -157,6 +158,12 @@ function getAudioAssets() {
|
|||||||
.sort((a, b) => (b.createdAtMs || 0) - (a.createdAtMs || 0));
|
.sort((a, b) => (b.createdAtMs || 0) - (a.createdAtMs || 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCodeAssets() {
|
||||||
|
return Array.from(assets.values())
|
||||||
|
.filter((asset) => isCodeAsset(asset))
|
||||||
|
.sort((a, b) => (b.createdAtMs || 0) - (a.createdAtMs || 0));
|
||||||
|
}
|
||||||
|
|
||||||
function getRenderOrder() {
|
function getRenderOrder() {
|
||||||
return [...getLayerOrder()]
|
return [...getLayerOrder()]
|
||||||
.reverse()
|
.reverse()
|
||||||
@@ -166,7 +173,7 @@ function getRenderOrder() {
|
|||||||
|
|
||||||
function getLayerValue(assetId) {
|
function getLayerValue(assetId) {
|
||||||
const asset = assets.get(assetId);
|
const asset = assets.get(assetId);
|
||||||
if (asset && isAudioAsset(asset)) {
|
if (asset && (isAudioAsset(asset) || isCodeAsset(asset))) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const order = getLayerOrder();
|
const order = getLayerOrder();
|
||||||
@@ -645,6 +652,11 @@ function drawAsset(asset) {
|
|||||||
ctx.translate(renderState.x + halfWidth, renderState.y + halfHeight);
|
ctx.translate(renderState.x + halfWidth, renderState.y + halfHeight);
|
||||||
ctx.rotate((renderState.rotation * Math.PI) / 180);
|
ctx.rotate((renderState.rotation * Math.PI) / 180);
|
||||||
|
|
||||||
|
if (isCodeAsset(asset)) {
|
||||||
|
ctx.restore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isAudioAsset(asset)) {
|
if (isAudioAsset(asset)) {
|
||||||
autoStartAudio(asset);
|
autoStartAudio(asset);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
@@ -930,6 +942,23 @@ function isAudioAsset(asset) {
|
|||||||
return type.startsWith("audio/");
|
return type.startsWith("audio/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCodeAsset(asset) {
|
||||||
|
const type = (asset?.mediaType || asset?.originalMediaType || "").toLowerCase();
|
||||||
|
return type.startsWith("application/javascript") || type.startsWith("text/javascript");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isJavaScriptFile(file) {
|
||||||
|
if (!file) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const type = (file.type || "").toLowerCase();
|
||||||
|
if (type.includes("javascript")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const name = (file.name || "").toLowerCase();
|
||||||
|
return name.endsWith(".js") || name.endsWith(".mjs");
|
||||||
|
}
|
||||||
|
|
||||||
function isVideoElement(element) {
|
function isVideoElement(element) {
|
||||||
return element && element.tagName === "VIDEO";
|
return element && element.tagName === "VIDEO";
|
||||||
}
|
}
|
||||||
@@ -939,6 +968,9 @@ function getDisplayMediaType(asset) {
|
|||||||
if (!raw) {
|
if (!raw) {
|
||||||
return "Unknown";
|
return "Unknown";
|
||||||
}
|
}
|
||||||
|
if (isCodeAsset(asset)) {
|
||||||
|
return "JavaScript";
|
||||||
|
}
|
||||||
const parts = raw.split("/");
|
const parts = raw.split("/");
|
||||||
return parts.length > 1 ? parts[1].toUpperCase() : raw.toUpperCase();
|
return parts.length > 1 ? parts[1].toUpperCase() : raw.toUpperCase();
|
||||||
}
|
}
|
||||||
@@ -1265,8 +1297,9 @@ function renderAssetList() {
|
|||||||
list.appendChild(createPendingListItem(pending));
|
list.appendChild(createPendingListItem(pending));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const codeAssets = getCodeAssets();
|
||||||
const audioAssets = getAudioAssets();
|
const audioAssets = getAudioAssets();
|
||||||
const sortedAssets = [...audioAssets, ...getAssetsByLayer()];
|
const sortedAssets = [...codeAssets, ...audioAssets, ...getAssetsByLayer()];
|
||||||
sortedAssets.forEach((asset) => {
|
sortedAssets.forEach((asset) => {
|
||||||
const li = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
li.className = "asset-item";
|
li.className = "asset-item";
|
||||||
@@ -1285,13 +1318,28 @@ function renderAssetList() {
|
|||||||
const name = document.createElement("strong");
|
const name = document.createElement("strong");
|
||||||
name.textContent = asset.name || `Asset ${asset.id.slice(0, 6)}`;
|
name.textContent = asset.name || `Asset ${asset.id.slice(0, 6)}`;
|
||||||
const details = document.createElement("small");
|
const details = document.createElement("small");
|
||||||
details.textContent = `${Math.round(asset.width)}x${Math.round(asset.height)}`;
|
details.textContent = isCodeAsset(asset)
|
||||||
|
? "JavaScript"
|
||||||
|
: `${Math.round(asset.width)}x${Math.round(asset.height)}`;
|
||||||
meta.appendChild(name);
|
meta.appendChild(name);
|
||||||
meta.appendChild(details);
|
meta.appendChild(details);
|
||||||
|
|
||||||
const actions = document.createElement("div");
|
const actions = document.createElement("div");
|
||||||
actions.className = "actions";
|
actions.className = "actions";
|
||||||
|
|
||||||
|
if (isCodeAsset(asset)) {
|
||||||
|
const editBtn = document.createElement("button");
|
||||||
|
editBtn.type = "button";
|
||||||
|
editBtn.className = "ghost icon-button";
|
||||||
|
editBtn.innerHTML = '<i class="fa-solid fa-code"></i>';
|
||||||
|
editBtn.title = "Edit script";
|
||||||
|
editBtn.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
openCodeAssetEditor(asset);
|
||||||
|
});
|
||||||
|
actions.appendChild(editBtn);
|
||||||
|
}
|
||||||
|
|
||||||
if (isAudioAsset(asset)) {
|
if (isAudioAsset(asset)) {
|
||||||
const playBtn = document.createElement("button");
|
const playBtn = document.createElement("button");
|
||||||
playBtn.type = "button";
|
playBtn.type = "button";
|
||||||
@@ -1313,7 +1361,7 @@ function renderAssetList() {
|
|||||||
actions.appendChild(playBtn);
|
actions.appendChild(playBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAudioAsset(asset)) {
|
if (!isAudioAsset(asset) && !isCodeAsset(asset)) {
|
||||||
const toggleBtn = document.createElement("button");
|
const toggleBtn = document.createElement("button");
|
||||||
toggleBtn.type = "button";
|
toggleBtn.type = "button";
|
||||||
toggleBtn.className = "ghost icon-button";
|
toggleBtn.className = "ghost icon-button";
|
||||||
@@ -1406,6 +1454,12 @@ function updatePlayButtonIcon(button, isLooping, isPlayingLoop) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createPreviewElement(asset) {
|
function createPreviewElement(asset) {
|
||||||
|
if (isCodeAsset(asset)) {
|
||||||
|
const icon = document.createElement("div");
|
||||||
|
icon.className = "asset-preview code-icon";
|
||||||
|
icon.innerHTML = '<i class="fa-solid fa-code" aria-hidden="true"></i>';
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
if (isAudioAsset(asset)) {
|
if (isAudioAsset(asset)) {
|
||||||
const icon = document.createElement("div");
|
const icon = document.createElement("div");
|
||||||
icon.className = "asset-preview audio-icon";
|
icon.className = "asset-preview audio-icon";
|
||||||
@@ -1621,7 +1675,7 @@ 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);
|
||||||
}
|
}
|
||||||
const hideLayout = isAudioAsset(asset);
|
const hideLayout = isAudioAsset(asset) || isCodeAsset(asset);
|
||||||
if (layoutSection) {
|
if (layoutSection) {
|
||||||
layoutSection.classList.toggle("hidden", hideLayout);
|
layoutSection.classList.toggle("hidden", hideLayout);
|
||||||
const layoutControls = layoutSection.querySelectorAll("input, button");
|
const layoutControls = layoutSection.querySelectorAll("input, button");
|
||||||
@@ -1633,7 +1687,8 @@ function updateSelectedAssetControls(asset = getSelectedAsset()) {
|
|||||||
if (assetActionButtons.length) {
|
if (assetActionButtons.length) {
|
||||||
assetActionButtons.forEach((button) => {
|
assetActionButtons.forEach((button) => {
|
||||||
const allowForAudio = button.dataset.audioEnabled === "true";
|
const allowForAudio = button.dataset.audioEnabled === "true";
|
||||||
const disableButton = hideLayout && !allowForAudio;
|
const allowForCode = button.dataset.codeEnabled === "true";
|
||||||
|
const disableButton = hideLayout && !(allowForAudio || allowForCode);
|
||||||
button.disabled = disableButton;
|
button.disabled = disableButton;
|
||||||
button.classList.toggle("disabled", disableButton);
|
button.classList.toggle("disabled", disableButton);
|
||||||
});
|
});
|
||||||
@@ -1703,8 +1758,13 @@ function updateSelectedAssetSummary(asset) {
|
|||||||
}
|
}
|
||||||
if (selectedAssetResolution) {
|
if (selectedAssetResolution) {
|
||||||
if (asset) {
|
if (asset) {
|
||||||
|
if (isCodeAsset(asset)) {
|
||||||
|
selectedAssetResolution.textContent = "";
|
||||||
|
selectedAssetResolution.classList.add("hidden");
|
||||||
|
} else {
|
||||||
selectedAssetResolution.textContent = `${Math.round(asset.width)}×${Math.round(asset.height)}`;
|
selectedAssetResolution.textContent = `${Math.round(asset.width)}×${Math.round(asset.height)}`;
|
||||||
selectedAssetResolution.classList.remove("hidden");
|
selectedAssetResolution.classList.remove("hidden");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
selectedAssetResolution.textContent = "";
|
selectedAssetResolution.textContent = "";
|
||||||
selectedAssetResolution.classList.add("hidden");
|
selectedAssetResolution.classList.add("hidden");
|
||||||
@@ -1723,7 +1783,7 @@ function updateSelectedAssetSummary(asset) {
|
|||||||
selectedAssetBadges.innerHTML = "";
|
selectedAssetBadges.innerHTML = "";
|
||||||
if (asset) {
|
if (asset) {
|
||||||
selectedAssetBadges.appendChild(createBadge(getDisplayMediaType(asset)));
|
selectedAssetBadges.appendChild(createBadge(getDisplayMediaType(asset)));
|
||||||
const aspectLabel = !isAudioAsset(asset) ? formatAspectRatioLabel(asset) : "";
|
const aspectLabel = !isAudioAsset(asset) && !isCodeAsset(asset) ? formatAspectRatioLabel(asset) : "";
|
||||||
if (aspectLabel) {
|
if (aspectLabel) {
|
||||||
selectedAssetBadges.appendChild(createBadge(aspectLabel, "subtle"));
|
selectedAssetBadges.appendChild(createBadge(aspectLabel, "subtle"));
|
||||||
}
|
}
|
||||||
@@ -1733,6 +1793,13 @@ function updateSelectedAssetSummary(asset) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (selectedEditBtn) {
|
||||||
|
selectedEditBtn.disabled = !asset || !isCodeAsset(asset);
|
||||||
|
selectedEditBtn.onclick = null;
|
||||||
|
if (asset && isCodeAsset(asset)) {
|
||||||
|
selectedEditBtn.onclick = () => openCodeAssetEditor(asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (selectedVisibilityBtn) {
|
if (selectedVisibilityBtn) {
|
||||||
selectedVisibilityBtn.disabled = !asset;
|
selectedVisibilityBtn.disabled = !asset;
|
||||||
selectedVisibilityBtn.onclick = null;
|
selectedVisibilityBtn.onclick = null;
|
||||||
@@ -1754,6 +1821,10 @@ function updateSelectedAssetSummary(asset) {
|
|||||||
}
|
}
|
||||||
triggerAudioPlayback(asset, nextPlay);
|
triggerAudioPlayback(asset, nextPlay);
|
||||||
};
|
};
|
||||||
|
} else if (asset && isCodeAsset(asset)) {
|
||||||
|
selectedVisibilityBtn.disabled = true;
|
||||||
|
selectedVisibilityBtn.title = "Script assets do not render on the canvas";
|
||||||
|
selectedVisibilityBtn.innerHTML = '<i class="fa-solid fa-eye-slash"></i>';
|
||||||
} else if (asset) {
|
} else if (asset) {
|
||||||
selectedVisibilityBtn.title = asset.hidden ? "Show asset" : "Hide asset";
|
selectedVisibilityBtn.title = asset.hidden ? "Show asset" : "Hide asset";
|
||||||
selectedVisibilityBtn.innerHTML = `<i class="fa-solid ${asset.hidden ? "fa-eye" : "fa-eye-slash"}"></i>`;
|
selectedVisibilityBtn.innerHTML = `<i class="fa-solid ${asset.hidden ? "fa-eye" : "fa-eye-slash"}"></i>`;
|
||||||
@@ -1769,6 +1840,61 @@ function updateSelectedAssetSummary(asset) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openCodeAssetEditor(asset) {
|
||||||
|
if (!asset) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const modal = document.getElementById("custom-asset-modal");
|
||||||
|
const nameInput = document.getElementById("custom-asset-name");
|
||||||
|
const codeInput = document.getElementById("custom-asset-code");
|
||||||
|
const errorWrapper = document.getElementById("custom-asset-error");
|
||||||
|
const errorTitle = document.getElementById("js-error-title");
|
||||||
|
const errorDetails = document.getElementById("js-error-details");
|
||||||
|
|
||||||
|
if (errorWrapper) {
|
||||||
|
errorWrapper.classList.add("hidden");
|
||||||
|
}
|
||||||
|
if (errorTitle) {
|
||||||
|
errorTitle.textContent = "";
|
||||||
|
}
|
||||||
|
if (errorDetails) {
|
||||||
|
errorDetails.textContent = "";
|
||||||
|
}
|
||||||
|
if (nameInput) {
|
||||||
|
nameInput.value = asset.name || "";
|
||||||
|
}
|
||||||
|
if (codeInput) {
|
||||||
|
codeInput.value = "";
|
||||||
|
codeInput.placeholder = "Loading script...";
|
||||||
|
codeInput.disabled = true;
|
||||||
|
codeInput.dataset.assetId = asset.id;
|
||||||
|
}
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(asset.url)
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to load script");
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
})
|
||||||
|
.then((text) => {
|
||||||
|
if (codeInput) {
|
||||||
|
codeInput.disabled = false;
|
||||||
|
codeInput.value = text;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (codeInput) {
|
||||||
|
codeInput.disabled = false;
|
||||||
|
codeInput.value = "";
|
||||||
|
}
|
||||||
|
showToast("Unable to load script content.", "error");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function ensureDurationMetadata(asset) {
|
function ensureDurationMetadata(asset) {
|
||||||
if (!asset || hasDuration(asset) || (!isVideoAsset(asset) && !isAudioAsset(asset))) {
|
if (!asset || hasDuration(asset) || (!isVideoAsset(asset) && !isAudioAsset(asset))) {
|
||||||
return;
|
return;
|
||||||
@@ -2092,7 +2218,7 @@ function uploadAsset(file = null) {
|
|||||||
const fileInput = document.getElementById("asset-file");
|
const fileInput = document.getElementById("asset-file");
|
||||||
const selectedFile = file || (fileInput?.files && fileInput.files.length ? fileInput.files[0] : null);
|
const selectedFile = file || (fileInput?.files && fileInput.files.length ? fileInput.files[0] : null);
|
||||||
if (!selectedFile) {
|
if (!selectedFile) {
|
||||||
showToast("Choose an image, GIF, video, or audio file to upload.", "info");
|
showToast("Choose an image, GIF, video, audio, or JavaScript file to upload.", "info");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (selectedFile.size > UPLOAD_LIMIT_BYTES) {
|
if (selectedFile.size > UPLOAD_LIMIT_BYTES) {
|
||||||
@@ -2100,6 +2226,32 @@ function uploadAsset(file = null) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isJavaScriptFile(selectedFile)) {
|
||||||
|
selectedFile
|
||||||
|
.text()
|
||||||
|
.then((source) => {
|
||||||
|
if (typeof getUserJavaScriptSourceError === "function") {
|
||||||
|
const error = getUserJavaScriptSourceError(source);
|
||||||
|
if (error) {
|
||||||
|
showToast(`JavaScript error: ${error.title}`, "error");
|
||||||
|
if (fileNameLabel) {
|
||||||
|
fileNameLabel.textContent = "Upload failed";
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
beginAssetUpload(selectedFile);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showToast("Unable to read the JavaScript file. Please try again.", "error");
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
beginAssetUpload(selectedFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
function beginAssetUpload(selectedFile) {
|
||||||
const pendingId = addPendingUpload(selectedFile.name);
|
const pendingId = addPendingUpload(selectedFile.name);
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
data.append("file", selectedFile);
|
data.append("file", selectedFile);
|
||||||
@@ -2121,10 +2273,11 @@ function uploadAsset(file = null) {
|
|||||||
showToast("Upload received. Processing asset...", "success");
|
showToast("Upload received. Processing asset...", "success");
|
||||||
updatePendingUpload(pendingId, { status: "processing" });
|
updatePendingUpload(pendingId, { status: "processing" });
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((e) => {
|
||||||
if (fileNameLabel) {
|
if (fileNameLabel) {
|
||||||
fileNameLabel.textContent = "Upload failed";
|
fileNameLabel.textContent = "Upload failed";
|
||||||
}
|
}
|
||||||
|
console.error(e);
|
||||||
removePendingUpload(pendingId);
|
removePendingUpload(pendingId);
|
||||||
showToast("Upload failed. Please try again with a supported file.", "error");
|
showToast("Upload failed. Please try again with a supported file.", "error");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ audioUnlockEvents.forEach((eventName) => {
|
|||||||
|
|
||||||
function ensureLayerPosition(assetId, placement = "keep") {
|
function ensureLayerPosition(assetId, placement = "keep") {
|
||||||
const asset = assets.get(assetId);
|
const asset = assets.get(assetId);
|
||||||
if (asset && isAudioAsset(asset)) {
|
if (asset && (isAudioAsset(asset) || isCodeAsset(asset))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const existingIndex = layerOrder.indexOf(assetId);
|
const existingIndex = layerOrder.indexOf(assetId);
|
||||||
@@ -58,10 +58,10 @@ function ensureLayerPosition(assetId, placement = "keep") {
|
|||||||
function getLayerOrder() {
|
function getLayerOrder() {
|
||||||
layerOrder = layerOrder.filter((id) => {
|
layerOrder = layerOrder.filter((id) => {
|
||||||
const asset = assets.get(id);
|
const asset = assets.get(id);
|
||||||
return asset && !isAudioAsset(asset);
|
return asset && !isAudioAsset(asset) && !isCodeAsset(asset);
|
||||||
});
|
});
|
||||||
assets.forEach((asset, id) => {
|
assets.forEach((asset, id) => {
|
||||||
if (isAudioAsset(asset)) {
|
if (isAudioAsset(asset) || isCodeAsset(asset)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!layerOrder.includes(id)) {
|
if (!layerOrder.includes(id)) {
|
||||||
@@ -345,6 +345,11 @@ function drawAsset(asset) {
|
|||||||
ctx.translate(renderState.x + halfWidth, renderState.y + halfHeight);
|
ctx.translate(renderState.x + halfWidth, renderState.y + halfHeight);
|
||||||
ctx.rotate((renderState.rotation * Math.PI) / 180);
|
ctx.rotate((renderState.rotation * Math.PI) / 180);
|
||||||
|
|
||||||
|
if (isCodeAsset(asset)) {
|
||||||
|
ctx.restore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isAudioAsset(asset)) {
|
if (isAudioAsset(asset)) {
|
||||||
if (!asset.hidden) {
|
if (!asset.hidden) {
|
||||||
autoStartAudio(asset);
|
autoStartAudio(asset);
|
||||||
@@ -433,6 +438,11 @@ function isAudioAsset(asset) {
|
|||||||
return asset?.mediaType?.startsWith("audio/");
|
return asset?.mediaType?.startsWith("audio/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCodeAsset(asset) {
|
||||||
|
const type = (asset?.mediaType || asset?.originalMediaType || "").toLowerCase();
|
||||||
|
return type.startsWith("application/javascript") || type.startsWith("text/javascript");
|
||||||
|
}
|
||||||
|
|
||||||
function isVideoElement(element) {
|
function isVideoElement(element) {
|
||||||
return element?.tagName === "VIDEO";
|
return element?.tagName === "VIDEO";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
id="asset-file"
|
id="asset-file"
|
||||||
class="file-input-field"
|
class="file-input-field"
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*,video/*,audio/*"
|
accept="image/*,video/*,audio/*,application/javascript,text/javascript,.js,.mjs"
|
||||||
onchange="handleFileSelection(this)"
|
onchange="handleFileSelection(this)"
|
||||||
/>
|
/>
|
||||||
<label for="asset-file" class="file-input-trigger">
|
<label for="asset-file" class="file-input-trigger">
|
||||||
@@ -312,6 +312,16 @@
|
|||||||
>
|
>
|
||||||
<i class="fa-solid fa-rotate-right"></i>
|
<i class="fa-solid fa-rotate-right"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
id="selected-asset-edit"
|
||||||
|
class="secondary"
|
||||||
|
type="button"
|
||||||
|
title="Edit script"
|
||||||
|
disabled
|
||||||
|
data-code-enabled="true"
|
||||||
|
>
|
||||||
|
<i class="fa-solid fa-code"></i>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
id="selected-asset-visibility"
|
id="selected-asset-visibility"
|
||||||
class="secondary"
|
class="secondary"
|
||||||
@@ -329,6 +339,7 @@
|
|||||||
title="Delete asset"
|
title="Delete asset"
|
||||||
disabled
|
disabled
|
||||||
data-audio-enabled="true"
|
data-audio-enabled="true"
|
||||||
|
data-code-enabled="true"
|
||||||
>
|
>
|
||||||
<i class="fa-solid fa-trash"></i>
|
<i class="fa-solid fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user